From e5d65f03d978fa379fa126b331be2a918a0174fe Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Oct 2015 16:04:48 -0400 Subject: [PATCH] Retire stackforge/satori --- .coveragerc | 7 - .gitignore | 59 --- .gitreview | 4 - .testr.conf | 8 - HACKING.rst | 106 ---- LICENSE | 202 -------- MANIFEST.in | 5 - README.rst | 152 +----- doc/.gitignore | 2 - doc/Makefile | 136 ----- doc/README.rst | 55 -- doc/source/conf.py | 413 --------------- doc/source/contributing.rst | 17 - doc/source/index.rst | 94 ---- doc/source/man/satori.rst | 51 -- doc/source/openstack_resources.rst | 78 --- doc/source/releases.rst | 25 - doc/source/schema.rst | 33 -- doc/source/sourcecode/.gitignore | 1 - doc/source/terminology.rst | 32 -- pylintrc | 15 - requirements-py3.txt | 8 - requirements.txt | 11 - run_tests.sh | 49 -- satori/__init__.py | 41 -- satori/bash.py | 257 ---------- satori/common/__init__.py | 1 - satori/common/logging.py | 140 ------ satori/common/popen.py | 23 - satori/common/templating.py | 128 ----- satori/contrib/psexec.py | 557 --------------------- satori/discovery.py | 141 ------ satori/dns.py | 113 ----- satori/errors.py | 116 ----- satori/formats/custom.jinja | 23 - satori/formats/text.jinja | 54 -- satori/serviceinstall.py | 303 ----------- satori/shell.py | 285 ----------- satori/smb.py | 347 ------------- satori/ssh.py | 512 ------------------- satori/sysinfo/__init__.py | 1 - satori/sysinfo/facter.py | 6 - satori/sysinfo/ohai.py | 6 - satori/sysinfo/ohai_solo.py | 201 -------- satori/sysinfo/posh_ohai.py | 299 ----------- satori/tests/__init__.py | 0 satori/tests/test_bash.py | 208 -------- satori/tests/test_common_logging.py | 58 --- satori/tests/test_common_templating.py | 79 --- satori/tests/test_dns.py | 294 ----------- satori/tests/test_formats_text.py | 188 ------- satori/tests/test_shell.py | 159 ------ satori/tests/test_ssh.py | 666 ------------------------- satori/tests/test_sysinfo_ohai_solo.py | 261 ---------- satori/tests/test_sysinfo_posh_ohai.py | 85 ---- satori/tests/test_utils.py | 131 ----- satori/tests/utils.py | 60 --- satori/tunnel.py | 167 ------- satori/utils.py | 221 -------- setup.cfg | 43 -- setup.py | 22 - test-requirements.txt | 15 - tools/install_venv.py | 66 --- tools/install_venv_common.py | 174 ------- tools/with_venv.sh | 4 - tox.ini | 52 -- 66 files changed, 5 insertions(+), 8065 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .gitignore delete mode 100644 .gitreview delete mode 100644 .testr.conf delete mode 100644 HACKING.rst delete mode 100644 LICENSE delete mode 100644 MANIFEST.in delete mode 100644 doc/.gitignore delete mode 100644 doc/Makefile delete mode 100644 doc/README.rst delete mode 100644 doc/source/conf.py delete mode 100644 doc/source/contributing.rst delete mode 100644 doc/source/index.rst delete mode 100644 doc/source/man/satori.rst delete mode 100644 doc/source/openstack_resources.rst delete mode 100644 doc/source/releases.rst delete mode 100644 doc/source/schema.rst delete mode 100644 doc/source/sourcecode/.gitignore delete mode 100644 doc/source/terminology.rst delete mode 100644 pylintrc delete mode 100644 requirements-py3.txt delete mode 100644 requirements.txt delete mode 100755 run_tests.sh delete mode 100644 satori/__init__.py delete mode 100644 satori/bash.py delete mode 100644 satori/common/__init__.py delete mode 100644 satori/common/logging.py delete mode 100644 satori/common/popen.py delete mode 100644 satori/common/templating.py delete mode 100644 satori/contrib/psexec.py delete mode 100644 satori/discovery.py delete mode 100644 satori/dns.py delete mode 100644 satori/errors.py delete mode 100644 satori/formats/custom.jinja delete mode 100644 satori/formats/text.jinja delete mode 100644 satori/serviceinstall.py delete mode 100644 satori/shell.py delete mode 100644 satori/smb.py delete mode 100644 satori/ssh.py delete mode 100644 satori/sysinfo/__init__.py delete mode 100644 satori/sysinfo/facter.py delete mode 100644 satori/sysinfo/ohai.py delete mode 100644 satori/sysinfo/ohai_solo.py delete mode 100644 satori/sysinfo/posh_ohai.py delete mode 100644 satori/tests/__init__.py delete mode 100644 satori/tests/test_bash.py delete mode 100644 satori/tests/test_common_logging.py delete mode 100644 satori/tests/test_common_templating.py delete mode 100644 satori/tests/test_dns.py delete mode 100644 satori/tests/test_formats_text.py delete mode 100644 satori/tests/test_shell.py delete mode 100644 satori/tests/test_ssh.py delete mode 100644 satori/tests/test_sysinfo_ohai_solo.py delete mode 100644 satori/tests/test_sysinfo_posh_ohai.py delete mode 100644 satori/tests/test_utils.py delete mode 100644 satori/tests/utils.py delete mode 100644 satori/tunnel.py delete mode 100644 satori/utils.py delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 test-requirements.txt delete mode 100644 tools/install_venv.py delete mode 100644 tools/install_venv_common.py delete mode 100755 tools/with_venv.sh delete mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index b688dc7..0000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -branch = True -source = satori -omit = satori/tests/* - -[report] -ignore_errors = True diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8c7bc68..0000000 --- a/.gitignore +++ /dev/null @@ -1,59 +0,0 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 -__pycache__ - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -.coverage.* -cover -covhtml -.tox -nosetests.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Rope -*/.ropeproject/* -.ropeproject/* - -*.DS_Store -*.log -*.swo -*.swp -*~ -.satori-venv -.testrepository -.tox -.venv -venv -AUTHORS -build -ChangeLog -dist -doc/build diff --git a/.gitreview b/.gitreview deleted file mode 100644 index 4ca79d8..0000000 --- a/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=stackforge/satori.git diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 6f0e8ca..0000000 --- a/.testr.conf +++ /dev/null @@ -1,8 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ - OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ - OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover -t . ./satori/tests $LISTOPT $IDOPTION - -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index 8744a93..0000000 --- a/HACKING.rst +++ /dev/null @@ -1,106 +0,0 @@ -OpenStack Style Commandments -============================ - -- Step 1: Read the OpenStack Style Commandments - http://docs.openstack.org/developer/hacking/ -- Step 2: Read on - -General -------- -- thou shalt not violate causality in our time cone, or else - -Docstrings ----------- - -Docstrings should ONLY use triple-double-quotes (``"""``) - -Single-line docstrings should NEVER have extraneous whitespace -between enclosing triple-double-quotes. - -Deviation! Sentence fragments do not have punctuation. Specifically in the -command classes the one line docstring is also the help string for that -command and those do not have periods. - - """A one line docstring looks like this""" - -Calling Methods ---------------- - -Deviation! When breaking up method calls due to the 79 char line length limit, -use the alternate 4 space indent. With the first argument on the succeeding -line all arguments will then be vertically aligned. Use the same convention -used with other data structure literals and terminate the method call with -the last argument line ending with a comma and the closing paren on its own -line indented to the starting line level. - - unnecessarily_long_function_name( - 'string one', - 'string two', - kwarg1=constants.ACTIVE, - kwarg2=['a', 'b', 'c'], - ) - -Text encoding -------------- - -Note: this section clearly has not been implemented in this project yet, it is -the intention to do so. - -All text within python code should be of type 'unicode'. - - WRONG: - - >>> s = 'foo' - >>> s - 'foo' - >>> type(s) - - - RIGHT: - - >>> u = u'foo' - >>> u - u'foo' - >>> type(u) - - -Transitions between internal unicode and external strings should always -be immediately and explicitly encoded or decoded. - -All external text that is not explicitly encoded (database storage, -commandline arguments, etc.) should be presumed to be encoded as utf-8. - - WRONG: - - mystring = infile.readline() - myreturnstring = do_some_magic_with(mystring) - outfile.write(myreturnstring) - - RIGHT: - - mystring = infile.readline() - mytext = s.decode('utf-8') - returntext = do_some_magic_with(mytext) - returnstring = returntext.encode('utf-8') - outfile.write(returnstring) - -Python 3.x Compatibility ------------------------- - -OpenStackClient strives to be Python 3.3 compatible. Common guidelines: - -* Convert print statements to functions: print statements should be converted - to an appropriate log or other output mechanism. -* Use six where applicable: x.iteritems is converted to six.iteritems(x) - for example. - -Running Tests -------------- - -Note: Oh boy, are we behind on writing tests. But they are coming! - -The testing system is based on a combination of tox and testr. If you just -want to run the whole suite, run `tox` and all will be fine. However, if -you'd like to dig in a bit more, you might want to learn some things about -testr itself. A basic walkthrough for OpenStack can be found at -http://wiki.openstack.org/testr diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e06d208..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c2334cc..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -graft satori/formats -include setup.py -include setup.cfg -prune satori/tests -global-exclude *.pyc *.sdx *.log *.db *.swp diff --git a/README.rst b/README.rst index dab5eb8..9006052 100644 --- a/README.rst +++ b/README.rst @@ -1,149 +1,7 @@ +This project is no longer maintained. -================================ -Satori - Configuration Discovery -================================ +The contents of this repository are still available in the Git source code +management system. To see the contents of this repository before it reached +its end of life, please check out the previous commit with +"git checkout HEAD^1". -Satori provides configuration discovery for existing infrastructure. It is -a `related OpenStack project`_. - -The charter for the project is to focus narrowly on discovering pre-existing -infrastructure and installed or running software. For example, given a URL and -some credentials, discover which resources (load balancer and servers) the URL -is hosted on and what software is running on those servers. - -Configuration discovery output could be used for: - -* Configuration analysis (ex. compared against a library of best practices) -* Configuration monitoring (ex. has the configuration changed?) -* Troubleshooting -* Heat Template generation -* Solum Application creation/import -* Creation of Chef recipes/cookbooks, Puppet modules, Ansible playbooks, setup - scripts, etc.. - -Getting Started -=============== - -Run discovery on the local system:: - - $ pip install satori - - $ satori localhost --system-info=ohai-solo -F json - # Installs and runs ohai-solo, outputs the data as JSON - - -Run against a URL with OpenStack credentials:: - - $ pip install satori - - $ satori https://www.foo.com - Address: - www.foo.com resolves to IPv4 address 192.0.2.24 - Domain: foo.com - Registrar: TUCOWS, INC. - Nameservers: NS1.DIGIMEDIA.COM, NS2.DIGIMEDIA.COM - Expires: 457 days - Host not found - -Deeper discovery is available if the network location (IP or hostname) is -hosted on an OpenStack cloud tenant that Satori can access. - -Cloud settings can be passed in on the command line or via `OpenStack tenant environment -variables`_. - -Run with OpenStack credentials:: - - $ satori 192.0.2.24 --os-username yourname --os-password yadayadayada --os-tenant-name myproject --os-auth-url http://... - -Or:: - - $ export OS_USERNAME=yourname - $ export OS_PASSWORD=yadayadayada - $ export OS_TENANT_NAME=myproject - $ export OS_AUTH_URL=http://... - $ satori foo.com - -Notice the discovery result now contains a ``Host`` section:: - - $ satori 192.0.2.24 --os-username yourname --os-password yadayadayada --os-tenant-name myproject --os-auth-url http://... - Host: - 192.0.2.24 is hosted on a Nova Instance - Instance Information: - URI: https://nova.api.somecloud.com/v2/111222/servers/d9119040-f767-414 - 1-95a4-d4dbf452363a - Name: sampleserver01.foo.com - ID: d9119040-f767-4141-95a4-d4dbf452363a - ip-addresses: - public: - ::ffff:404:404 - 192.0.2.24 - private: - 10.1.1.156 - System Information: - Ubuntu 12.04 installed - Server was rebooted 11 days, 22 hours ago - /dev/xvda1 is using 9% of its inodes. - Running Services: - httpd on 127.0.0.1:8080 - varnishd on 0.0.0.0:80 - sshd on 0.0.0.0:22 - httpd: - Using 7 of 100 MaxClients - -Documentation -============= - -Additional documentation is located in the ``doc/`` directory and is hosted at -http://satori.readthedocs.org/. - -Start Hacking -============= - -We recommend using a virtualenv to install the client. This description -uses the `install virtualenv`_ script to create the virtualenv:: - - $ python tools/install_venv.py - $ source .venv/bin/activate - $ python setup.py develop - -Unit tests can be ran simply by running:: - - $ tox - - # or, just style checks - $ tox -e pep8 - - # or, just python 2.7 checks - $ tox -e py27 - - -Checking test coverage:: - - # Run tests with coverage - $ tox -ecover - - # generate the report - $ coverage html -d covhtml -i - - # open it in a broweser - $ open covhtml/index.html - - -Links -===== -- `OpenStack Wiki`_ -- `Documentation`_ -- `Code`_ -- `Launchpad Project`_ -- `Features`_ -- `Issues`_ - -.. _OpenStack Wiki: https://wiki.openstack.org/Satori -.. _Documentation: http://satori.readthedocs.org/ -.. _OpenStack tenant environment variables: http://docs.openstack.org/developer/python-novaclient/shell.html -.. _related OpenStack project: https://wiki.openstack.org/wiki/ProjectTypes -.. _install virtualenv: https://github.com/stackforge/satori/blob/master/tools/install_venv.py -.. _Code: https://github.com/stackforge/satori -.. _Launchpad Project: https://launchpad.net/satori -.. _Features: https://blueprints.launchpad.net/satori -.. _Issues: https://bugs.launchpad.net/satori/ diff --git a/doc/.gitignore b/doc/.gitignore deleted file mode 100644 index 8647666..0000000 --- a/doc/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target/ -build/ \ No newline at end of file diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 2cdd0f5..0000000 --- a/doc/Makefile +++ /dev/null @@ -1,136 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html pdf dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " pdf to make pdf with rst2pdf" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -pdf: - $(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) $(BUILDDIR)/pdf - @echo - @echo "Build finished. The PDFs are in $(BUILDDIR)/pdf." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/NebulaDocs.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/NebulaDocs.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/NebulaDocs" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/NebulaDocs" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/README.rst b/doc/README.rst deleted file mode 100644 index 4a87359..0000000 --- a/doc/README.rst +++ /dev/null @@ -1,55 +0,0 @@ -=========================== -Building the developer docs -=========================== - -Dependencies -============ - -You'll need to install python *Sphinx* package -package: - -:: - - sudo pip install sphinx - -If you are using the virtualenv you'll need to install them in the -virtualenv. - -Get Help -======== - -Just type make to get help: - -:: - - make - -It will list available build targets. - -Build Doc -========= - -To build the man pages: - -:: - - make man - -To build the developer documentation as HTML: - -:: - - make html - -Type *make* for more formats. - -Test Doc -======== - -If you modify doc files, you can type: - -:: - - make doctest - -to check whether the format has problem. \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100644 index 2e88fa5..0000000 --- a/doc/source/conf.py +++ /dev/null @@ -1,413 +0,0 @@ -# -*- coding: utf-8 -*- -# -# OpenStack Configuration Discovery documentation build configuration file, created -# by sphinx-quickstart on Wed May 16 12:05:58 2012. -# -# This file is execfile()d with the current directory set to its containing -# dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import glob -import os -import re -import sys - -import pbr.version - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) -CONTRIB_DIR = os.path.join(ROOT, 'contrib') -PLUGIN_DIRS = glob.glob(os.path.join(CONTRIB_DIR, '*')) - -sys.path.insert(0, ROOT) -sys.path.insert(0, BASE_DIR) -sys.path = PLUGIN_DIRS + sys.path - - -# -# Automatically write module docs -# -def write_autodoc_index(): - - def get_contrib_sources(): - module_dirs = glob.glob(os.path.join(CONTRIB_DIR, '*')) - module_names = map(os.path.basename, module_dirs) - - return dict( - ('contrib/%s' % module_name, - {'module': module_name, - 'path': os.path.join(CONTRIB_DIR, module_name)} - ) - for module_name in module_names) - - def find_autodoc_modules(module_name, sourcedir): - """Return a list of modules in the SOURCE directory.""" - modlist = [] - os.chdir(os.path.join(sourcedir, module_name)) - print("SEARCHING %s" % sourcedir) - for root, dirs, files in os.walk("."): - for filename in files: - if filename.endswith(".py"): - # remove the pieces of the root - elements = root.split(os.path.sep) - # replace the leading "." with the module name - elements[0] = module_name - # and get the base module name - base, extension = os.path.splitext(filename) - if not (base == "__init__"): - elements.append(base) - result = ".".join(elements) - modlist.append(result) - return modlist - - RSTDIR = os.path.abspath(os.path.join(BASE_DIR, "sourcecode")) - SRCS = {'satori': {'module': 'satori', - 'path': ROOT}} - SRCS.update(get_contrib_sources()) - - EXCLUDED_MODULES = ('satori.doc', - '.*\.tests') - CURRENT_SOURCES = {} - - if not(os.path.exists(RSTDIR)): - os.mkdir(RSTDIR) - CURRENT_SOURCES[RSTDIR] = ['autoindex.rst', '.gitignore'] - - INDEXOUT = open(os.path.join(RSTDIR, "autoindex.rst"), "w") - INDEXOUT.write("=================\n") - INDEXOUT.write("Source Code Index\n") - INDEXOUT.write("=================\n") - - for title, info in SRCS.items(): - path = info['path'] - modulename = info['module'] - sys.stdout.write("Generating source documentation for %s\n" % - title) - INDEXOUT.write("\n%s\n" % title.capitalize()) - INDEXOUT.write("%s\n" % ("=" * len(title),)) - INDEXOUT.write(".. toctree::\n") - INDEXOUT.write(" :maxdepth: 1\n") - INDEXOUT.write("\n") - - MOD_DIR = os.path.join(RSTDIR, title) - CURRENT_SOURCES[MOD_DIR] = [] - if not(os.path.exists(MOD_DIR)): - os.makedirs(MOD_DIR) - for module in find_autodoc_modules(modulename, path): - if any([re.match(exclude, module) - for exclude - in EXCLUDED_MODULES]): - print("Excluded module %s." % module) - continue - mod_path = os.path.join(path, *module.split(".")) - generated_file = os.path.join(MOD_DIR, "%s.rst" % module) - - INDEXOUT.write(" %s/%s\n" % (title, module)) - - # Find the __init__.py module if this is a directory - if os.path.isdir(mod_path): - source_file = ".".join((os.path.join(mod_path, "__init__"), - "py",)) - else: - source_file = ".".join((os.path.join(mod_path), "py")) - - CURRENT_SOURCES[MOD_DIR].append("%s.rst" % module) - # Only generate a new file if the source has changed or we don't - # have a doc file to begin with. - if not os.access(generated_file, os.F_OK) or \ - os.stat(generated_file).st_mtime < \ - os.stat(source_file).st_mtime: - print("Module %s updated, generating new documentation." - % module) - FILEOUT = open(generated_file, "w") - header = "The :mod:`%s` Module" % module - FILEOUT.write("%s\n" % ("=" * len(header),)) - FILEOUT.write("%s\n" % header) - FILEOUT.write("%s\n" % ("=" * len(header),)) - FILEOUT.write(".. automodule:: %s\n" % module) - FILEOUT.write(" :members:\n") - FILEOUT.write(" :undoc-members:\n") - FILEOUT.write(" :show-inheritance:\n") - FILEOUT.write(" :noindex:\n") - FILEOUT.close() - - INDEXOUT.close() - - # Delete auto-generated .rst files for sources which no longer exist - for directory, subdirs, files in list(os.walk(RSTDIR)): - for old_file in files: - if old_file not in CURRENT_SOURCES.get(directory, []): - print("Removing outdated file for %s" % old_file) - os.remove(os.path.join(directory, old_file)) - -write_autodoc_index() - - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) - -# -- General configuration ---------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.pngmath', - 'sphinx.ext.viewcode', - 'sphinx.ext.doctest'] - -todo_include_todos = True - -# Add any paths that contain templates here, relative to this directory. -if os.getenv('HUDSON_PUBLISH_DOCS'): - templates_path = ['_ga', '_templates'] -else: - templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Satori' -copyright = u'2012-2013 OpenStack Foundation' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -version_info = pbr.version.VersionInfo('satori') -# -# The short X.Y version. -version = version_info.version_string() -# The full version, including alpha/beta/rc tags. -release = version_info.release_string() - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['**/#*', '**~', '**/#*#'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -primary_domain = 'py' -nitpicky = False - - -# -- Options for HTML output -------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -html_theme_options = { - "nosidebar": "false" -} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' -git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1" -html_last_updated_fmt = os.popen(git_cmd).read() - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Satoridoc' - - - -# -- Options for LaTeX output ------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]) -# . -latex_documents = [ - ('index', 'Satori.tex', - u'OpenStack Configuration Discovery Documentation', - u'OpenStack', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output ------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ( - 'man/satori', - 'satori', - u'OpenStack Configuration Discovery', - [u'OpenStack contributors'], - 1, - ), -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ----------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'OpenStackConfigurationDiscovery', - u'OpenStack Configuration Discovery Documentation', - u'OpenStack', 'OpenStackConfigurationDiscovery', - 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst deleted file mode 100644 index 4cfbbf3..0000000 --- a/doc/source/contributing.rst +++ /dev/null @@ -1,17 +0,0 @@ -============ -Contributing -============ - -Satori's code is hosted on `GitHub`_. Our development process follows the -`OpenStack Gerrit`_ workflow which is much different than most projects on -Github. - -If you find a problem, please `file a bug`_. Feature additions and design -discussions are taking place in `blueprints`_. `Reviewing code`_ is an easy way -to start contributing. - -.. _OpenStack Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow -.. _GitHub: https://github.com/stackforge/satori -.. _file a bug: https://bugs.launchpad.net/satori -.. _blueprints: https://blueprints.launchpad.net/satori -.. _Reviewing code: https://review.openstack.org/#/q/status:open+project:stackforge/satori,n,z diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index 069e280..0000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,94 +0,0 @@ -================================= -OpenStack Configuration Discovery -================================= - -Satori is a configuration discovery tool for OpenStack and OpenStack tenant hosted applications. - -.. toctree:: - :maxdepth: 1 - - contributing - releases - terminology - schema - satori - - -Get Satori ------------- - -Satori is distributed as a Python package. The pip command will install the -latest version. - -:: - - $ pip install satori - -If you want to install from the latest source code, these commands will fetch -the code and install it. - -:: - - $ git clone https://github.com/stackforge/satori.git - $ cd satori - $ pip install -r requirements.txt - $ sudo python setup.py install - - -Use Satori ------------ - -:: - - $ satori www.foo.com - Domain: foo.com - Registered at TUCOWS DOMAINS INC. - Expires in 475 days. - Name servers: - DNS1.STABLETRANSIT.COM - DNS2.STABLETRANSIT.COM - Address: - www.foo.com resolves to IPv4 address 4.4.4.4 - Host: - 4.4.4.4 (www.foo.com) is hosted on a Nova Instance - Instance Information: - URI: https://nova.api.somecloud.com/v2/111222/servers/d9119040 - Name: sampleserver01.foo.com - ID: d9119040-f767-4141-95a4-d4dbf452363a - ip-addresses: - public: - ::ffff:404:404 - 4.4.4.4 - private: - 10.1.1.156 - Listening Services: - 0.0.0.0:6082 varnishd - 127.0.0.1:8080 apache2 - 127.0.0.1:3306 mysqld - Talking to: - 10.1.1.71 on 27017 - - -Links -===== -- `OpenStack Wiki`_ -- `Code`_ -- `Launchpad Project`_ -- `Features`_ -- `Issues`_ - -.. _OpenStack Wiki: https://wiki.openstack.org/Satori -.. _OpenStack tenant environment variables: http://docs.openstack.org/developer/python-novaclient/shell.html -.. _related OpenStack project: https://wiki.openstack.org/wiki/ProjectTypes -.. _install virtualenv: https://github.com/stackforge/satori/blob/master/tools/install_venv.py -.. _Code: https://github.com/stackforge/satori -.. _Launchpad Project: https://launchpad.net/satori -.. _Features: https://blueprints.launchpad.net/satori -.. _Issues: https://bugs.launchpad.net/satori/ - -Index ------ - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/doc/source/man/satori.rst b/doc/source/man/satori.rst deleted file mode 100644 index eab82e0..0000000 --- a/doc/source/man/satori.rst +++ /dev/null @@ -1,51 +0,0 @@ -==================== -:program:`satori` -==================== - -OpenStack Configuration Discovery - -SYNOPSIS -======== - -:program:`satori` - - -DESCRIPTION -=========== - -:program:`satori` provides configuration discovery. - - - -OPTIONS -======= - -:program:`satori` has no options yet - - -AUTHORS -======= - -Please refer to the AUTHORS file distributed with Satori. - - -COPYRIGHT -========= - -Copyright 2011-2013 OpenStack Foundation and the authors listed in the AUTHORS file. - - -LICENSE -======= - -http://www.apache.org/licenses/LICENSE-2.0 - - -SEE ALSO -======== - -The `Satori page `_ -in the `OpenStack Wiki `_ contains further -documentation. -s -The individual OpenStack project CLIs, the OpenStack API references. \ No newline at end of file diff --git a/doc/source/openstack_resources.rst b/doc/source/openstack_resources.rst deleted file mode 100644 index 156166b..0000000 --- a/doc/source/openstack_resources.rst +++ /dev/null @@ -1,78 +0,0 @@ -================================= -OpenStack Control Plane Discovery -================================= - -Satori supports :ref:`control plane ` discovery of -resources that belong to an OpenStack tenant. To discover OpenStack specific -information for a resource, provide credentials to Satori for the tenant that -owns the resource. - - -OpenStack Credentials -===================== - -OpenStack credentials can be provided on the command line or injected into -shell environment variables. Satori reuses the `OpenStack Nova conventions`_ for -environment variables since many Satori users also use the `nova`_ program. - -Use the export command to store the credentials in the shell environment: - -:: - $ export OS_USERNAME=yourname - $ export OS_PASSWORD=yadayadayada - $ export OS_TENANT_NAME=myproject - $ export OS_AUTH_URL=http://... - $ satori foo.com - -Alternatively, the credentials can be passed on the command line: - -:: - $ satori foo.com \ - --os-username yourname \ - --os-password yadayadayada \ - --os-tenant-name myproject \ - --os-auth-url http://... - - -Discovered Host -=============== - -If the domain name or IP address provided belongs to the authenticated -tenant, the OpenStack resource data (Server ID, IPs, etc) will be -returned. In this example, the OpenStack credentials were provided via -environment variables. The "Host" section is only available because the -control plane discovery was possible using the OpenStack credentials. - -:: - - $ satori www.foo.com - Domain: foo.com - Registered at TUCOWS DOMAINS INC. - Expires in 475 days. - Name servers: - DNS1.STABLETRANSIT.COM - DNS2.STABLETRANSIT.COM - Address: - www.foo.com resolves to IPv4 address 192.0.2.10 - Host: - 192.0.2.10 (www.foo.com) is hosted on a Nova Instance - Instance Information: - URI: https://nova.api.somecloud.com/v2/111222/servers/d9119040-f767-414 - 1-95a4-d4dbf452363a - Name: sampleserver01.foo.com - ID: d9119040-f767-4141-95a4-d4dbf452363a - ip-addresses: - public: - ::ffff:404:404 - 192.0.2.10 - private: - 10.1.1.156 - Listening Services: - 0.0.0.0:6082 varnishd - 127.0.0.1:8080 apache2 - 127.0.0.1:3306 mysqld - Talking to: - 10.1.1.71 on 27017 - -.. _nova: https://github.com/openstack/python-novaclient -.. _OpenStack Nova conventions: https://github.com/openstack/python-novaclient/blob/master/README.rst#id1 diff --git a/doc/source/releases.rst b/doc/source/releases.rst deleted file mode 100644 index 5e54090..0000000 --- a/doc/source/releases.rst +++ /dev/null @@ -1,25 +0,0 @@ -============= -Release Notes -============= - -0.1.4 (20 Mar 2014) -=================== - -* Data plane discovery (logs on to machines) -* Localhost discovery -* SSH module -* Templated output -* Bug fixes - - -0.1.3 (18 Feb 2014) -=================== - -* Bug fixes -* DNS added among other things - - -0.1.0 (28 Jan 2014) -=================== - -* Project setup diff --git a/doc/source/schema.rst b/doc/source/schema.rst deleted file mode 100644 index 5091c05..0000000 --- a/doc/source/schema.rst +++ /dev/null @@ -1,33 +0,0 @@ -====== -Schema -====== - -The following list of fields describes the data returned from Satori. - - -Target -====== - -Target contains the address suplplied to run the discovery. - - -Found -===== - -All data items discovered are returned under the found key. Keys to resources -discovered are also added under found, but the actual resources are stored -under the resources key. - - -Resources -========= - -All resources (servers, load balancers, DNS domains, etc...) are stored under -the resources key. - -Each resource contains the following keys: - -* **key**: a globally unique identifier for the resource (could be a URI) -* **id**: the id in the system that hosts the resource -* **type**: the resource type using Heat or Heat-like resource types -* **data**: any additional fields for that resource diff --git a/doc/source/sourcecode/.gitignore b/doc/source/sourcecode/.gitignore deleted file mode 100644 index 1a6bc85..0000000 --- a/doc/source/sourcecode/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.rst \ No newline at end of file diff --git a/doc/source/terminology.rst b/doc/source/terminology.rst deleted file mode 100644 index b715f0e..0000000 --- a/doc/source/terminology.rst +++ /dev/null @@ -1,32 +0,0 @@ -============= -Terminology -============= - -Opinions -======== - -Opinions are being discussed at https://wiki.openstack.org/wiki/Satori/OpinionsProposal. - -.. _terminology_control_plane: - -Control Plane Discovery -======================= - -Control plane discovery is the process of making API calls to management -systems like OpenStack or IT asset management systems. An external management -system can show relationships between resources that can further improve -the discovery process. For example, a data plane discovery of a single server -will reveal that a server has a storage device attached to it. Control plane -discovery using an OpenStack plugin can reveal the details of the Cinder -volume. - -Satori can load plugins that enable these systems to be queried. - -Data Plane Discovery -==================== - -Data plane discovery is the process of connecting to a resource and using -native tools to extract information. For example, it can provide information -about the user list, installed software and processes that are running. - -Satori can load plugins that enable data plane discovery. diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 02f5209..0000000 --- a/pylintrc +++ /dev/null @@ -1,15 +0,0 @@ -[Messages Control] -# W0511: TODOs in code comments are fine. -# W0142: *args and **kwargs are fine. -disable-msg=W0511,W0142 - -# Don't require docstrings on tests. -no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ - -[Design] -min-public-methods=0 -max-args=6 - -[Master] -#We try to keep contrib files unmodified -ignore=satori/contrib \ No newline at end of file diff --git a/requirements-py3.txt b/requirements-py3.txt deleted file mode 100644 index 4e8c726..0000000 --- a/requirements-py3.txt +++ /dev/null @@ -1,8 +0,0 @@ -iso8601>=0.1.5 -Jinja2>=2.7.1 -paramiko>=1.13.0 # py33 support -pbr>=0.5.21,<1.0 -python-novaclient>=2.15.0 # py33 support -pythonwhois>=2.1.0 # py33 support -six>=1.8.0 -tldextract>=1.2 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d38732a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -impacket>=0.9.11 -ipaddress>=1.0.6 # in stdlib as of python3.3 -iso8601>=0.1.5 -Jinja2>=2.7.1 # bug resolve @2.7.1 -paramiko>=1.12.0 # ecdsa added -pbr>=0.5.21,<1.0 -python-novaclient>=2.6.0.1 # breaks before -pythonwhois>=2.4.3 -six>=1.8.0 # six.moves.shlex introduced -tldextract>=1.2 -argparse diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index be90eee..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -function usage { - echo "Usage: $0 [OPTION]..." - echo "Run satori's test suite(s)" - echo "" - echo " -p, --pep8 Just run pep8" - echo " -h, --help Print this usage message" - echo "" - echo "This script is deprecated and currently retained for compatibility." - echo 'You can run the full test suite for multiple environments by running "tox".' - echo 'You can run tests for only python 2.7 by running "tox -e py27", or run only' - echo 'the pep8 tests with "tox -e pep8".' - exit -} - -command -v tox > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo 'This script requires "tox" to run.' - echo 'You can install it with "pip install tox".' - exit 1; -fi - -just_pep8=0 - -function process_option { - case "$1" in - -h|--help) usage;; - -p|--pep8) let just_pep8=1;; - esac -} - -for arg in "$@"; do - process_option $arg -done - -if [ $just_pep8 -eq 1 ]; then - exec tox -e pep8 -fi - -tox -e py27 $toxargs 2>&1 | tee run_tests.err.log || exit 1 -tox_exit_code=${PIPESTATUS[0]} -if [ 0$tox_exit_code -ne 0 ]; then - exit $tox_exit_code -fi - -if [ -z "$toxargs" ]; then - tox -e pep8 -fi diff --git a/satori/__init__.py b/satori/__init__.py deleted file mode 100644 index 989ced3..0000000 --- a/satori/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Satori main module.""" - -__all__ = ('__version__') - -try: - import eventlet - eventlet.monkey_patch() -except ImportError: - pass - -import pbr.version - -from satori import shell - - -version_info = pbr.version.VersionInfo('satori') -try: - __version__ = version_info.version_string() -except AttributeError: - __version__ = None - - -def discover(address=None): - """Temporary to demo python API. - - TODO(zns): make it real - """ - shell.main(argv=[address]) - return {'address': address, 'other info': '...'} diff --git a/satori/bash.py b/satori/bash.py deleted file mode 100644 index fb5a87f..0000000 --- a/satori/bash.py +++ /dev/null @@ -1,257 +0,0 @@ -# Copyright 2012-2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Shell classes for executing commands on a system. - -Execute commands over ssh or using the python subprocess module. -""" - -import logging -import shlex - -from satori.common import popen -from satori import errors -from satori import smb -from satori import ssh -from satori import utils - -LOG = logging.getLogger(__name__) - - -class ShellMixin(object): - - """Handle platform detection and define execute command.""" - - def execute(self, command, **kwargs): - """Execute a (shell) command on the target. - - :param command: Shell command to be executed - :param with_exit_code: Include the exit_code in the return body. - :param cwd: The child's current directory will be changed - to `cwd` before it is executed. Note that this - directory is not considered when searching the - executable, so you can't specify the program's - path relative to this argument - :returns: a dict with stdin, stdout, and - (optionally), the exit_code of the call - - See SSH.remote_execute(), SMB.remote_execute(), and - LocalShell.execute() for client-specific keyword arguments. - """ - pass - - @property - def platform_info(self): - """Provide distro, version, architecture.""" - pass - - def is_debian(self): - """Indicate whether the system is Debian based. - - Uses the platform_info property. - """ - if not self.platform_info['dist']: - raise errors.UndeterminedPlatform( - 'Unable to determine whether the system is Debian based.') - return self.platform_info['dist'].lower() in ['debian', 'ubuntu'] - - def is_fedora(self): - """Indicate whether the system in Fedora based. - - Uses the platform_info property. - """ - if not self.platform_info['dist']: - raise errors.UndeterminedPlatform( - 'Unable to determine whether the system is Fedora based.') - return (self.platform_info['dist'].lower() in - ['redhat', 'centos', 'fedora', 'el']) - - def is_osx(self): - """Indicate whether the system is Apple OSX based. - - Uses the platform_info property. - """ - if not self.platform_info['dist']: - raise errors.UndeterminedPlatform( - 'Unable to determine whether the system is OS X based.') - return (self.platform_info['dist'].lower() in - ['darwin', 'macosx']) - - def is_windows(self): - """Indicate whether the system is Windows based. - - Uses the platform_info property. - """ - if hasattr(self, '_client'): - if isinstance(self._client, smb.SMBClient): - return True - if not self.platform_info['dist']: - raise errors.UndeterminedPlatform( - 'Unable to determine whether the system is Windows based.') - - return self.platform_info['dist'].startswith('win') - - -class LocalShell(ShellMixin): - - """Execute shell commands on local machine.""" - - def __init__(self, user=None, password=None, interactive=False): - """An interface for executing shell commands locally. - - :param user: The user to execute the command as. - Defaults to the current user. - :param password: The password for `user` - :param interactive: If true, prompt for password if missing. - - """ - self.user = user - self.password = password - self.interactive = interactive - - # properties - self._platform_info = None - - @property - def platform_info(self): - """Return distro, version, and system architecture.""" - if not self._platform_info: - self._platform_info = utils.get_platform_info() - return self._platform_info - - def execute(self, command, **kwargs): - """Execute a command (containing no shell operators) locally. - - :param command: Shell command to be executed. - :param with_exit_code: Include the exit_code in the return body. - Default is False. - :param cwd: The child's current directory will be changed - to `cwd` before it is executed. Note that this - directory is not considered when searching the - executable, so you can't specify the program's - path relative to this argument - :returns: A dict with stdin, stdout, and - (optionally) the exit code. - """ - cwd = kwargs.get('cwd') - with_exit_code = kwargs.get('with_exit_code') - spipe = popen.PIPE - - cmd = shlex.split(command) - LOG.debug("Executing `%s` on local machine", command) - result = popen.popen( - cmd, stdout=spipe, stderr=spipe, cwd=cwd) - out, err = result.communicate() - resultdict = { - 'stdout': out.strip(), - 'stderr': err.strip(), - } - if with_exit_code: - resultdict.update({'exit_code': result.returncode}) - return resultdict - - -class RemoteShell(ShellMixin): - - """Execute shell commands on a remote machine over ssh.""" - - def __init__(self, address, password=None, username=None, - private_key=None, key_filename=None, port=None, - timeout=None, gateway=None, options=None, interactive=False, - protocol='ssh', root_password=None, **kwargs): - """An interface for executing shell commands on remote machines. - - :param str host: The ip address or host name of the server - to connect to - :param str password: A password to use for authentication - or for unlocking a private key - :param username: The username to authenticate as - :param private_key: Private SSH Key string to use - (instead of using a filename) - :param root_password: root user password to be used if username is - not root. This will use username and password - to login and then 'su' to root using - root_password - :param key_filename: a private key filename (path) - :param port: tcp/ip port to use (defaults to 22) - :param float timeout: an optional timeout (in seconds) for the - TCP connection - :param socket gateway: an existing SSH instance to use - for proxying - :param dict options: A dictionary used to set ssh options - (when proxying). - e.g. for `ssh -o StrictHostKeyChecking=no`, - you would provide - (.., options={'StrictHostKeyChecking': 'no'}) - Conversion of booleans is also supported, - (.., options={'StrictHostKeyChecking': False}) - is equivalent. - :keyword interactive: If true, prompt for password if missing. - """ - if kwargs: - LOG.warning("Satori RemoteClient received unrecognized " - "keyword arguments: %s", kwargs.keys()) - - if protocol == 'smb': - self._client = smb.connect(address, password=password, - username=username, - port=port, timeout=timeout, - gateway=gateway) - else: - self._client = ssh.connect(address, password=password, - username=username, - private_key=private_key, - key_filename=key_filename, - port=port, timeout=timeout, - gateway=gateway, - options=options, - interactive=interactive, - root_password=root_password) - self.host = self._client.host - self.port = self._client.port - - @property - def platform_info(self): - """Return distro, version, architecture.""" - return self._client.platform_info - - def __del__(self): - """Destructor which should close the connection.""" - self.close() - - def __enter__(self): - """Context manager establish connection.""" - self.connect() - return self - - def __exit__(self, *exc_info): - """Context manager close connection.""" - self.close() - - def connect(self): - """Connect to the remote host.""" - return self._client.connect() - - def test_connection(self): - """Test the connection to the remote host.""" - return self._client.test_connection() - - def execute(self, command, **kwargs): - """Execute given command over ssh.""" - return self._client.remote_execute(command, **kwargs) - - def close(self): - """Close the connection to the remote host.""" - return self._client.close() diff --git a/satori/common/__init__.py b/satori/common/__init__.py deleted file mode 100644 index b5f6188..0000000 --- a/satori/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Module to hold all shared code used inside Satori.""" diff --git a/satori/common/logging.py b/satori/common/logging.py deleted file mode 100644 index 7600290..0000000 --- a/satori/common/logging.py +++ /dev/null @@ -1,140 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Logging Module. - -This module handles logging to the standard python logging subsystem and to the -console. -""" - -from __future__ import absolute_import - -import logging -import os -import sys - -LOG = logging.getLogger(__name__) - - -class DebugFormatter(logging.Formatter): - - """Log formatter. - - Outputs any 'data' values passed in the 'extra' parameter if provided. - - **Example**: - - .. code-block:: python - - LOG.debug("My message", extra={'data': locals()}) - """ - - def format(self, record): - """Print out any 'extra' data provided in logs.""" - if hasattr(record, 'data'): - return "%s. DEBUG DATA=%s" % (logging.Formatter.format(self, - record), record.__dict__['data']) - return logging.Formatter.format(self, record) - - -def init_logging(config, default_config=None): - """Configure logging based on log config file. - - Turn on console logging if no logging files found - - :param config: object with configuration namespace (ex. argparse parser) - :keyword default_config: path to a python logging configuration file - """ - if config.get('logconfig') and os.path.isfile(config.get('logconfig')): - logging.config.fileConfig(config['logconfig'], - disable_existing_loggers=False) - elif default_config and os.path.isfile(default_config): - logging.config.fileConfig(default_config, - disable_existing_loggers=False) - else: - init_console_logging(config) - - -def find_console_handler(logger): - """Return a stream handler, if it exists.""" - for handler in logger.handlers: - if (isinstance(handler, logging.StreamHandler) and - handler.stream == sys.stderr): - return handler - - -def log_level(config): - """Get debug settings from configuration. - - --debug: turn on additional debug code/inspection (implies - logging.DEBUG) - --verbose: turn up logging output (logging.DEBUG) - --quiet: turn down logging output (logging.WARNING) - default is logging.INFO - - :param config: object with configuration namespace (ex. argparse parser) - """ - if config.get('debug') is True: - return logging.DEBUG - elif config.get('verbose') is True: - return logging.DEBUG - elif config.get('quiet') is True: - return logging.WARNING - else: - return logging.INFO - - -def get_debug_formatter(config): - """Get debug formatter based on configuration. - - :param config: configuration namespace (ex. argparser) - - --debug: log line numbers and file data also - --verbose: standard debug - --quiet: turn down logging output (logging.WARNING) - default is logging.INFO - - :param config: object with configuration namespace (ex. argparse parser) - """ - if config.get('debug') is True: - return DebugFormatter('%(pathname)s:%(lineno)d: %(levelname)-8s ' - '%(message)s') - elif config.get('verbose') is True: - return logging.Formatter( - '%(name)-30s: %(levelname)-8s %(message)s') - elif config.get('quiet') is True: - return logging.Formatter('%(message)s') - else: - return logging.Formatter('%(message)s') - - -def init_console_logging(config): - """Enable logging to the console. - - :param config: object with configuration namespace (ex. argparse parser) - """ - # define a Handler which writes messages to the sys.stderr - console = find_console_handler(logging.getLogger()) - if not console: - console = logging.StreamHandler() - logging_level = log_level(config) - console.setLevel(logging_level) - - # set a format which is simpler for console use - formatter = get_debug_formatter(config) - # tell the handler to use this format - console.setFormatter(formatter) - # add the handler to the root logger - logging.getLogger().addHandler(console) - logging.getLogger().setLevel(logging_level) - global LOG # pylint: disable=W0603 - LOG = logging.getLogger(__name__) # reset diff --git a/satori/common/popen.py b/satori/common/popen.py deleted file mode 100644 index 8104fe8..0000000 --- a/satori/common/popen.py +++ /dev/null @@ -1,23 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Popen wrapper to allow custom patching of subprocess.Popen.""" - -import subprocess - -PIPE = subprocess.PIPE -STDOUT = subprocess.STDOUT - - -def popen(*args, **kwargs): - """Wrap Popen to allow for higher level patching if necessary.""" - return subprocess.Popen(*args, **kwargs) diff --git a/satori/common/templating.py b/satori/common/templating.py deleted file mode 100644 index ce7e4dd..0000000 --- a/satori/common/templating.py +++ /dev/null @@ -1,128 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Templating module.""" - -from __future__ import absolute_import - -import json -import logging - -import jinja2 -from jinja2 import sandbox -import six - -CODE_CACHE = {} -LOG = logging.getLogger(__name__) - -if six.PY3: - StandardError = Exception - - -class TemplateException(Exception): - - """Error applying template.""" - - -class CompilerCache(jinja2.BytecodeCache): - - """Cache for compiled template code. - - This is leveraged when satori is used from within a long-running process, - like a server. It does not speed things up for command-line use. - """ - - def load_bytecode(self, bucket): - """Load compiled code from cache.""" - if bucket.key in CODE_CACHE: - bucket.bytecode_from_string(CODE_CACHE[bucket.key]) - - def dump_bytecode(self, bucket): - """Write compiled code into cache.""" - CODE_CACHE[bucket.key] = bucket.bytecode_to_string() - - -def do_prepend(value, param='/'): - """Prepend a string if the passed in string exists. - - Example: - The template '{{ root|prepend('/')}}/path'; - Called with root undefined renders: - /path - Called with root defined as 'root' renders: - /root/path - """ - if value: - return '%s%s' % (param, value) - else: - return '' - - -def preserve_linefeeds(value): - """Escape linefeeds. - - To make templates work with both YAML and JSON, escape linefeeds instead of - allowing Jinja to render them. - """ - return value.replace("\n", "\\n").replace("\r", "") - - -def get_jinja_environment(template, extra_globals=None, **env_vars): - """Return a sandboxed jinja environment.""" - template_map = {'template': template} - env = sandbox.ImmutableSandboxedEnvironment( - loader=jinja2.DictLoader(template_map), - bytecode_cache=CompilerCache(), **env_vars) - env.filters['prepend'] = do_prepend - env.filters['preserve'] = preserve_linefeeds - env.globals['json'] = json - if extra_globals: - env.globals.update(extra_globals) - return env - - -def parse(template, extra_globals=None, env_vars=None, **kwargs): - """Parse template. - - :param template: the template contents as a string - :param extra_globals: additional globals to include - :param kwargs: extra arguments are passed to the renderer - """ - if env_vars is None: - env_vars = {} - env = get_jinja_environment(template, extra_globals=extra_globals, - **env_vars) - - minimum_kwargs = { - 'data': {}, - } - minimum_kwargs.update(kwargs) - - try: - template = env.get_template('template') - except jinja2.TemplateSyntaxError as exc: - LOG.error(exc, exc_info=True) - error_message = "Template had a syntax error: %s" % exc - raise TemplateException(error_message) - - try: - result = template.render(**minimum_kwargs) - # TODO(zns): exceptions in Jinja template sometimes missing traceback - except jinja2.TemplateError as exc: - LOG.error(exc, exc_info=True) - error_message = "Template had an error: %s" % exc - raise TemplateException(error_message) - except StandardError as exc: - LOG.error(exc, exc_info=True) - error_message = "Template rendering failed: %s" % exc - raise TemplateException(error_message) - return result diff --git a/satori/contrib/psexec.py b/satori/contrib/psexec.py deleted file mode 100644 index 82c5447..0000000 --- a/satori/contrib/psexec.py +++ /dev/null @@ -1,557 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2003-2012 CORE Security Technologies -# -# This software is provided under under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# $Id: psexec.py 712 2012-09-06 04:26:22Z bethus@gmail.com $ -# -# PSEXEC like functionality example using -# RemComSvc (https://github.com/kavika13/RemCom) -# -# Author: -# beto (bethus@gmail.com) -# -# Reference for: -# DCE/RPC and SMB. - -""". - -OK -""" - -import cmd -import os -import re -import sys - -from impacket.dcerpc import dcerpc -from impacket.dcerpc import transport -from impacket.examples import remcomsvc -from impacket import smbconnection -from impacket import structure as im_structure -from impacket import version - -from satori import serviceinstall - -import argparse -import random -import string - -try: - import eventlet - threading = eventlet.patcher.original('threading') - time = eventlet.patcher.original('time') -except ImportError: - import threading - import time - - -class RemComMessage(im_structure.Structure): - - """.""" - - structure = ( - ('Command', '4096s=""'), - ('WorkingDir', '260s=""'), - ('Priority', ' 0: - try: - s.waitNamedPipe(tid, pipe) - pipeReady = True - except Exception: - tries -= 1 - time.sleep(2) - pass - - if tries == 0: - print('[!] Pipe not ready, aborting') - raise - - fid = s.openFile(tid, pipe, accessMask, creationOption=0x40, - fileAttributes=0x80) - - return fid - - def doStuff(self, rpctransport): - """.""" - dce = dcerpc.DCERPC_v5(rpctransport) - try: - dce.connect() - except Exception as e: - print(e) - sys.exit(1) - - global dialect - dialect = rpctransport.get_smb_connection().getDialect() - - try: - unInstalled = False - s = rpctransport.get_smb_connection() - - # We don't wanna deal with timeouts from now on. - s.setTimeout(100000) - svcName = "RackspaceSystemDiscovery" - executableName = "RackspaceSystemDiscovery.exe" - if self.__exeFile is None: - svc = remcomsvc.RemComSvc() - installService = serviceinstall.ServiceInstall(s, svc, - svcName, - executableName) - else: - try: - f = open(self.__exeFile) - except Exception as e: - print(e) - sys.exit(1) - installService = serviceinstall.ServiceInstall(s, f, - svcName, - executableName) - - installService.install() - - if self.__exeFile is not None: - f.close() - - tid = s.connectTree('IPC$') - fid_main = self.openPipe(s, tid, '\RemCom_communicaton', 0x12019f) - - packet = RemComMessage() - pid = os.getpid() - - packet['Machine'] = ''.join([random.choice(string.letters) - for i in range(4)]) - if self.__path is not None: - packet['WorkingDir'] = self.__path - packet['Command'] = self.__command - packet['ProcessID'] = pid - - s.writeNamedPipe(tid, fid_main, str(packet)) - - # Here we'll store the command we type so we don't print it back ;) - # ( I know.. globals are nasty :P ) - global LastDataSent - LastDataSent = '' - - retCode = None - # Create the pipes threads - stdin_pipe = RemoteStdInPipe(rpctransport, - '\%s%s%d' % (RemComSTDIN, - packet['Machine'], - packet['ProcessID']), - smbconnection.smb.FILE_WRITE_DATA | - smbconnection.smb.FILE_APPEND_DATA, - installService.getShare()) - stdin_pipe.start() - stdout_pipe = RemoteStdOutPipe(rpctransport, - '\%s%s%d' % (RemComSTDOUT, - packet['Machine'], - packet['ProcessID']), - smbconnection.smb.FILE_READ_DATA) - stdout_pipe.start() - stderr_pipe = RemoteStdErrPipe(rpctransport, - '\%s%s%d' % (RemComSTDERR, - packet['Machine'], - packet['ProcessID']), - smbconnection.smb.FILE_READ_DATA) - stderr_pipe.start() - - # And we stay here till the end - ans = s.readNamedPipe(tid, fid_main, 8) - - if len(ans): - retCode = RemComResponse(ans) - print("[*] Process %s finished with ErrorCode: %d, " - "ReturnCode: %d" % (self.__command, retCode['ErrorCode'], - retCode['ReturnCode'])) - installService.uninstall() - unInstalled = True - sys.exit(retCode['ReturnCode']) - - except Exception: - if unInstalled is False: - installService.uninstall() - sys.stdout.flush() - if retCode: - sys.exit(retCode['ReturnCode']) - else: - sys.exit(1) - - -class Pipes(threading.Thread): - - """.""" - - def __init__(self, transport, pipe, permissions, share=None): - """.""" - threading.Thread.__init__(self) - self.server = 0 - self.transport = transport - self.credentials = transport.get_credentials() - self.tid = 0 - self.fid = 0 - self.share = share - self.port = transport.get_dport() - self.pipe = pipe - self.permissions = permissions - self.daemon = True - - def connectPipe(self): - """.""" - try: - lock.acquire() - global dialect - - remoteHost = self.transport.get_smb_connection().getRemoteHost() - # self.server = SMBConnection('*SMBSERVER', - # self.transport.get_smb_connection().getRemoteHost(), - # sess_port = self.port, preferredDialect = SMB_DIALECT) - self.server = smbconnection.SMBConnection('*SMBSERVER', remoteHost, - sess_port=self.port, - preferredDialect=dialect) # noqa - user, passwd, domain, lm, nt = self.credentials - self.server.login(user, passwd, domain, lm, nt) - lock.release() - self.tid = self.server.connectTree('IPC$') - - self.server.waitNamedPipe(self.tid, self.pipe) - self.fid = self.server.openFile(self.tid, self.pipe, - self.permissions, - creationOption=0x40, - fileAttributes=0x80) - self.server.setTimeout(1000000) - except Exception: - message = ("[!] Something wen't wrong connecting the pipes(%s), " - "try again") - print(message % self.__class__) - - -class RemoteStdOutPipe(Pipes): - - """.""" - - def __init__(self, transport, pipe, permisssions): - """.""" - Pipes.__init__(self, transport, pipe, permisssions) - - def run(self): - """.""" - self.connectPipe() - while True: - try: - ans = self.server.readFile(self.tid, self.fid, 0, 1024) - except Exception: - pass - else: - try: - global LastDataSent - if ans != LastDataSent: # noqa - sys.stdout.write(ans) - sys.stdout.flush() - else: - # Don't echo what I sent, and clear it up - LastDataSent = '' - # Just in case this got out of sync, i'm cleaning it - # up if there are more than 10 chars, - # it will give false positives tho.. we should find a - # better way to handle this. - if LastDataSent > 10: - LastDataSent = '' - except Exception: - pass - - -class RemoteStdErrPipe(Pipes): - - """.""" - - def __init__(self, transport, pipe, permisssions): - """.""" - Pipes.__init__(self, transport, pipe, permisssions) - - def run(self): - """.""" - self.connectPipe() - while True: - try: - ans = self.server.readFile(self.tid, self.fid, 0, 1024) - except Exception: - pass - else: - try: - sys.stderr.write(str(ans)) - sys.stderr.flush() - except Exception: - pass - - -class RemoteShell(cmd.Cmd): - - """.""" - - def __init__(self, server, port, credentials, tid, fid, share): - """.""" - cmd.Cmd.__init__(self, False) - self.prompt = '\x08' - self.server = server - self.transferClient = None - self.tid = tid - self.fid = fid - self.credentials = credentials - self.share = share - self.port = port - self.intro = '[!] Press help for extra shell commands' - - def connect_transferClient(self): - """.""" - # self.transferClient = SMBConnection('*SMBSERVER', - # self.server.getRemoteHost(), sess_port = self.port, - # preferredDialect = SMB_DIALECT) - self.transferClient = smbconnection.SMBConnection('*SMBSERVER', - self.server.getRemoteHost(), - sess_port=self.port, - preferredDialect=dialect) # noqa - user, passwd, domain, lm, nt = self.credentials - self.transferClient.login(user, passwd, domain, lm, nt) - - def do_help(self, line): - """.""" - print(""" - lcd {path} - changes the current local directory to {path} - exit - terminates the server process (and this session) - put {src_file, dst_path} - uploads a local file to the dst_path RELATIVE to - the connected share (%s) - get {file} - downloads pathname RELATIVE to the connected - share (%s) to the current local dir - ! {cmd} - executes a local shell cmd -""" % (self.share, self.share)) - self.send_data('\r\n', False) - - def do_shell(self, s): - """.""" - os.system(s) - self.send_data('\r\n') - - def do_get(self, src_path): - """.""" - try: - if self.transferClient is None: - self.connect_transferClient() - - import ntpath - filename = ntpath.basename(src_path) - fh = open(filename, 'wb') - print("[*] Downloading %s\%s" % (self.share, src_path)) - self.transferClient.getFile(self.share, src_path, fh.write) - fh.close() - except Exception as e: - print(e) - pass - - self.send_data('\r\n') - - def do_put(self, s): - """.""" - try: - if self.transferClient is None: - self.connect_transferClient() - params = s.split(' ') - if len(params) > 1: - src_path = params[0] - dst_path = params[1] - elif len(params) == 1: - src_path = params[0] - dst_path = '/' - - src_file = os.path.basename(src_path) - fh = open(src_path, 'rb') - f = dst_path + '/' + src_file - pathname = string.replace(f, '/', '\\') - print("[*] Uploading %s to %s\\%s" % (src_file, self.share, - dst_path)) - self.transferClient.putFile(self.share, pathname, fh.read) - fh.close() - except Exception as e: - print(e) - pass - - self.send_data('\r\n') - - def do_lcd(self, s): - """.""" - if s == '': - print(os.getcwd()) - else: - os.chdir(s) - self.send_data('\r\n') - - def emptyline(self): - """.""" - self.send_data('\r\n') - return - - def do_EOF(self, line): - """.""" - self.server.logoff() - - def default(self, line): - """.""" - self.send_data(line+'\r\n') - - def send_data(self, data, hideOutput=True): - """.""" - if hideOutput is True: - global LastDataSent - LastDataSent = data - else: - LastDataSent = '' - self.server.writeFile(self.tid, self.fid, data) - - -class RemoteStdInPipe(Pipes): - - """RemoteStdInPipe class. - - Used to connect to RemComSTDIN named pipe on remote system - """ - - def __init__(self, transport, pipe, permisssions, share=None): - """Constructor.""" - Pipes.__init__(self, transport, pipe, permisssions, share) - - def run(self): - """.""" - self.connectPipe() - self.shell = RemoteShell(self.server, self.port, self.credentials, - self.tid, self.fid, self.share) - self.shell.cmdloop() - - -# Process command-line arguments. -if __name__ == '__main__': - print(version.BANNER) - - parser = argparse.ArgumentParser() - - parser.add_argument('target', action='store', - help='[domain/][username[:password]@]
') - parser.add_argument('command', action='store', - help='command to execute at the target (w/o path)') - parser.add_argument('-path', action='store', - help='path of the command to execute') - parser.add_argument( - '-file', action='store', - help="alternative RemCom binary (be sure it doesn't require CRT)") - parser.add_argument( - '-port', action='store', - help='alternative port to use, this will copy settings from 445/SMB') - parser.add_argument('protocol', choices=PSEXEC.KNOWN_PROTOCOLS.keys(), - nargs='?', default='445/SMB', - help='transport protocol (default 445/SMB)') - - group = parser.add_argument_group('authentication') - - group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", - help='NTLM hashes, format is LMHASH:NTHASH') - - if len(sys.argv) == 1: - parser.print_help() - sys.exit(1) - - options = parser.parse_args() - - domain, username, password, address = re.compile( - '(?:(?:([^/@:]*)/)?([^@:]*)(?::([^.]*))?@)?(.*)' - ).match(options.target).groups('') - - if domain is None: - domain = '' - - if options.port: - options.protocol = "%s/SMB" % options.port - - executer = PSEXEC(options.command, options.path, options.file, - options.protocol, username, password, domain, - options.hashes) - - if options.protocol not in PSEXEC.KNOWN_PROTOCOLS: - connection_string = 'ncacn_np:%s[\\pipe\\svcctl]' - PSEXEC.KNOWN_PROTOCOLS[options.protocol] = (connection_string, - options.port) - - executer.run(address) diff --git a/satori/discovery.py b/satori/discovery.py deleted file mode 100644 index 3a0adc8..0000000 --- a/satori/discovery.py +++ /dev/null @@ -1,141 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Discovery Module. - - TODO(zns): testing, refactoring, etc... just using this to demonstrate - functionality - -Example usage: - - from satori import discovery - discovery.run(address="foo.com") -""" - -from __future__ import print_function - -import sys -import traceback - -import ipaddress -import novaclient.v1_1 -from pythonwhois import shared -import six - -from satori import dns -from satori import utils - - -def run(target, config=None, interactive=False): - """Run discovery and return results.""" - if config is None: - config = {} - - found = {} - resources = {} - errors = {} - results = { - 'target': target, - 'created': utils.get_time_string(), - 'found': found, - 'resources': resources, - } - if utils.is_valid_ip_address(target): - ip_address = target - else: - hostname = dns.parse_target_hostname(target) - found['hostname'] = hostname - ip_address = six.text_type(dns.resolve_hostname(hostname)) - # TODO(sam): Use ipaddress.ip_address.is_global - # .is_private - # .is_unspecified - # .is_multicast - # To determine address "type" - if not ipaddress.ip_address(ip_address).is_loopback: - try: - domain_info = dns.domain_info(hostname) - resource_type = 'OS::DNS::Domain' - identifier = '%s:%s' % (resource_type, hostname) - resources[identifier] = { - 'type': resource_type, - 'key': identifier, - } - found['domain-key'] = identifier - resources[identifier]['data'] = domain_info - if 'registered' in domain_info: - found['registered-domain'] = domain_info['registered'] - except shared.WhoisException as exc: - results['domain'] = str(exc) - found['ip-address'] = ip_address - - host, host_errors = discover_host(ip_address, config, - interactive=interactive) - if host_errors: - errors.update(host_errors) - key = host.get('key') or ip_address - resources[key] = host - found['host-key'] = key - results['updated'] = utils.get_time_string() - return results, errors - - -def discover_host(address, config, interactive=False): - """Discover host by IP address.""" - host = {} - errors = {} - if config.get('username'): - server = find_nova_host(address, config) - if server: - host['type'] = 'OS::Nova::Instance' - data = {} - host['data'] = data - data['uri'] = [l['href'] for l in server.links - if l['rel'] == 'self'][0] - data['name'] = server.name - data['id'] = server.id - data['addresses'] = server.addresses - host['key'] = data['uri'] - - if config.get('system_info'): - module_name = config['system_info'].replace("-", "_") - if '.' not in module_name: - module_name = 'satori.sysinfo.%s' % module_name - system_info_module = utils.import_object(module_name) - try: - result = system_info_module.get_systeminfo( - address, config, interactive=interactive) - host.setdefault('data', {}) - host['data']['system_info'] = result - except Exception as exc: - exc_traceback = sys.exc_info()[2] - errors['system_info'] = { - 'type': "ERROR", - 'message': str(exc), - 'exception': exc, - 'traceback': traceback.format_tb(exc_traceback), - } - return host, errors - - -def find_nova_host(address, config): - """See if a nova instance has the supplied address.""" - nova = novaclient.v1_1.client.Client(config['username'], - config['password'], - config['tenant_id'], - config['authurl'], - region_name=config['region'], - service_type="compute") - for server in nova.servers.list(): - for network_addresses in six.itervalues(server.addresses): - for ip_address in network_addresses: - if ip_address['addr'] == address: - return server diff --git a/satori/dns.py b/satori/dns.py deleted file mode 100644 index fae6a72..0000000 --- a/satori/dns.py +++ /dev/null @@ -1,113 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Satori DNS Discovery.""" - -import datetime -import logging -import socket - -import pythonwhois -from six.moves.urllib import parse as urlparse -import tldextract - -from satori import errors -from satori import utils - - -LOG = logging.getLogger(__name__) - - -def parse_target_hostname(target): - """Get IP address or FQDN of a target which could be a URL or address.""" - if not target: - raise errors.SatoriInvalidNetloc("Target must be supplied.") - try: - parsed = urlparse.urlparse(target) - except AttributeError as err: - error = "Target `%s` is unparseable. Error: %s" % (target, err) - LOG.exception(error) - raise errors.SatoriInvalidNetloc(error) - - # Domain names and IP are in netloc when parsed with a protocol - # they will be in path if parsed without a protocol - return parsed.netloc or parsed.path - - -def resolve_hostname(hostname): - """Get IP address of hostname.""" - try: - address = socket.gethostbyname(hostname) - except socket.gaierror: - error = "`%s` is an invalid domain." % hostname - raise errors.SatoriInvalidDomain(error) - return address - - -def get_registered_domain(hostname): - """Get the root DNS domain of an FQDN.""" - return tldextract.extract(hostname).registered_domain - - -def ip_info(ip_address): - """Get as much information as possible for a given ip address.""" - if not utils.is_valid_ip_address(ip_address): - error = "`%s` is an invalid IP address." % ip_address - raise errors.SatoriInvalidIP(error) - - result = pythonwhois.get_whois(ip_address) - - return { - 'whois': result['raw'] - } - - -def domain_info(domain): - """Get as much information as possible for a given domain name.""" - registered_domain = get_registered_domain(domain) - if utils.is_valid_ip_address(domain) or registered_domain == '': - error = "`%s` is an invalid domain." % domain - raise errors.SatoriInvalidDomain(error) - - result = pythonwhois.get_whois(registered_domain) - registrar = [] - if 'registrar' in result and len(result['registrar']) > 0: - registrar = result['registrar'][0] - nameservers = result.get('nameservers', []) - days_until_expires = None - expires = None - if 'expiration_date' in result: - if (isinstance(result['expiration_date'], list) - and len(result['expiration_date']) > 0): - expires = result['expiration_date'][0] - if isinstance(expires, datetime.datetime): - days_until_expires = (expires - datetime.datetime.now()).days - expires = utils.get_time_string(time_obj=expires) - else: - days_until_expires = (utils.parse_time_string(expires) - - datetime.datetime.now()).days - return { - 'name': registered_domain, - 'whois': result['raw'], - 'registrar': registrar, - 'nameservers': nameservers, - 'days_until_expires': days_until_expires, - 'expiration_date': expires, - } - - -def netloc_info(netloc): - """Determine if netloc is an IP or domain name.""" - if utils.is_valid_ip_address(netloc): - ip_info(netloc) - else: - domain_info(netloc) diff --git a/satori/errors.py b/satori/errors.py deleted file mode 100644 index 06bc494..0000000 --- a/satori/errors.py +++ /dev/null @@ -1,116 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Satori Discovery Errors.""" - - -class SatoriException(Exception): - - """Parent class for Satori exceptions. - - Accepts a string error message that that accept a str description. - """ - - -class UndeterminedPlatform(SatoriException): - - """The target system's platform could not be determined.""" - - -class SatoriInvalidNetloc(SatoriException): - - """Netloc that cannot be parsed by `urlparse`.""" - - -class SatoriInvalidDomain(SatoriException): - - """Invalid Domain provided.""" - - -class SatoriInvalidIP(SatoriException): - - """Invalid IP provided.""" - - -class SatoriShellException(SatoriException): - - """Invalid shell parameters.""" - - -class SatoriAuthenticationException(SatoriException): - - """Invalid login credentials.""" - - -class SatoriSMBAuthenticationException(SatoriAuthenticationException): - - """Invalid login credentials for use over SMB to server.""" - - -class SatoriSMBLockoutException(SatoriSMBAuthenticationException): - - """Too many invalid logon attempts, the user has been locked out.""" - - -class SatoriSMBFileSharingException(SatoriException): - - """Incompatible shared access flags for a file on the Windows system.""" - - -class GetPTYRetryFailure(SatoriException): - - """Tried to re-run command with get_pty to no avail.""" - - -class DiscoveryException(SatoriException): - - """Discovery exception with custom message.""" - - -class SatoriDuplicateCommandException(SatoriException): - - """The command cannot be run because it was already found to be running.""" - - -class UnsupportedPlatform(DiscoveryException): - - """Unsupported operating system or distro.""" - - -class SystemInfoCommandMissing(DiscoveryException): - - """Command that provides system information is missing.""" - - -class SystemInfoCommandOld(DiscoveryException): - - """Command that provides system information is outdated.""" - - -class SystemInfoNotJson(DiscoveryException): - - """Command did not produce valid JSON.""" - - -class SystemInfoMissingJson(DiscoveryException): - - """Command did not produce stdout containing JSON.""" - - -class SystemInfoInvalid(DiscoveryException): - - """Command did not produce valid JSON or XML.""" - - -class SystemInfoCommandInstallFailed(DiscoveryException): - - """Failed to install package that provides system information.""" diff --git a/satori/formats/custom.jinja b/satori/formats/custom.jinja deleted file mode 100644 index 03e5f3b..0000000 --- a/satori/formats/custom.jinja +++ /dev/null @@ -1,23 +0,0 @@ -{# - -This is a jinja template used to customize the output of satori. You can add -as many of those as you'd like to your setup. You can reference them using the ---format (or -F) argument. satori takes the format you asked for and appends -".jinja" to it to look up the file from this dirfectory. - -You have some global variables available to you: - -- target: that's the address or URL supplied at the command line -- data: all the discovery data - - -For example, to use this template: - - $ satori openstack.org -F custo - - Hi! localhost - -Happy customizing :-) - -#} -Hi! {{ target }} diff --git a/satori/formats/text.jinja b/satori/formats/text.jinja deleted file mode 100644 index d400a1e..0000000 --- a/satori/formats/text.jinja +++ /dev/null @@ -1,54 +0,0 @@ -{% set found = data['found'] | default({}) %} -{% set resources = data['resources'] | default({'n/a': {}}) %} -{% set address = found['ip-address'] %} -{% set hostkey = found['host-key'] | default('n/a') %} -{% set domainkey = found['domain-key'] | default('n/a') %} -{% set server = resources[hostkey] | default(False) %} -{% set domain = resources[domainkey] | default(False) %} - -{% if found['ip-address'] != target %}Address: - {{ target }} resolves to IPv4 address {{ found['ip-address'] }} -{%- endif %} - -{% if domain %}Domain: {{ domain['data'].name }} - Registrar: {{ domain['data'].registrar }} -{% if domain['data'].nameservers %} - Nameservers: {% for nameserver in domain['data'].nameservers %}{{nameserver}}{% if not loop.last %}, {% endif %}{% endfor %} - -{% endif %} -{% if domain['data'].days_until_expires %} - Expires: {{ domain['data'].days_until_expires }} days -{% endif %} -{%- endif %} -{% if server and server.type == 'OS::Nova::Instance' %} -Host: - {{ found['ip-address'] }} ({{ target }}) is hosted on a Nova instance -{% if 'data' in server %} Instance Information: - URI: {{ server['data'].uri | default('n/a') }} - Name: {{ server['data'].name | default('n/a') }} - ID: {{ server['data'].id | default('n/a') }} -{% if 'addresses' in server['data'] %} ip-addresses: -{% for name, addresses in server['data'].addresses.items() %} - {{ name }}: -{% for address in addresses %} - {{ address.addr }} -{% endfor %} -{% endfor %}{% endif %}{% endif %} -{% elif found['ip-address'] %} -Host: - ip-address: {{ found['ip-address'] }} -{% else %}Host not found -{% endif %} -{% if server and 'data' in server and server['data'].system_info %} -{% if 'remote_services' in server['data'].system_info %} - Listening Services: -{% for remote in server['data'].system_info.remote_services | sort %} - {{ remote.ip }}:{{ remote.port }} {{ remote.process }} -{% endfor %}{% endif %} -{% if 'connections' in server['data'].system_info %} - Talking to: -{% for connection in server['data'].system_info.connections | dictsort %} - {{ connection[0] }}{% if connection[1] %} on {% for port in connection[1] %}{{ port }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %} - -{% endfor %}{% endif %} -{% endif %} diff --git a/satori/serviceinstall.py b/satori/serviceinstall.py deleted file mode 100644 index c82b86e..0000000 --- a/satori/serviceinstall.py +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright (c) 2003-2012 CORE Security Technologies -# -# This software is provided under under a slightly modified version -# of the Apache Software License. See the accompanying LICENSE file -# for more information. -# -# $Id: serviceinstall.py 1141 2014-02-12 16:39:51Z bethus@gmail.com $ -# -# Service Install Helper library used by psexec and smbrelayx -# You provide an already established connection and an exefile -# (or class that mimics a file class) and this will install and -# execute the service, and then uninstall (install(), uninstall(). -# It tries to take care as much as possible to leave everything clean. -# -# Author: -# Alberto Solino (bethus@gmail.com) -# -"""This module has been copied from impacket.examples.serviceinstall. - -It exposes a class that can be used to install services on Windows devices -""" - -import random -import string - -from impacket.dcerpc import dcerpc -from impacket.dcerpc import srvsvc -from impacket.dcerpc import svcctl -from impacket.dcerpc import transport -from impacket import smb -from impacket import smb3 -from impacket import smbconnection - - -class ServiceInstall(): - - """Class to manage Services on a remote windows server. - - This class is slightly improved from the example in the impacket package - in a way that it allows to specify a service and executable name during - instantiation rather than using a random name by default - """ - - def __init__(self, SMBObject, exeFile, serviceName=None, - binaryServiceName=None): - """Contructor of the class. - - :param SMBObject: existing SMBObject - :param exeFile: file handle or class that mimics a file - class, this will be used to create the - service - :param serviceName: name of the service to be created, will be - random if not set - :param binaryServiceName name of the uploaded file, wil be random if - not set - """ - print("In constructor now!!!") - self._rpctransport = 0 - if not serviceName: - self.__service_name = ''.join( - [random.choice(string.letters) for i in range(4)]) - else: - self.__service_name = serviceName - if not binaryServiceName: - self.__binary_service_name = ''.join( - [random.choice(string.letters) for i in range(8)]) + '.exe' - else: - self.__binary_service_name = binaryServiceName - self.__exeFile = exeFile - - # We might receive two different types of objects, always end up - # with a SMBConnection one - if isinstance(SMBObject, smb.SMB) or isinstance(SMBObject, smb3.SMB3): - self.connection = smbconnection.SMBConnection( - existingConnection=SMBObject) - else: - self.connection = SMBObject - - self.share = '' - - def getShare(self): - """Return the writable share that has been used to upload the file.""" - return self.share - - def getShares(self): - """Return a list of shares on the remote windows server.""" - # Setup up a DCE SMBTransport with the connection already in place - print("[*] Requesting shares on %s....." % ( - self.connection.getRemoteHost())) - try: - self._rpctransport = transport.SMBTransport( - '', '', filename=r'\srvsvc', smb_connection=self.connection) - self._dce = dcerpc.DCERPC_v5(self._rpctransport) - self._dce.connect() - - self._dce.bind(srvsvc.MSRPC_UUID_SRVSVC) - srv_svc = srvsvc.DCERPCSrvSvc(self._dce) - resp = srv_svc.get_share_enum_1(self._rpctransport.get_dip()) - return resp - except Exception: - print("[!] Error requesting shares on %s, aborting....." % ( - self.connection.getRemoteHost())) - raise - - def createService(self, handle, share, path): - """Install Service on the remote server. - - This method will connect to the SVCManager on the remote server and - install the service as specified in the constructor. - """ - print("[*] Creating service %s on %s....." % ( - self.__service_name, self.connection.getRemoteHost())) - - # First we try to open the service in case it exists. - # If it does, we remove it. - try: - resp = self.rpcsvc.OpenServiceW( - handle, self.__service_name.encode('utf-16le')) - except Exception as e: - if e.get_error_code() == svcctl.ERROR_SERVICE_DOES_NOT_EXISTS: - # We're good, pass the exception - pass - else: - raise - else: - # It exists, remove it - self.rpcsvc.DeleteService(resp['ContextHandle']) - self.rpcsvc.CloseServiceHandle(resp['ContextHandle']) - - # Create the service - command = '%s\\%s' % (path, self.__binary_service_name) - try: - resp = self.rpcsvc.CreateServiceW( - handle, self.__service_name.encode('utf-16le'), - self.__service_name.encode('utf-16le'), - command.encode('utf-16le')) - except Exception: - print("[!] Error creating service %s on %s" % ( - self.__service_name, self.connection.getRemoteHost())) - raise - else: - return resp['ContextHandle'] - - def openSvcManager(self): - """Connect to the SVCManager on the remote host.""" - print("[*] Opening SVCManager on %s...." - "." % self.connection.getRemoteHost()) - # Setup up a DCE SMBTransport with the connection already in place - self._rpctransport = transport.SMBTransport( - '', '', filename=r'\svcctl', smb_connection=self.connection) - self._dce = dcerpc.DCERPC_v5(self._rpctransport) - self._dce.connect() - self._dce.bind(svcctl.MSRPC_UUID_SVCCTL) - self.rpcsvc = svcctl.DCERPCSvcCtl(self._dce) - try: - resp = self.rpcsvc.OpenSCManagerW() - except Exception: - print("[!] Error opening SVCManager on %s...." - "." % self.connection.getRemoteHost()) - raise Exception('Unable to open SVCManager') - else: - return resp['ContextHandle'] - - def copy_file(self, src, tree, dst): - """Copy file to remote SMB share.""" - print("[*] Uploading file %s" % dst) - if isinstance(src, str): - # We have a filename - fh = open(src, 'rb') - else: - # We have a class instance, it must have a read method - fh = src - f = dst - pathname = string.replace(f, '/', '\\') - try: - self.connection.putFile(tree, pathname, fh.read) - except Exception: - print("[!] Error uploading file %s, aborting....." % dst) - raise - fh.close() - - def findWritableShare(self, shares): - """Retrieve a list of writable shares on the remote host.""" - # Check we can write a file on the shares, stop in the first one - for i in shares: - if (i['Type'] == smb.SHARED_DISK or - i['Type'] == smb.SHARED_DISK_HIDDEN): - share = i['NetName'].decode('utf-16le')[:-1] - try: - self.connection.createDirectory(share, 'BETO') - except Exception: - # Can't create, pass - print("[!] share '%s' is not writable." % share) - pass - else: - print('[*] Found writable share %s' % share) - self.connection.deleteDirectory(share, 'BETO') - return str(share) - return None - - def install(self): # noqa - """Install the service on the remote host.""" - if self.connection.isGuestSession(): - print("[!] Authenticated as Guest. Aborting") - self.connection.logoff() - del(self.connection) - else: - fileCopied = False - serviceCreated = False - # Do the stuff here - try: - # Let's get the shares - shares = self.getShares() - self.share = self.findWritableShare(shares) - self.copy_file(self.__exeFile, - self.share, - self.__binary_service_name) - fileCopied = True - svcManager = self.openSvcManager() - if svcManager != 0: - serverName = self.connection.getServerName() - if serverName != '': - path = '\\\\%s\\%s' % (serverName, self.share) - else: - path = '\\\\127.0.0.1\\' + self.share - service = self.createService(svcManager, self.share, path) - serviceCreated = True - if service != 0: - # Start service - print('[*] Starting service %s....' - '.' % self.__service_name) - try: - self.rpcsvc.StartServiceW(service) - except Exception: - pass - self.rpcsvc.CloseServiceHandle(service) - self.rpcsvc.CloseServiceHandle(svcManager) - return True - except Exception as e: - print("[!] Error performing the installation, cleaning up: " - "%s" % e) - try: - self.rpcsvc.StopService(service) - except Exception: - pass - if fileCopied is True: - try: - self.connection.deleteFile(self.share, - self.__binary_service_name) - except Exception: - pass - if serviceCreated is True: - try: - self.rpcsvc.DeleteService(service) - except Exception: - pass - return False - - def uninstall(self): - """Uninstall service from remote host and delete file from share.""" - fileCopied = True - serviceCreated = True - # Do the stuff here - try: - # Let's get the shares - svcManager = self.openSvcManager() - if svcManager != 0: - resp = self.rpcsvc.OpenServiceA(svcManager, - self.__service_name) - service = resp['ContextHandle'] - print('[*] Stoping service %s.....' % self.__service_name) - try: - self.rpcsvc.StopService(service) - except Exception: - pass - print('[*] Removing service %s.....' % self.__service_name) - self.rpcsvc.DeleteService(service) - self.rpcsvc.CloseServiceHandle(service) - self.rpcsvc.CloseServiceHandle(svcManager) - print('[*] Removing file %s.....' % self.__binary_service_name) - self.connection.deleteFile(self.share, self.__binary_service_name) - except Exception: - print("[!] Error performing the uninstallation, cleaning up") - try: - self.rpcsvc.StopService(service) - except Exception: - pass - if fileCopied is True: - try: - self.connection.deleteFile(self.share, - self.__binary_service_name) - except Exception: - try: - self.connection.deleteFile(self.share, - self.__binary_service_name) - except Exception: - pass - pass - if serviceCreated is True: - try: - self.rpcsvc.DeleteService(service) - except Exception: - pass diff --git a/satori/shell.py b/satori/shell.py deleted file mode 100644 index a9b2d32..0000000 --- a/satori/shell.py +++ /dev/null @@ -1,285 +0,0 @@ -# Copyright 2012-2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Command-line interface to Configuration Discovery. - -Accept a network location, run through the discovery process and report the -findings back to the user. -""" - -from __future__ import print_function - -import argparse -import json -import logging -import os -import sys - -from satori.common import logging as common_logging -from satori.common import templating -from satori import discovery -from satori import errors - -LOG = logging.getLogger(__name__) - - -def netloc_parser(data): - """Parse the netloc parameter. - - :returns: username, url. - """ - if data and '@' in data: - first_at = data.index('@') - return (data[0:first_at] or None), data[first_at + 1:] or None - else: - return None, data or None - - -def parse_args(argv): - """Parse the command line arguments.""" - parser = argparse.ArgumentParser(description='Configuration discovery.') - parser.add_argument( - 'netloc', - help="Network location as a URL, address, or ssh-style user@address. " - "E.g. https://domain.com, sub.domain.com, 4.3.2.1, or root@web01. " - "Supplying a username before an @ without the `--system-info` " - " argument will default `--system-info` to 'ohai-solo'." - ) - - # - # Openstack Client Settings - # - openstack_group = parser.add_argument_group( - 'OpenStack Settings', - "Cloud credentials, settings and endpoints. If a network location is " - "found to be hosted on the tenant additional information is provided." - ) - openstack_group.add_argument( - '--os-username', - dest='username', - default=os.environ.get('OS_USERNAME'), - help="OpenStack Auth username. Defaults to env[OS_USERNAME]." - ) - openstack_group.add_argument( - '--os-password', - dest='password', - default=os.environ.get('OS_PASSWORD'), - help="OpenStack Auth password. Defaults to env[OS_PASSWORD]." - ) - openstack_group.add_argument( - '--os-region-name', - dest='region', - default=os.environ.get('OS_REGION_NAME'), - help="OpenStack region. Defaults to env[OS_REGION_NAME]." - ) - openstack_group.add_argument( - '--os-auth-url', - dest='authurl', - default=os.environ.get('OS_AUTH_URL'), - help="OpenStack Auth endpoint. Defaults to env[OS_AUTH_URL]." - ) - openstack_group.add_argument( - '--os-compute-api-version', - dest='compute_api_version', - default=os.environ.get('OS_COMPUTE_API_VERSION', '1.1'), - help="OpenStack Compute API version. Defaults to " - "env[OS_COMPUTE_API_VERSION] or 1.1." - ) - # Tenant name or ID can be supplied - tenant_group = openstack_group.add_mutually_exclusive_group() - tenant_group.add_argument( - '--os-tenant-name', - dest='tenant_name', - default=os.environ.get('OS_TENANT_NAME'), - help="OpenStack Auth tenant name. Defaults to env[OS_TENANT_NAME]." - ) - tenant_group.add_argument( - '--os-tenant-id', - dest='tenant_id', - default=os.environ.get('OS_TENANT_ID'), - help="OpenStack Auth tenant ID. Defaults to env[OS_TENANT_ID]." - ) - - # - # Plugins - # - parser.add_argument( - '--system-info', - help="Mechanism to use on a Nova resource to obtain system " - "information. E.g. ohai, facts, factor." - ) - - # - # Output formatting and logging - # - parser.add_argument( - '--format', '-F', - dest='format', - default='text', - help="Format for output (json or text)." - ) - parser.add_argument( - "--logconfig", - help="Optional logging configuration file." - ) - parser.add_argument( - "-d", "--debug", - action="store_true", - help="turn on additional debugging inspection and " - "output including full HTTP requests and responses. " - "Log output includes source file path and line " - "numbers." - ) - parser.add_argument( - "-v", "--verbose", - action="store_true", - help="turn up logging to DEBUG (default is INFO)." - ) - parser.add_argument( - "-q", "--quiet", - action="store_true", - help="turn down logging to WARN (default is INFO)." - ) - - # - # SSH options - # - ssh_group = parser.add_argument_group( - 'ssh-like Settings', - 'To be used to access hosts.' - ) - # ssh.py actualy handles the defaults. We're documenting it here so that - # the command-line help string is informative, but the default is set in - # ssh.py (by calling paramiko's load_system_host_keys). - ssh_group.add_argument( - "-i", "--host-key-path", - type=argparse.FileType('r'), - help="Selects a file from which the identity (private key) for public " - "key authentication is read. The default ~/.ssh/id_dsa, " - "~/.ssh/id_ecdsa and ~/.ssh/id_rsa. Supplying this without the " - "`--system-info` argument will default `--system-info` to 'ohai-solo'." - ) - ssh_group.add_argument( - "-o", - metavar="ssh_options", - help="Mirrors the ssh -o option. See ssh_config(5)." - ) - - config = parser.parse_args(argv) - if config.host_key_path: - config.host_key = config.host_key_path.read() - else: - config.host_key = None - - # argparse lacks a method to say "if this option is set, require these too" - required_to_access_cloud = [ - config.username, - config.password, - config.authurl, - config.region, - config.tenant_name or config.tenant_id, - ] - if any(required_to_access_cloud) and not all(required_to_access_cloud): - raise errors.SatoriShellException( - "To connect to an OpenStack cloud you must supply a username, " - "password, authentication endpoint, region and tenant. Either " - "provide all of these settings or none of them." - ) - - username, url = netloc_parser(config.netloc) - config.netloc = url - - if (config.host_key or config.username) and not config.system_info: - config.system_info = 'ohai-solo' - - if username: - config.host_username = username - else: - config.host_username = 'root' - - return vars(config) - - -def main(argv=None): - """Discover an existing configuration for a network location.""" - config = parse_args(argv) - common_logging.init_logging(config) - - if not (config['format'] == 'json' or - check_format(config['format'] or "text")): - sys.exit("Output format file (%s) not found or accessible. Try " - "specifying raw JSON format using `--format json`" % - get_template_path(config['format'])) - - try: - results, errors = discovery.run(config['netloc'], config, - interactive=True) - print(format_output(config['netloc'], results, - template_name=config['format'])) - if errors: - sys.stderr.write(format_errors(errors, config)) - except Exception as exc: # pylint: disable=W0703 - if config['debug']: - LOG.exception(exc) - return str(exc) - - sys.exit(0) - - -def get_template_path(name): - """Get template path from name.""" - root_dir = os.path.dirname(__file__) - return os.path.join(root_dir, "formats", "%s.jinja" % name) - - -def check_format(name): - """Verify that we have the requested format template.""" - template_path = get_template_path(name) - return os.path.exists(template_path) - - -def get_template(name): - """Get template text from templates directory by name.""" - root_dir = os.path.dirname(__file__) - template_path = os.path.join(root_dir, "formats", "%s.jinja" % name) - with open(template_path, 'r') as handle: - template = handle.read() - return template - - -def format_output(discovered_target, results, template_name="text"): - """Format results in CLI format.""" - if template_name == 'json': - return(json.dumps(results, indent=2)) - else: - template = get_template(template_name) - env_vars = dict(lstrip_blocks=True, trim_blocks=True) - return templating.parse(template, target=discovered_target, - data=results, env_vars=env_vars).strip('\n') - - -def format_errors(errors, config): - """Format errors for output to console.""" - if config['debug']: - return str(errors) - else: - formatted = {} - for key, error in errors.items(): - formatted[key] = error['message'] - return str(formatted) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/satori/smb.py b/satori/smb.py deleted file mode 100644 index bab5b31..0000000 --- a/satori/smb.py +++ /dev/null @@ -1,347 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# pylint: disable=W0703 - -"""Windows remote client module implemented using psexec.py.""" - -try: - import eventlet - eventlet.monkey_patch() - from eventlet.green import time -except ImportError: - import time - -import ast -import base64 -import logging -import os -import re -import shlex -import tempfile - -from satori.common import popen -from satori import errors -from satori import ssh -from satori import tunnel - -LOG = logging.getLogger(__name__) - - -def connect(*args, **kwargs): - """Connect to a remote device using psexec.py.""" - try: - return SMBClient.get_client(*args, **kwargs) - except Exception as exc: - LOG.error("ERROR: pse.py failed to connect: %s", str(exc)) - - -def _posh_encode(command): - """Encode a powershell command to base64. - - This is using utf-16 encoding and disregarding the first two bytes - :param command: command to encode - """ - return base64.b64encode(command.encode('utf-16')[2:]) - - -class SubprocessError(Exception): - - """Custom Exception. - - This will be raised when the subprocess running psexec.py has exited. - """ - - pass - - -class SMBClient(object): # pylint: disable=R0902 - - """Connects to devices over SMB/psexec to execute commands.""" - - _prompt_pattern = re.compile(r'^[a-zA-Z]:\\.*>$', re.MULTILINE) - - # pylint: disable=R0913 - def __init__(self, host, password=None, username="Administrator", - port=445, timeout=10, gateway=None, **kwargs): - """Create an instance of the PSE class. - - :param str host: The ip address or host name of the server - to connect to - :param str password: A password to use for authentication - :param str username: The username to authenticate as (defaults to - Administrator) - :param int port: tcp/ip port to use (defaults to 445) - :param float timeout: an optional timeout (in seconds) for the - TCP connection - :param gateway: instance of satori.ssh.SSH to be used to set up - an SSH tunnel (equivalent to ssh -L) - """ - self.password = password - self.host = host - self.port = port or 445 - self.username = username or 'Administrator' - self.timeout = timeout - self._connected = False - self._platform_info = None - self._process = None - self._orig_host = None - self._orig_port = None - self.ssh_tunnel = None - self._substituted_command = None - - # creating temp file to talk to _process with - self._file_write = tempfile.NamedTemporaryFile() - self._file_read = open(self._file_write.name, 'r') - - self._command = ("nice python %s/contrib/psexec.py -port %s %s:%s@%s " - "'c:\\Windows\\sysnative\\cmd'") - self._output = '' - self.gateway = gateway - - if gateway: - if not isinstance(self.gateway, ssh.SSH): - raise TypeError("'gateway' must be a satori.ssh.SSH instance. " - "( instances of this type are returned by" - "satori.ssh.connect() )") - - if kwargs: - LOG.debug("DEBUG: Following arguments passed into PSE constructor " - "not used: %s", kwargs.keys()) - - def __del__(self): - """Destructor of the PSE class.""" - try: - self.close() - except ValueError: - pass - - @classmethod - def get_client(cls, *args, **kwargs): - """Return a pse client object from this module.""" - return cls(*args, **kwargs) - - @property - def platform_info(self): - """Return Windows edition, version and architecture. - - requires Powershell version 3 - """ - if not self._platform_info: - command = ('Get-WmiObject Win32_OperatingSystem |' - ' select @{n="dist";e={$_.Caption.Trim()}},' - '@{n="version";e={$_.Version}},@{n="arch";' - 'e={$_.OSArchitecture}} | ' - ' ConvertTo-Json -Compress') - stdout = self.remote_execute(command, retry=3) - self._platform_info = ast.literal_eval(stdout) - - return self._platform_info - - def create_tunnel(self): - """Create an ssh tunnel via gateway. - - This will tunnel a local ephemeral port to the host's port. - This will preserve the original host and port - """ - self.ssh_tunnel = tunnel.Tunnel(self.host, self.port, self.gateway) - self._orig_host = self.host - self._orig_port = self.port - self.host, self.port = self.ssh_tunnel.address - self.ssh_tunnel.serve_forever(async=True) - - def shutdown_tunnel(self): - """Terminate the ssh tunnel. Restores original host and port.""" - self.ssh_tunnel.shutdown() - self.host = self._orig_host - self.port = self._orig_port - - def test_connection(self): - """Connect to a Windows server and disconnect again. - - Make sure the returncode is 0, otherwise return False - """ - self.connect() - self.close() - self._get_output() - if self._output.find('ErrorCode: 0, ReturnCode: 0') > -1: - return True - else: - return False - - def connect(self): - """Attempt a connection using psexec.py. - - This will create a subprocess.Popen() instance and communicate with it - via _file_read/_file_write and _process.stdin - """ - try: - if self._connected and self._process: - if self._process.poll() is None: - return - else: - self._process.wait() - if self.gateway: - self.shutdown_tunnel() - if self.gateway: - self.create_tunnel() - self._substituted_command = self._command % ( - os.path.dirname(__file__), - self.port, - self.username, - self.password, - self.host) - self._process = popen.popen( - shlex.split(self._substituted_command), - stdout=self._file_write, - stderr=popen.STDOUT, - stdin=popen.PIPE, - close_fds=True, - universal_newlines=True, - bufsize=-1) - output = '' - while not self._prompt_pattern.findall(output): - output += self._get_output() - self._connected = True - except Exception: - LOG.error("Failed to connect to host %s over smb", - self.host, exc_info=True) - self.close() - raise - - def close(self): - """Close the psexec connection by sending 'exit' to the subprocess. - - This will cleanly exit psexec (i.e. stop and uninstall the service and - delete the files) - - This method will be called when an instance of this class is about to - being destroyed. It will try to close the connection (which will clean - up on the remote server) and catch the exception that is raised when - the connection has already been closed. - """ - try: - self._process.communicate('exit') - except Exception as exc: - LOG.warning("ERROR: Failed to close %s: %s", self, str(exc)) - del exc - try: - if self.gateway: - self.shutdown_tunnel() - self.gateway.close() - except Exception as exc: - LOG.warning("ERROR: Failed to close gateway %s: %s", self.gateway, - str(exc)) - del exc - finally: - try: - self._process.kill() - except OSError: - LOG.exception("Tried killing psexec subprocess.") - - def remote_execute(self, command, powershell=True, retry=0, **kwargs): - """Execute a command on a remote host. - - :param command: Command to be executed - :param powershell: If True, command will be interpreted as Powershell - command and therefore converted to base64 and - prepended with 'powershell -EncodedCommand - :param int retry: Number of retries when SubprocessError is thrown - by _get_output before giving up - """ - self.connect() - if powershell: - command = ('powershell -EncodedCommand %s' % - _posh_encode(command)) - LOG.info("Executing command: %s", command) - self._process.stdin.write('%s\n' % command) - self._process.stdin.flush() - try: - output = self._get_output() - LOG.debug("Stdout produced: %s", output) - output = "\n".join(output.splitlines()[:-1]).strip() - return output - except Exception: - LOG.error("Error while reading output from command %s on %s", - command, self.host, exc_info=True) - if not retry: - raise - else: - return self.remote_execute(command, powershell=powershell, - retry=retry - 1) - - def _handle_output(self, output): - """Check for process termination, exit code, or error messages. - - If the exit code is available and is zero, return True. This rountine - will raise an exception in the case of a non-zero exit code. - """ - if self._process.poll() is not None: - if self._process.returncode == 0: - return True - if "The attempted logon is invalid" in output: - msg = [k for k in output.splitlines() if k][-1].strip() - raise errors.SatoriSMBAuthenticationException(msg) - elif "The user account has been automatically locked" in output: - msg = [k for k in output.splitlines() if k][-1].strip() - raise errors.SatoriSMBLockoutException(msg) - elif "cannot be opened because the share access flags" in output: - # A file cannot be opened because the share - # access flags are incompatible - msg = [k for k in output.splitlines() if k][-1].strip() - raise errors.SatoriSMBFileSharingException(msg) - else: - raise SubprocessError("subprocess with pid: %s has " - "terminated unexpectedly with " - "return code: %s | %s" - % (self._process.pid, - self._process.poll(), output)) - - def _get_output(self, prompt_expected=True, wait=500): - """Retrieve output from _process. - - This method will wait until output is started to be received and then - wait until no further output is received within a defined period - :param prompt_expected: only return when regular expression defined - in _prompt_pattern is matched - :param wait: Time in milliseconds to wait in each of the - two loops that wait for (more) output. - """ - tmp_out = '' - while tmp_out == '': - self._file_read.seek(0, 1) - tmp_out += self._file_read.read() - # leave loop if underlying process has a return code - # obviously meaning that it has terminated - if self._handle_output(tmp_out): - break - time.sleep(float(wait) / 1000) - else: - LOG.debug("Loop 1 - stdout read: %s", tmp_out) - - stdout = tmp_out - while (tmp_out != '' or - (not self._prompt_pattern.findall(stdout) and - prompt_expected)): - self._file_read.seek(0, 1) - tmp_out = self._file_read.read() - stdout += tmp_out - # leave loop if underlying process has a return code - # obviously meaning that it has terminated - if self._handle_output(tmp_out): - break - time.sleep(float(wait) / 1000) - else: - LOG.debug("Loop 2 - stdout read: %s", tmp_out) - - self._output += stdout - stdout = stdout.replace('\r', '').replace('\x08', '') - return stdout diff --git a/satori/ssh.py b/satori/ssh.py deleted file mode 100644 index ab76a6b..0000000 --- a/satori/ssh.py +++ /dev/null @@ -1,512 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""SSH Module for connecting to and automating remote commands. - -Supports proxying through an ssh tunnel ('gateway' keyword argument.) - -To control the behavior of the SSH client, use the specific connect_with_* -calls. The .connect() call behaves like the ssh command and attempts a number -of connection methods, including using the curent user's ssh keys. - -If interactive is set to true, the module will also prompt for a password if no -other connection methods succeeded. - -Note that test_connection() calls connect(). To test a connection and control -the authentication methods used, just call connect_with_* and catch any -exceptions instead of using test_connect(). -""" - -import ast -import getpass -import logging -import os -import re -import time - -import paramiko -import six - -from satori import errors -from satori import utils - -LOG = logging.getLogger(__name__) -MIN_PASSWORD_PROMPT_LEN = 8 -MAX_PASSWORD_PROMPT_LEN = 64 -TEMPFILE_PREFIX = ".satori.tmp.key." -TTY_REQUIRED = [ - "you must have a tty to run sudo", - "is not a tty", - "no tty present", - "must be run from a terminal", -] - - -def shellquote(s): - r"""Quote a string for use on a command line. - - This wraps the string in single-quotes and converts any existing - single-quotes to r"'\''". Here the first single-quote ends the - previous quoting, the escaped single-quote becomes a literal - single-quote, and the last single-quote quotes the next part of - the string. - """ - return "'%s'" % s.replace("'", r"'\''") - - -def make_pkey(private_key): - """Return a paramiko.pkey.PKey from private key string.""" - key_classes = [paramiko.rsakey.RSAKey, - paramiko.dsskey.DSSKey, - paramiko.ecdsakey.ECDSAKey, ] - - keyfile = six.StringIO(private_key) - for cls in key_classes: - keyfile.seek(0) - try: - pkey = cls.from_private_key(keyfile) - except paramiko.SSHException: - continue - else: - keytype = cls - LOG.info("Valid SSH Key provided (%s)", keytype.__name__) - return pkey - - raise paramiko.SSHException("Is not a valid private key") - - -def connect(*args, **kwargs): - """Connect to a remote device over SSH.""" - try: - return SSH.get_client(*args, **kwargs) - except TypeError as exc: - msg = "got an unexpected" - if msg in str(exc): - message = "%s " + str(exc)[str(exc).index(msg):] - raise exc.__class__(message % "connect()") - raise - - -class AcceptMissingHostKey(paramiko.client.MissingHostKeyPolicy): - - """Allow connections to hosts whose fingerprints are not on record.""" - - # pylint: disable=R0903 - def missing_host_key(self, client, hostname, key): - """Add missing host key.""" - # pylint: disable=W0212 - client._host_keys.add(hostname, key.get_name(), key) - - -class SSH(paramiko.SSHClient): # pylint: disable=R0902 - - """Connects to devices via SSH to execute commands.""" - - # pylint: disable=R0913 - def __init__(self, host, password=None, username="root", private_key=None, - root_password=None, key_filename=None, port=22, - timeout=20, gateway=None, options=None, interactive=False): - """Create an instance of the SSH class. - - :param str host: The ip address or host name of the server - to connect to - :param str password: A password to use for authentication - or for unlocking a private key - :param username: The username to authenticate as - :param root_password: root user password to be used if 'username' - is not root. This will use 'username' and - 'password to login and then 'su' to root - using root_password - :param private_key: Private SSH Key string to use - (instead of using a filename) - :param key_filename: a private key filename (path) - :param port: tcp/ip port to use (defaults to 22) - :param float timeout: an optional timeout (in seconds) for the - TCP connection - :param socket gateway: an existing SSH instance to use - for proxying - :param dict options: A dictionary used to set ssh options - (when proxying). - e.g. for `ssh -o StrictHostKeyChecking=no`, - you would provide - (.., options={'StrictHostKeyChecking': 'no'}) - Conversion of booleans is also supported, - (.., options={'StrictHostKeyChecking': False}) - is equivalent. - :keyword interactive: If true, prompt for password if missing. - """ - self.password = password - self.host = host - self.username = username or 'root' - self.root_password = root_password - self.private_key = private_key - self.key_filename = key_filename - self.port = port or 22 - self.timeout = timeout - self._platform_info = None - self.options = options or {} - self.gateway = gateway - self.sock = None - self.interactive = interactive - - self.escalation_command = 'sudo -i %s' - if self.root_password: - self.escalation_command = "su -c '%s'" - - if self.gateway: - if not isinstance(self.gateway, SSH): - raise TypeError("'gateway' must be a satori.ssh.SSH instance. " - "( instances of this type are returned by " - "satori.ssh.connect() )") - - super(SSH, self).__init__() - - def __del__(self): - """Destructor to close the connection.""" - self.close() - - @classmethod - def get_client(cls, *args, **kwargs): - """Return an ssh client object from this module.""" - return cls(*args, **kwargs) - - @property - def platform_info(self): - """Return distro, version, architecture. - - Requires >= Python 2.4 on remote system. - """ - if not self._platform_info: - platform_command = "import platform,sys\n" - platform_command += utils.get_source_definition( - utils.get_platform_info) - platform_command += ("\nsys.stdout.write(str(" - "get_platform_info()))\n") - command = 'echo %s | python' % shellquote(platform_command) - output = self.remote_execute(command) - stdout = re.split('\n|\r\n', output['stdout'])[-1].strip() - if stdout: - try: - plat = ast.literal_eval(stdout) - except SyntaxError as exc: - plat = {'dist': 'unknown'} - LOG.warning("Error parsing response from host '%s': %s", - self.host, output, exc_info=exc) - else: - plat = {'dist': 'unknown'} - LOG.warning("Blank response from host '%s': %s", - self.host, output) - self._platform_info = plat - return self._platform_info - - def connect_with_host_keys(self): - """Try connecting with locally available keys (ex. ~/.ssh/id_rsa).""" - LOG.debug("Trying to connect with local host keys") - return self._connect(look_for_keys=True, allow_agent=False) - - def connect_with_password(self): - """Try connecting with password.""" - LOG.debug("Trying to connect with password") - if self.interactive and not self.password: - LOG.debug("Prompting for password (interactive=%s)", - self.interactive) - try: - self.password = getpass.getpass("Enter password for %s:" % - self.username) - except KeyboardInterrupt: - LOG.debug("User cancelled at password prompt") - if not self.password: - raise paramiko.PasswordRequiredException("Password not provided") - return self._connect( - password=self.password, - look_for_keys=False, - allow_agent=False) - - def connect_with_key_file(self): - """Try connecting with key file.""" - LOG.debug("Trying to connect with key file") - if not self.key_filename: - raise paramiko.AuthenticationException("No key file supplied") - return self._connect( - key_filename=os.path.expanduser(self.key_filename), - look_for_keys=False, - allow_agent=False) - - def connect_with_key(self): - """Try connecting with key string.""" - LOG.debug("Trying to connect with private key string") - if not self.private_key: - raise paramiko.AuthenticationException("No key supplied") - pkey = make_pkey(self.private_key) - return self._connect( - pkey=pkey, - look_for_keys=False, - allow_agent=False) - - def _connect(self, **kwargs): - """Set up client and connect to target.""" - self.load_system_host_keys() - - if self.options.get('StrictHostKeyChecking') in (False, "no"): - self.set_missing_host_key_policy(AcceptMissingHostKey()) - - if self.gateway: - # lazy load - if not self.gateway.get_transport(): - self.gateway.connect() - self.sock = self.gateway.get_transport().open_channel( - 'direct-tcpip', (self.host, self.port), ('', 0)) - - return super(SSH, self).connect( - self.host, - timeout=kwargs.pop('timeout', self.timeout), - port=kwargs.pop('port', self.port), - username=kwargs.pop('username', self.username), - pkey=kwargs.pop('pkey', None), - sock=kwargs.pop('sock', self.sock), - **kwargs) - - def connect(self): # pylint: disable=W0221 - """Attempt an SSH connection through paramiko.SSHClient.connect. - - The order for authentication attempts is: - - private_key - - key_filename - - any key discoverable in ~/.ssh/ - - username/password (will prompt if the password is not supplied and - interactive is true) - """ - # idempotency - if self.get_transport(): - if self.get_transport().is_active(): - return - - if self.private_key: - try: - return self.connect_with_key() - except paramiko.SSHException: - pass # try next method - - if self.key_filename: - try: - return self.connect_with_key_file() - except paramiko.SSHException: - pass # try next method - - try: - return self.connect_with_host_keys() - except paramiko.SSHException: - pass # try next method - - try: - return self.connect_with_password() - except paramiko.BadHostKeyException as exc: - msg = ( - "ssh://%s@%s:%d failed: %s. You might have a bad key " - "entry on your server, but this is a security issue and " - "won't be handled automatically. To fix this you can remove " - "the host entry for this host from the /.ssh/known_hosts file") - LOG.info(msg, self.username, self.host, self.port, exc) - raise exc - except Exception as exc: - LOG.info('ssh://%s@%s:%d failed. %s', - self.username, self.host, self.port, exc) - raise exc - - def test_connection(self): - """Connect to an ssh server and verify that it responds. - - The order for authentication attempts is: - (1) private_key - (2) key_filename - (3) any key discoverable in ~/.ssh/ - (4) username/password - """ - LOG.debug("Checking for a response from ssh://%s@%s:%d.", - self.username, self.host, self.port) - try: - self.connect() - LOG.debug("ssh://%s@%s:%d is up.", - self.username, self.host, self.port) - return True - except Exception as exc: - LOG.info("ssh://%s@%s:%d failed. %s", - self.username, self.host, self.port, exc) - return False - finally: - self.close() - - def close(self): - """Close the connection to the remote host. - - If an ssh tunnel is being used, close that first. - """ - if self.gateway: - self.gateway.close() - return super(SSH, self).close() - - def _handle_tty_required(self, results, get_pty): - """Determine whether the result implies a tty request.""" - if any(m in str(k) for m in TTY_REQUIRED for k in results.values()): - LOG.info('%s requires TTY for sudo/su. Using TTY mode.', - self.host) - if get_pty is True: # if this is *already* True - raise errors.GetPTYRetryFailure( - "Running command with get_pty=True FAILED: %s@%s:%d" - % (self.username, self.host, self.port)) - else: - return True - return False - - def _handle_password_prompt(self, stdin, stdout, su_auth=False): - """Determine whether the remote host is prompting for a password. - - Respond to the prompt through stdin if applicable. - """ - if not stdout.channel.closed: - buflen = len(stdout.channel.in_buffer) - # min and max determined from max username length - # and a set of encountered linux password prompts - if MIN_PASSWORD_PROMPT_LEN < buflen < MAX_PASSWORD_PROMPT_LEN: - prompt = stdout.channel.recv(buflen) - if all(m in prompt.lower() - for m in ['password', ':']): - LOG.warning("%s@%s encountered prompt! of length " - " [%s] {%s}", - self.username, self.host, buflen, prompt) - if su_auth: - LOG.warning("Escalating using 'su -'.") - stdin.write("%s\n" % self.root_password) - else: - stdin.write("%s\n" % self.password) - stdin.flush() - return True - else: - LOG.warning("Nearly a False-Positive on " - "password prompt detection. [%s] {%s}", - buflen, prompt) - stdout.channel.send(prompt) - - return False - - def _command_is_already_running(self, command): - """Check to see if the command is already running using ps & grep.""" - # check plain 'command' w/o prefix or escalation - check_cmd = 'ps -ef |grep -v grep|grep -c "%s"' % command - result = self.remote_execute(check_cmd, keepalive=True, - allow_many=True) - if result['stdout'] != '0': - return True - else: - LOG.debug("Remote command %s IS NOT already running. " - "Continuing with remote_execute.", command) - - def remote_execute(self, command, with_exit_code=False, # noqa - get_pty=False, cwd=None, keepalive=True, - escalate=False, allow_many=True, **kw): - """Execute an ssh command on a remote host. - - Tries cert auth first and falls back - to password auth if password provided. - - :param command: Shell command to be executed by this function. - :param with_exit_code: Include the exit_code in the return body. - :param cwd: The child's current directory will be changed - to `cwd` before it is executed. Note that this - directory is not considered when searching the - executable, so you can't specify the program's - path relative to this argument - :param get_pty: Request a pseudo-terminal from the server. - :param allow_many: If False, do not run command if it is already - found running on remote client. - - :returns: a dict with stdin, stdout, - and (optionally) the exit code of the call. - """ - if escalate and self.username != 'root': - run_command = self.escalation_command % command - else: - run_command = command - - if cwd: - prefix = "cd %s && " % cwd - run_command = prefix + run_command - - # _command_is_already_running wont be called if allow_many is True - # python is great :) - if not allow_many and self._command_is_already_running(command): - raise errors.SatoriDuplicateCommandException( - "Remote command %s is already running and allow_many was " - "set to False. Aborting remote_execute." % command) - try: - self.connect() - results = None - chan = self.get_transport().open_session() - su_auth = False - if 'su -' in run_command: - su_auth = True - get_pty = True - if get_pty: - chan.get_pty() - stdin = chan.makefile('wb') - stdout = chan.makefile('rb') - stderr = chan.makefile_stderr('rb') - LOG.debug("Executing '%s' on ssh://%s@%s:%s.", - run_command, self.username, self.host, self.port) - chan.exec_command(run_command) - LOG.debug('ssh://%s@%s:%d responded.', self.username, self.host, - self.port) - - time.sleep(.25) - self._handle_password_prompt(stdin, stdout, su_auth=su_auth) - - results = { - 'stdout': stdout.read().strip(), - 'stderr': stderr.read() - } - - LOG.debug("STDOUT from ssh://%s@%s:%d: %.5000s ...", - self.username, self.host, self.port, - results['stdout']) - LOG.debug("STDERR from ssh://%s@%s:%d: %.5000s ...", - self.username, self.host, self.port, - results['stderr']) - exit_code = chan.recv_exit_status() - - if with_exit_code: - results.update({'exit_code': exit_code}) - if not keepalive: - chan.close() - - if self._handle_tty_required(results, get_pty): - return self.remote_execute( - command, with_exit_code=with_exit_code, get_pty=True, - cwd=cwd, keepalive=keepalive, escalate=escalate, - allow_many=allow_many) - - return results - - except Exception as exc: - LOG.info("ssh://%s@%s:%d failed. | %s", self.username, self.host, - self.port, exc) - raise - finally: - if not keepalive: - self.close() - - -# Share SSH.__init__'s docstring -connect.__doc__ = SSH.__init__.__doc__ -try: - SSH.__dict__['get_client'].__doc__ = SSH.__dict__['__init__'].__doc__ -except AttributeError: - SSH.get_client.__func__.__doc__ = SSH.__init__.__doc__ diff --git a/satori/sysinfo/__init__.py b/satori/sysinfo/__init__.py deleted file mode 100644 index 921cf73..0000000 --- a/satori/sysinfo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Modules for Data Plane Discovery.""" diff --git a/satori/sysinfo/facter.py b/satori/sysinfo/facter.py deleted file mode 100644 index 19cff0d..0000000 --- a/satori/sysinfo/facter.py +++ /dev/null @@ -1,6 +0,0 @@ -""".""" - - -def get_systeminfo(resource, config, interactive=False): - """.""" - return {'facter': 'is better'} diff --git a/satori/sysinfo/ohai.py b/satori/sysinfo/ohai.py deleted file mode 100644 index 363f6b3..0000000 --- a/satori/sysinfo/ohai.py +++ /dev/null @@ -1,6 +0,0 @@ -""".""" - - -def get_systeminfo(resource, config, interactive=False): - """.""" - return {'ohai': 'there!'} diff --git a/satori/sysinfo/ohai_solo.py b/satori/sysinfo/ohai_solo.py deleted file mode 100644 index 3ef4bad..0000000 --- a/satori/sysinfo/ohai_solo.py +++ /dev/null @@ -1,201 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# pylint: disable=W0622 -"""Ohai Solo Data Plane Discovery Module.""" - -import json -import logging - -import ipaddress as ipaddress_module -import six - -from satori import bash -from satori import errors -from satori import utils - -LOG = logging.getLogger(__name__) - - -def get_systeminfo(ipaddress, config, interactive=False): - """Run data plane discovery using this module against a host. - - :param ipaddress: address to the host to discover. - :param config: arguments and configuration suppplied to satori. - :keyword interactive: whether to prompt the user for information. - """ - if (ipaddress in utils.get_local_ips() or - ipaddress_module.ip_address(six.text_type(ipaddress)).is_loopback): - - client = bash.LocalShell() - client.host = "localhost" - client.port = 0 - perform_install(client) - return system_info(client) - - else: - with bash.RemoteShell( - ipaddress, username=config['host_username'], - private_key=config['host_key'], - interactive=interactive) as client: - perform_install(client) - return system_info(client) - - -def system_info(client, with_install=False, install_dir=None): - """Run ohai-solo on a remote system and gather the output. - - :param client: :class:`ssh.SSH` instance - :param with_install Will install ohai-solo if set to True - :param install_dir string containing directory to install to - :returns: dict -- system information from ohai-solo - :raises: SystemInfoCommandMissing, SystemInfoCommandOld, SystemInfoNotJson - SystemInfoMissingJson - - SystemInfoCommandMissing if `ohai` is not installed. - SystemInfoCommandOld if `ohai` is not the latest. - SystemInfoNotJson if `ohai` does not return valid JSON. - SystemInfoMissingJson if `ohai` does not return any JSON. - """ - if with_install: - perform_install(client, install_dir=install_dir) - - if client.is_windows(): - raise errors.UnsupportedPlatform( - "ohai-solo is a linux-only sytem info provider. " - "Target platform was %s", client.platform_info['dist']) - - ohai_solo_prefix = (install_dir or '/opt') - ohai_solo_command = six.moves.shlex_quote("%s/ohai-solo/bin/ohai-solo" - % ohai_solo_prefix) - command = ("unset GEM_CACHE GEM_HOME GEM_PATH && " - "sudo %s" % ohai_solo_command) - - output = client.execute(command, escalate=True, allow_many=False) - not_found_msgs = ["command not found", "Could not find ohai"] - if any(m in k for m in not_found_msgs - for k in list(output.values()) if isinstance(k, - six.string_types)): - LOG.warning("SystemInfoCommandMissing on host: [%s]", client.host) - raise errors.SystemInfoCommandMissing("ohai-solo missing on %s" % - client.host) - # use string formatting to handle unicode - unicode_output = "%s" % output['stdout'] - try: - results = json.loads(unicode_output) - except ValueError as exc: - try: - clean_output = get_json(unicode_output) - results = json.loads(clean_output) - except ValueError as exc: - raise errors.SystemInfoNotJson(exc) - return results - - -def perform_install(client, install_dir=None): - """Install ohai-solo on remote system. - - :param client: :class:`ssh.SSH` instance - :param install_dir string containing directory to install to - """ - LOG.info("Installing (or updating) ohai-solo on device %s at %s:%d", - client.host, client.host, client.port) - - # Check if it a windows box, but fail safely to Linux - is_windows = False - try: - is_windows = client.is_windows() - except Exception: - pass - if is_windows: - raise errors.UnsupportedPlatform( - "ohai-solo is a linux-only sytem info provider. " - "Target platform was %s", client.platform_info['dist']) - else: - # Download to host - command = ("wget -N http://readonly.configdiscovery.rackspace.com" - "/install.sh") - output = client.execute(command, cwd='/tmp', escalate=True, - allow_many=False) - LOG.debug("Downloaded ohai-solo | %s", output['stdout']) - - # Run install - command = "bash install.sh" - if install_dir: - command = "%s -t -i %s" % (command, - six.moves.shlex_quote(install_dir)) - - install_output = client.execute(command, cwd='/tmp', - with_exit_code=True, - escalate=True, allow_many=False) - LOG.debug("Ran ohai-solo install script. | %s.", - install_output['stdout']) - - # Be a good citizen and clean up your tmp data - command = "rm install.sh" - client.execute(command, cwd='/tmp', escalate=True, allow_many=False) - - # Process install command output - if install_output['exit_code'] != 0: - raise errors.SystemInfoCommandInstallFailed( - install_output['stderr'][:256]) - else: - return install_output - - -def remove_remote(client, install_dir=None): - """Remove ohai-solo from specifc remote system. - - :param install_dir string containing directory ohai-solo was installed in - Currently supports: - - ubuntu [10.x, 12.x] - - debian [6.x, 7.x] - - redhat [5.x, 6.x] - - centos [5.x, 6.x] - """ - if client.is_windows(): - raise errors.UnsupportedPlatform( - "ohai-solo is a linux-only sytem info provider. " - "Target platform was %s", client.platform_info['dist']) - else: - platform_info = client.platform_info - if install_dir is not None: - install_dir = six.moves.shlex_quote("%s/ohai-solo/" % install_dir) - remove = 'rm -rf %s' % install_dir - elif client.is_debian(): - remove = "dpkg --purge ohai-solo" - elif client.is_fedora(): - remove = "yum -y erase ohai-solo" - else: - raise errors.UnsupportedPlatform("Unknown distro: %s" % - platform_info['dist']) - command = "%s" % remove - output = client.execute(command, cwd='/tmp', escalate=True) - return output - - -def get_json(data): - """Find the JSON string in data and return a string. - - :param data: :string: - :returns: string -- JSON string stripped of non-JSON data - :raises: SystemInfoMissingJson - - SystemInfoMissingJson if `ohai` does not return any JSON. - """ - try: - first = data.index('{') - last = data.rindex('}') - return data[first:last + 1] - except ValueError as exc: - context = {"ValueError": "%s" % exc} - raise errors.SystemInfoMissingJson(context) diff --git a/satori/sysinfo/posh_ohai.py b/satori/sysinfo/posh_ohai.py deleted file mode 100644 index e3b2f25..0000000 --- a/satori/sysinfo/posh_ohai.py +++ /dev/null @@ -1,299 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# pylint: disable=W0622 -"""PoSh-Ohai Data Plane Discovery Module.""" - -import json -import logging -import xml.etree.ElementTree as ET - -import ipaddress as ipaddress_module -import six - -from satori import bash -from satori import errors -from satori import utils - -LOG = logging.getLogger(__name__) - - -def get_systeminfo(ipaddress, config, interactive=False): - """Run data plane discovery using this module against a host. - - :param ipaddress: address to the host to discover. - :param config: arguments and configuration suppplied to satori. - :keyword interactive: whether to prompt the user for information. - """ - if (ipaddress in utils.get_local_ips() or - ipaddress_module.ip_address(six.text_type(ipaddress)).is_loopback): - - client = bash.LocalShell() - client.host = "localhost" - client.port = 0 - perform_install(client) - return system_info(client) - - else: - with bash.RemoteShell( - ipaddress, username=config['host_username'], - private_key=config['host_key'], - interactive=interactive) as client: - perform_install(client) - return system_info(client) - - -def system_info(client, with_install=False, install_dir=None): - """Run Posh-Ohai on a remote system and gather the output. - - :param client: :class:`smb.SMB` instance - :param install_dir -- this is for compatibility and is ignored - :returns: dict -- system information from PoSh-Ohai - :raises: SystemInfoCommandMissing, SystemInfoCommandOld, SystemInfoInvalid - - SystemInfoCommandMissing if `posh-ohai` is not installed. - SystemInfoCommandOld if `posh-ohai` is not the latest. - SystemInfoInvalid if `posh-ohai` does not return valid JSON or XML. - """ - if with_install: - perform_install(client) - - if client.is_windows(): - powershell_command = ('Import-Module -Name Posh-Ohai;' - 'Get-ComputerConfiguration') - output = client.execute(powershell_command) - unicode_output = "%s" % output - load_clean_json = lambda output: json.loads(get_json(output)) - last_err = None - for loader in json.loads, parse_xml, load_clean_json: - try: - return loader(unicode_output) - except ValueError as err: - last_err = err - raise errors.SystemInfoInvalid(last_err) - else: - raise errors.PlatformNotSupported( - "PoSh-Ohai is a Windows-only sytem info provider. " - "Target platform was %s", client.platform_info['dist']) - - -def perform_install(client, install_dir=None): - """Install PoSh-Ohai on remote system. - - :param install_dir -- For compatibility. Ignored. - """ - LOG.info("Installing (or updating) PoSh-Ohai on device %s at %s:%d", - client.host, client.host, client.port) - - # Check is it is a windows box, but fail safely to Linux - is_windows = False - try: - is_windows = client.is_windows() - except Exception: - pass - if is_windows: - powershell_command = ('[scriptblock]::Create((New-Object -TypeName ' - 'System.Net.WebClient).DownloadString(' - '"http://readonly.configdiscovery.rackspace.com' - '/deploy.ps1")).Invoke()') - # check output to ensure that installation was successful - # if not, raise SystemInfoCommandInstallFailed - output = client.execute(powershell_command) - return output - else: - raise errors.PlatformNotSupported( - "PoSh-Ohai is a Windows-only sytem info provider. " - "Target platform was %s", client.platform_info['dist']) - - -def remove_remote(client, install_dir=None): - """Remove PoSh-Ohai from specifc remote system. - - :param install_dir -- for compatibility. Ignored. - - Currently supports: - - ubuntu [10.x, 12.x] - - debian [6.x, 7.x] - - redhat [5.x, 6.x] - - centos [5.x, 6.x] - """ - if client.is_windows(): - powershell_command = ('Remove-Item -Path (Join-Path -Path ' - '$($env:PSModulePath.Split(";") ' - '| Where-Object { $_.StartsWith(' - '$env:SystemRoot)}) -ChildPath ' - '"PoSh-Ohai") -Recurse -Force -ErrorAction ' - 'SilentlyContinue') - output = client.execute(powershell_command) - return output - else: - raise errors.PlatformNotSupported( - "PoSh-Ohai is a Windows-only sytem info provider. " - "Target platform was %s", client.platform_info['dist']) - - -def get_json(data): - """Find the JSON string in data and return a string. - - :param data: :string: - :returns: string -- JSON string stripped of non-JSON data - """ - first = data.index('{') - last = data.rindex('}') - return data[first:last + 1] - - -def parse_text(elem): - """Parse text from an element. - - >>> parse_text(ET.XML('Hello World')) - 'Hello World' - >>> parse_text(ET.XML('True ')) - True - >>> parse_text(ET.XML('123')) - 123 - >>> print(parse_text(ET.XML(''))) - None - """ - if elem.text is None: - return None - try: - return int(elem.text) - except ValueError: - pass - text = elem.text.strip() - if text == 'True': - return True - if text == 'False': - return False - return elem.text - - -def parse_list(elem): - """Parse list of properties. - - >>> parse_list(ET.XML('')) - [] - >>> xml = ''' - ... Hello - ... World - ... ''' - >>> parse_list(ET.XML(xml)) - ['Hello', 'World'] - """ - return [parse_elem(c) for c in elem] - - -def parse_attrib_dict(elem): - """Parse list of properties. - - >>> parse_attrib_dict(ET.XML('')) - {} - >>> xml = ''' - ... Hello - ... World - ... ''' - >>> d = parse_attrib_dict(ET.XML(xml)) - >>> sorted(d.items()) - [('noun', 'World'), ('verb', 'Hello')] - """ - keys = [c.get('Name') for c in elem] - values = [parse_elem(c) for c in elem] - return dict(zip(keys, values)) - - -def parse_key_value_dict(elem): - """Parse list of properties. - - >>> parse_key_value_dict(ET.XML('')) - {} - >>> xml = ''' - ... verb - ... Hello - ... noun - ... World - ... ''' - >>> d = parse_key_value_dict(ET.XML(xml)) - >>> sorted(d.items()) - [('noun', 'World'), ('verb', 'Hello')] - """ - keys = [c.text for c in elem[::2]] - values = [parse_elem(c) for c in elem[1::2]] - return dict(zip(keys, values)) - - -def parse_elem(elem): - """Determine element type and dispatch to other parse functions.""" - if len(elem) == 0: - return parse_text(elem) - if not elem[0].attrib: - return parse_list(elem) - if elem[0].get('Name') == 'Key': - return parse_key_value_dict(elem) - return parse_attrib_dict(elem) - - -def parse_xml(ohai_output): - r"""Parse XML Posh-Ohai output. - - >>> output = '''\ - ... - ... - ... - ... platform_family - ... Windows - ... logonhistory - ... - ... 0x6dd0359 - ... - ... user - ... WIN2008R2\\Administrator - ... logontype - ... 10 - ... - ... - ... loggedon_users - ... - ... - ... 995 - ... WIN2008R2\IUSR - ... Service - ... - ... - ... 999 - ... WIN2008R2\SYSTEM - ... Local System - ... - ... - ... - ... ''' - >>> import pprint - >>> pprint.pprint(parse_xml(output)) - {'loggedon_users': [{'Session': 995, - 'Type': 'Service', - 'User': 'WIN2008R2\\IUSR'}, - {'Session': 999, - 'Type': 'Local System', - 'User': 'WIN2008R2\\SYSTEM'}], - 'logonhistory': {'0x6dd0359': {'logontype': 10, - 'user': 'WIN2008R2\\Administrator'}}, - 'platform_family': 'Windows'} - """ - try: - root = ET.XML(ohai_output) - except ET.ParseError as err: - raise ValueError(err) - try: - properties = root[0] - except IndexError as err: - raise ValueError('XML had unexpected structure') - return parse_elem(properties) diff --git a/satori/tests/__init__.py b/satori/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/satori/tests/test_bash.py b/satori/tests/test_bash.py deleted file mode 100644 index b9da53c..0000000 --- a/satori/tests/test_bash.py +++ /dev/null @@ -1,208 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# pylint: disable=C0111, C0103, W0212, R0904 -"""Satori SSH Module Tests.""" - -import collections -import unittest - -import mock - -from satori import bash -from satori import errors -from satori.tests import utils - - -class TestBashModule(utils.TestCase): - - def setUp(self): - super(TestBashModule, self).setUp() - testrun = collections.namedtuple( - "TestCmd", ["command", "stdout", "returncode"]) - self.testrun = testrun( - command="echo hello", stdout="hello\n", returncode=0) - self.resultdict = {'stdout': self.testrun.stdout.strip(), - 'stderr': ''} - - -class TestLocalShell(TestBashModule): - - def setUp(self): - super(TestLocalShell, self).setUp() - popen_patcher = mock.patch.object(bash.popen, 'popen') - self.mock_popen = popen_patcher.start() - mock_result = mock.MagicMock() - mock_result.returncode = self.testrun.returncode - self.mock_popen.return_value = mock_result - mock_result.communicate.return_value = (self.testrun.stdout, '') - self.localshell = bash.LocalShell() - self.addCleanup(popen_patcher.stop) - - def test_execute(self): - self.localshell.execute(self.testrun.command) - self.mock_popen.assert_called_once_with( - self.testrun.command.split(), cwd=None, stderr=-1, stdout=-1) - - def test_execute_resultdict(self): - resultdict = self.localshell.execute(self.testrun.command) - self.assertEqual(self.resultdict, resultdict) - - def test_execute_with_exit_code_resultdict(self): - resultdict = self.localshell.execute( - self.testrun.command, with_exit_code=True) - self.resultdict.update({'exit_code': self.testrun.returncode}) - self.assertEqual(self.resultdict, resultdict) - - -@mock.patch.object(bash.utils, 'get_platform_info') -class TestLocalPlatformInfo(utils.TestCase): - - def test_is_debian(self, mock_gpi): - mock_gpi.return_value = {'dist': 'Debian'} - self.assertIsInstance(bash.LocalShell().is_debian(), bool) - - def test_is_fedora(self, mock_gpi): - mock_gpi.return_value = {'dist': 'Fedora'} - self.assertIsInstance(bash.LocalShell().is_fedora(), bool) - - def test_is_osx(self, mock_gpi): - mock_gpi.return_value = {'dist': 'Darwin'} - self.assertIsInstance(bash.LocalShell().is_windows(), bool) - - def test_is_windows(self, mock_gpi): - mock_gpi.return_value = {'dist': 'Windows'} - self.assertIsInstance(bash.LocalShell().is_osx(), bool) - - -class TestLocalPlatformInfoUndetermined(TestLocalShell): - - def setUp(self): - blanks = {'dist': '', 'arch': '', 'version': ''} - pinfo_patcher = mock.patch.object( - bash.LocalShell, 'platform_info', new_callable=mock.PropertyMock) - self.mock_platform_info = pinfo_patcher.start() - self.mock_platform_info.return_value = blanks - super(TestLocalPlatformInfoUndetermined, self).setUp() - self.addCleanup(pinfo_patcher.stop) - - def test_is_debian(self): - self.assertRaises(errors.UndeterminedPlatform, - self.localshell.is_debian) - - def test_is_fedora(self): - self.assertRaises(errors.UndeterminedPlatform, - self.localshell.is_fedora) - - def test_is_osx(self): - self.assertRaises(errors.UndeterminedPlatform, - self.localshell.is_osx) - - def test_is_windows(self): - self.assertRaises(errors.UndeterminedPlatform, - self.localshell.is_windows) - - -class TestRemoteShell(TestBashModule): - - def setUp(self): - super(TestRemoteShell, self).setUp() - execute_patcher = mock.patch.object(bash.ssh.SSH, 'remote_execute') - self.mock_execute = execute_patcher.start() - self.mock_execute.return_value = self.resultdict - self.remoteshell = bash.RemoteShell('192.168.2.10') - self.addCleanup(execute_patcher.stop) - - def test_execute(self): - self.remoteshell.execute(self.testrun.command) - self.mock_execute.assert_called_once_with( - self.testrun.command) - - def test_execute_resultdict(self): - resultdict = self.remoteshell.execute(self.testrun.command) - self.assertEqual(self.resultdict, resultdict) - - def test_execute_with_exit_code_resultdict(self): - resultdict = self.remoteshell.execute( - self.testrun.command, with_exit_code=True) - self.resultdict.update({'exit_code': self.testrun.returncode}) - self.assertEqual(self.resultdict, resultdict) - - -class TestRemoteShellInit(unittest.TestCase): - - def initpatch(self, ssh_instance, *args, **kwargs): - ssh_instance.host = self.host - ssh_instance.port = self.port - self._instance = ssh_instance - - def setUp(self): - self.host = "192.168.2.10" - self.port = 23 - - @mock.patch.object(bash.ssh.SSH, '__init__', return_value=None, - autospec=True) - def test_init_contains_kwargs(self, mock_init): - mock_init.side_effect = self.initpatch - allkwargs = { - 'password': 'pass', - 'username': 'user', - 'private_key': 'pkey', - 'key_filename': 'pkeyfile', - 'port': self.port, - 'timeout': 100, - 'gateway': 'g', - 'options': {'StrictHostKeyChecking': False}, - 'interactive': True, - 'root_password': 'sudopass', - } - self.remoteshell = bash.RemoteShell(self.host, **allkwargs) - mock_init.assert_called_once_with(self._instance, self.host, **allkwargs) - - -class TestContextManager(utils.TestCase): - - def setUp(self): - super(TestContextManager, self).setUp() - connect_patcher = mock.patch.object(bash.RemoteShell, 'connect') - close_patcher = mock.patch.object(bash.RemoteShell, 'close') - self.mock_connect = connect_patcher.start() - self.mock_close = close_patcher.start() - self.addCleanup(connect_patcher.stop) - self.addCleanup(close_patcher.stop) - - def test_context_manager(self): - with bash.RemoteShell('192.168.2.10') as client: - pass - self.assertTrue(self.mock_connect.call_count == 1) - # >=1 because __del__ (in most python implementations) - # calls close() - self.assertTrue(self.mock_close.call_count >= 1) - -class TestIsDistro(TestRemoteShell): - - def setUp(self): - super(TestIsDistro, self).setUp() - self.platformdict = self.resultdict.copy() - self.platformdict['stdout'] = str(bash.LocalShell().platform_info) - - def test_remote_platform_info(self): - self.mock_execute.return_value = self.platformdict - result = self.remoteshell.platform_info - self.assertIsInstance(result, dict) - self.assertTrue(all(k in result - for k in ('arch', 'dist', 'version'))) - assert self.mock_execute.called - - -if __name__ == "__main__": - unittest.main() diff --git a/satori/tests/test_common_logging.py b/satori/tests/test_common_logging.py deleted file mode 100644 index 2aececf..0000000 --- a/satori/tests/test_common_logging.py +++ /dev/null @@ -1,58 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Satori Logging Module Tests.""" - -import logging as stdlib_logging -import unittest - -import mock - -from satori.common import logging -from satori.tests import utils - - -class TestLoggingSetup(utils.TestCase): - - """Logging Setup tests.""" - - def test_logging_default_info(self): - config = {} - with mock.patch.dict(config, {'logconfig': None}): - logging.init_logging(config) - self.assertEqual(stdlib_logging.getLogger().level, - stdlib_logging.INFO) - - def test_logging_debug_flag(self): - config = {} - with mock.patch.dict(config, {'logconfig': None, 'debug': True}): - logging.init_logging(config) - self.assertEqual(stdlib_logging.getLogger().level, - stdlib_logging.DEBUG) - - def test_logging_verbose_flag(self): - config = {} - with mock.patch.dict(config, {'logconfig': None, 'verbose': True}): - logging.init_logging(config) - self.assertEqual(stdlib_logging.getLogger().level, - stdlib_logging.DEBUG) - - def test_logging_quiet_flag(self): - config = {} - with mock.patch.dict(config, {'logconfig': None, 'quiet': True}): - logging.init_logging(config) - self.assertEqual(stdlib_logging.getLogger().level, - stdlib_logging.WARN) - - -if __name__ == "__main__": - unittest.main() diff --git a/satori/tests/test_common_templating.py b/satori/tests/test_common_templating.py deleted file mode 100644 index 98b4389..0000000 --- a/satori/tests/test_common_templating.py +++ /dev/null @@ -1,79 +0,0 @@ -# pylint: disable=C0103,R0904 - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Unit Tests for Templating module.""" - -import unittest - -from satori.common import templating - - -def fail_fixture(): - """Used to simulate a template error.""" - raise AttributeError("Boom!") - - -class TestTemplating(unittest.TestCase): - - """Test Templating Module.""" - - def test_prepend_function(self): - """preserve returns escaped linefeeds.""" - result = templating.parse("{{ root|prepend('/')}}/path", root="etc") - self.assertEqual(result, '/etc/path') - - def test_prepend_function_blank(self): - """preserve returns escaped linefeeds.""" - result = templating.parse("{{ root|prepend('/')}}/path") - self.assertEqual(result, '/path') - - def test_preserve_linefeed_escaping(self): - """preserve returns escaped linefeeds.""" - result = templating.parse('{{ "A\nB" | preserve }}') - self.assertEqual(result, 'A\\nB') - - def test_template_extra_globals(self): - """Globals are available in template.""" - result = templating.parse("{{ foo }}", foo="bar") - self.assertEqual(result, 'bar') - - def test_template_syntax_error(self): - """jinja.TemplateSyntaxError is caught.""" - self.assertRaises(templating.TemplateException, templating.parse, - "{{ not closed") - - def test_template_undefined_error(self): - """jinja.UndefinedError is caught.""" - self.assertRaises(templating.TemplateException, templating.parse, - "{{ unknown() }}") - - def test_template_exception(self): - """Exception in global is caught.""" - self.assertRaises(templating.TemplateException, templating.parse, - "{{ boom() }}", boom=fail_fixture) - - def test_extra_globals(self): - """Validates globals are set.""" - env = templating.get_jinja_environment("", {'foo': 1}) - self.assertTrue('foo' in env.globals) - self.assertEqual(env.globals['foo'], 1) - - def test_json_included(self): - """json library available to template.""" - result = templating.parse("{{ json.dumps({'data': 1}) }}") - self.assertEqual(result, '{"data": 1}') - - -if __name__ == '__main__': - unittest.main() diff --git a/satori/tests/test_dns.py b/satori/tests/test_dns.py deleted file mode 100644 index 67bd193..0000000 --- a/satori/tests/test_dns.py +++ /dev/null @@ -1,294 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Satori DNS Discovery.""" - -import socket -import unittest - -from freezegun import freeze_time -import mock -import pythonwhois -import six - -from satori import dns -from satori import errors -from satori.tests import utils - - -class TestDNS(utils.TestCase): - - def setUp(self): - self.ip = "4.3.2.1" - self.domain = "domain.com" - self.mysocket = socket - self.mysocket.gethostbyname = mock.MagicMock(name='gethostbyname') - - self.WHOIS = [""" - The data in Fake Company WHOIS database is provided - by Fake Company for information purposes only. By submitting - WHOIS query, you agree that you will use this data only for lawful - purpose. In addition, you agree not to: - (a) use the data to allow, enable, or otherwise support marketing - activities, regardless of the medium. Such media include but are - not limited to e-mail, telephone, facsimile, postal mail, SMS, and - wireless alerts; or - (b) use the data to enable high volume, electronic processes - that sendqueries or data to the systems of any Registry Operator or - ICANN-Accredited registrar, except as necessary to register - domain names or modify existing registrations. - (c) sell or redistribute the data except insofar as it has been - incorporated into a value-added product that does not permit - the extraction of a portion of the data from the value-added - product or service for use by other parties. - Fake Company reserves the right to modify these terms at any time. - Fake Company cannot guarantee the accuracy of the data provided. - By accessing and using Fake Company WHOIS service, you agree to - these terms. - - NOTE: FAILURE TO LOCATE A RECORD IN THE WHOIS DATABASE IS NOT - INDICATIVE OF THE AVAILABILITY OF A DOMAIN NAME. - - Domain Name: mytestdomain.com - Registry Domain ID: - Registrar WHOIS Server: whois.fakecompany.com - Registrar URL: http://www.fakecompany.com - Updated Date: 2013-08-15T05:02:28Z - Creation Date: 2010-11-01T23:57:06Z - Registrar Registration Expiration Date: 2020-01-01T00:00:00Z - Registrar: Fake Company, Inc - Registrar IANA ID: 106 - Registrar Abuse Contact Email: abuse@fakecompany.com - Registrar Abuse Contact Phone: +44.2070159370 - Reseller: - Domain Status: ACTIVE - Registry Registrant ID: - Registrant Name: Host Master - Registrant Organization: Rackspace US, Inc. - Registrant Street: 5000 Walzem Road - Registrant City: San Antonio, - Registrant State/Province: Texas - Registrant Postal Code: 78218 - Registrant Country: US - Registrant Phone: - Registrant Phone Ext: - Registrant Fax: - Registrant Fax Ext: - Registrant Email: - Registry Admin ID: - Admin Name: Host Master - Admin Organization: Rackspace US, Inc. - Admin Street: 5000 Walzem Road - Admin City: San Antonio, - Admin State/Province: Texas - Admin Postal Code: 78218 - Admin Country: US - Admin Phone: +1.2103124712 - Admin Phone Ext: - Admin Fax: - Admin Fax Ext: - Admin Email: domains@rackspace.com - Registry Tech ID: - Tech Name: Domain Administrator - Tech Organization: NetNames Hostmaster - Tech Street: 3rd Floor Prospero House - Tech Street: 241 Borough High Street - Tech City: Borough - Tech State/Province: London - Tech Postal Code: SE1 1GA - Tech Country: GB - Tech Phone: +44.2070159370 - Tech Phone Ext: - Tech Fax: +44.2070159375 - Tech Fax Ext: - Tech Email: corporate-services@netnames.com - Name Server: ns1.domain.com - Name Server: ns2.domain.com - DNSSEC: - URL of the ICANN WHOIS Data Problem System: http://wdprs.fake.net/ - >>> Last update of WHOIS database: 2014-02-18T03:39:52 UTC <<< - """] - - patcher = mock.patch.object(pythonwhois.net, 'get_whois_raw') - self.mock_get_whois_raw = patcher.start() - self.mock_get_whois_raw.return_value = self.WHOIS, [None] - self.addCleanup(patcher.stop) - - super(TestDNS, self).setUp() - - def test_resolve_domain_name_returns_ip(self): - self.mysocket.gethostbyname.return_value = self.ip - self.assertEqual(self.ip, dns.resolve_hostname(self.domain)) - - def test_resolve_ip_returns_ip(self): - self.mysocket.gethostbyname.return_value = self.ip - self.assertEqual(self.ip, dns.resolve_hostname(self.ip)) - - def test_resolve_int_raises_invalid_netloc_error(self): - self.assertRaises( - errors.SatoriInvalidNetloc, - dns.parse_target_hostname, - 100) - - def test_resolve_none_raises_invalid_netloc_error(self): - self.assertRaises( - errors.SatoriInvalidNetloc, - dns.parse_target_hostname, - None) - - def test_registered_domain_subdomain_removed(self): - self.assertEqual( - self.domain, - dns.get_registered_domain("www." + self.domain) - ) - - def test_registered_domain_path_removed(self): - self.assertEqual( - self.domain, - dns.get_registered_domain("www." + self.domain + "/path") - ) - - def test_domain_info_returns_nameservers_from_whois(self): - data = dns.domain_info(self.domain) - self.assertEqual( - ['ns1.domain.com', 'ns2.domain.com'], - data['nameservers'] - ) - - def test_domain_info_returns_nameservers_as_list(self): - data = dns.domain_info(self.domain) - self.assertIsInstance( - data['nameservers'], - list - ) - - def test_domain_info_returns_registrar_from_whois(self): - data = dns.domain_info(self.domain) - self.assertEqual( - 'Fake Company, Inc', - data['registrar'] - ) - - def test_domain_info_returns_no_registrar_from_whois(self): - small_whois = [""" - Domain : example.io - Status : Live - Expiry : 2014-11-06 - - NS 1 : dns1.example.com - NS 2 : dns2.example.com - """] - self.mock_get_whois_raw.return_value = small_whois, [None] - data = dns.domain_info(self.domain) - self.assertEqual( - [], - data['registrar'] - ) - - def test_domain_info_returns_domain_name_from_parameter(self): - data = dns.domain_info(self.domain) - self.assertEqual( - self.domain, - data['name'] - ) - - def test_domain_info_returns_slimmed_down_domain_name(self): - data = dns.domain_info("s1.www." + self.domain) - self.assertEqual( - self.domain, - data['name'] - ) - - @freeze_time("2019-01-01") - def test_domain_info_returns_365_day_expiration(self): - data = dns.domain_info(self.domain) - self.assertEqual( - 365, - data['days_until_expires'] - ) - - def test_domain_info_returns_none_for_days_until_expires(self): - small_whois = [""" - Domain : example.io - Status : Live - - NS 1 : dns1.example.com - NS 2 : dns2.example.com - """] - self.mock_get_whois_raw.return_value = small_whois, [None] - data = dns.domain_info(self.domain) - self.assertEqual( - data['days_until_expires'], - None - ) - - def test_domain_info_returns_array_of_strings_whois_data(self): - data = dns.domain_info(self.domain) - self.assertIsInstance(data['whois'][0], str) - - def test_domain_info_returns_string_date_for_expiry(self): - small_whois = [""" - Domain : example.io - Status : Live - Expiry : 2014-11-06 - - NS 1 : dns1.example.com - NS 2 : dns2.example.com - """] - self.mock_get_whois_raw.return_value = small_whois, [None] - data = dns.domain_info(self.domain) - self.assertIsInstance(data['expiration_date'], six.string_types) - - def test_domain_info_returns_string_for_expiration_date_string(self): - data = dns.domain_info(self.domain) - self.assertIsInstance(data['expiration_date'], six.string_types) - - def test_domain_info_returns_none_for_missing_expiration_date(self): - small_whois = [""" - Domain : example.io - Status : Live - - NS 1 : dns1.example.com - NS 2 : dns2.example.com - """] - self.mock_get_whois_raw.return_value = small_whois, [None] - data = dns.domain_info(self.domain) - self.assertIsNone(data['expiration_date']) - - def test_domain_info_raises_invalid_domain_error(self): - ip_whois = [""" - Home net HOME-NET-192-168 (NET-192-0-0-0-1) - Home Inc. HOME-NET-192-168-0 (NET-192-168-0-0-1) - """] - self.mock_get_whois_raw.return_value = ip_whois, [None] - self.assertRaises( - errors.SatoriInvalidDomain, - dns.domain_info, - "192.168.0.1" - ) - - def test_ip_info_raises_invalid_ip_error(self): - self.assertRaises( - errors.SatoriInvalidIP, - dns.ip_info, - "example.com" - ) - - def test_ip_info_raises_invalid_ip_error_bad_ip(self): - self.assertRaises( - errors.SatoriInvalidIP, - dns.ip_info, - "1.2.3" - ) - -if __name__ == "__main__": - unittest.main() diff --git a/satori/tests/test_formats_text.py b/satori/tests/test_formats_text.py deleted file mode 100644 index 9d457d5..0000000 --- a/satori/tests/test_formats_text.py +++ /dev/null @@ -1,188 +0,0 @@ -# pylint: disable=C0103,R0904 - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Tests for Format Templates.""" - -import unittest - -from satori.common import templating -from satori import shell - - -class TestTextTemplate(unittest.TestCase): - - """Test Text Template.""" - - def setUp(self): - self.template = shell.get_template('text') - - def test_no_data(self): - """Handles response with no host.""" - env_vars = dict(lstrip_blocks=True, trim_blocks=True) - result = templating.parse(self.template, data={}, env_vars=env_vars) - self.assertEqual(result.strip('\n'), 'Host not found') - - def test_target_is_ip(self): - """Handles response when host is just the supplied address.""" - env_vars = dict(lstrip_blocks=True, trim_blocks=True) - result = templating.parse(self.template, target='127.0.0.1', - data={'found': {'ip-address': '127.0.0.1'}}, - env_vars=env_vars) - self.assertEqual(result.strip('\n'), - 'Host:\n ip-address: 127.0.0.1') - - def test_host_not_server(self): - """Handles response when host is not a nova instance.""" - env_vars = dict(lstrip_blocks=True, trim_blocks=True) - result = templating.parse(self.template, target='localhost', - data={'found': {'ip-address': '127.0.0.1'}}, - env_vars=env_vars) - self.assertEqual(result.strip('\n'), - 'Address:\n localhost resolves to IPv4 address ' - '127.0.0.1\nHost:\n ip-address: 127.0.0.1') - - def test_host_is_nova_instance(self): - """Handles response when host is a nova instance.""" - data = { - 'found': { - 'ip-address': '10.1.1.45', - 'hostname': 'x', - 'host-key': 'https://servers/path' - }, - 'target': 'instance.nova.local', - 'resources': { - 'https://servers/path': { - 'type': 'OS::Nova::Instance', - 'data': { - 'uri': 'https://servers/path', - 'id': '1000B', - 'name': 'x', - 'addresses': { - 'public': [{'type': 'ipv4', 'addr': '10.1.1.45'}] - }, - 'system_info': { - 'connections': { - '192.168.2.100': [], - '192.168.2.101': [433], - '192.168.2.102': [8080, 8081] - }, - 'remote_services': [ - { - 'ip': '0.0.0.0', - 'process': 'nginx', - 'port': 80 - } - ] - } - } - } - } - } - env_vars = dict(lstrip_blocks=True, trim_blocks=True) - result = templating.parse(self.template, - target='instance.nova.local', - data=data, env_vars=env_vars) - expected = """\ -Address: - instance.nova.local resolves to IPv4 address 10.1.1.45 -Host: - 10.1.1.45 (instance.nova.local) is hosted on a Nova instance - Instance Information: - URI: https://servers/path - Name: x - ID: 1000B - ip-addresses: - public: - 10.1.1.45 - Listening Services: - 0.0.0.0:80 nginx - Talking to: - 192.168.2.100 - 192.168.2.101 on 433 - 192.168.2.102 on 8080, 8081""" - self.assertEqual(result.strip('\n'), expected) - - def test_host_has_no_data(self): - """Handles response when host is a nova instance.""" - data = { - 'found': { - 'ip-address': '10.1.1.45', - 'hostname': 'x', - 'host-key': 'https://servers/path' - }, - 'target': 'instance.nova.local', - 'resources': { - 'https://servers/path': { - 'type': 'OS::Nova::Instance' - } - } - } - env_vars = dict(lstrip_blocks=True, trim_blocks=True) - result = templating.parse(self.template, - target='instance.nova.local', - data=data, env_vars=env_vars) - expected = """\ -Address: - instance.nova.local resolves to IPv4 address 10.1.1.45 -Host: - 10.1.1.45 (instance.nova.local) is hosted on a Nova instance""" - self.assertEqual(result.strip('\n'), expected) - - def test_host_data_missing_items(self): - """Handles response when host is a nova instance.""" - data = { - 'found': { - 'ip-address': '10.1.1.45', - 'hostname': 'x', - 'host-key': 'https://servers/path' - }, - 'target': 'instance.nova.local', - 'resources': { - 'https://servers/path': { - 'type': 'OS::Nova::Instance', - 'data': { - 'id': '1000B', - 'system_info': { - 'remote_services': [ - { - 'ip': '0.0.0.0', - 'process': 'nginx', - 'port': 80 - } - ] - } - } - } - } - } - env_vars = dict(lstrip_blocks=True, trim_blocks=True) - result = templating.parse(self.template, - target='instance.nova.local', - data=data, env_vars=env_vars) - expected = """\ -Address: - instance.nova.local resolves to IPv4 address 10.1.1.45 -Host: - 10.1.1.45 (instance.nova.local) is hosted on a Nova instance - Instance Information: - URI: n/a - Name: n/a - ID: 1000B - Listening Services: - 0.0.0.0:80 nginx""" - self.assertEqual(result.strip('\n'), expected) - - -if __name__ == '__main__': - unittest.main() diff --git a/satori/tests/test_shell.py b/satori/tests/test_shell.py deleted file mode 100644 index 6b1b874..0000000 --- a/satori/tests/test_shell.py +++ /dev/null @@ -1,159 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Unit Tests for Shell module.""" - -import copy -import sys -import unittest - -import fixtures -import mock -import six - -from satori import errors -from satori import shell -from satori.tests import utils - - -if six.PY2: - BUILTINS = "__builtin__" -else: - BUILTINS = "builtins" - - -class TestTemplating(utils.TestCase): - - """Test Templating Code.""" - - @mock.patch('%s.open' % BUILTINS) - def test_get_template(self, mock_open): - """Verify that get_template looks for the right template.""" - manager = mock_open.return_value.__enter__.return_value - manager.read.return_value = 'some data' - result = shell.get_template("foo") - self.assertEqual(result, "some data") - call_ = mock_open.call_args_list[0] - args, _ = call_ - path, modifier = args - self.assertTrue(path.endswith("/foo.jinja")) - self.assertEqual(modifier, 'r') - - @mock.patch.object(shell, 'get_template') - def test_output_results(self, mock_template): - """Verify that output formatter parses supllied template.""" - mock_template.return_value = 'Output: {{ data.foo }}' - result = shell.format_output("127.0.0.1", {'foo': 1}) - self.assertEqual(result, "Output: 1") - -FAKE_ENV = { - 'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where' -} - -FAKE_ENV2 = { - 'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where' -} - - -class TestArgParsing(utils.TestCase): - - """Test Argument Parsing.""" - - def setUp(self): - super(TestArgParsing, self).setUp() - patcher = mock.patch.object(shell, "os") - self.mock_os = patcher.start() - self.mock_os.environ = {} - self.addCleanup(patcher.stop) - - def make_env(self, exclude=None, fake_env=FAKE_ENV): - """Create a patched os.environ. - - Borrowed from python-novaclient/novaclient/tests/test_shell.py. - """ - - env = dict((k, v) for k, v in fake_env.items() if k != exclude) - self.useFixture(fixtures.MonkeyPatch('os.environ', env)) - - def run_shell(self, argstr, exitcodes=(0,)): - """Simulate a user shell. - - Borrowed from python-novaclient/novaclient/tests/test_shell.py. - """ - - orig = sys.stdout - orig_stderr = sys.stderr - try: - sys.stdout = six.StringIO() - sys.stderr = six.StringIO() - shell.main(argstr.split()) - except SystemExit: - exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertIn(exc_value.code, exitcodes) - finally: - stdout = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - stderr = sys.stderr.getvalue() - sys.stderr.close() - sys.stderr = orig_stderr - return (stdout, stderr) - - def test_missing_openstack_field_raises_argument_exception(self): - """Verify that all 'required' OpenStack fields are needed. - - Iterate over the list of fields, remove one and verify that - an exception is raised. - """ - fields = [ - '--os-username=bob', - '--os-password=secret', - '--os-auth-url=http://domain.com/v1/auth', - '--os-region-name=hawaii', - '--os-tenant-name=bobs-better-burger', - ] - for i in range(len(fields)): - fields_copy = copy.copy(fields) - fields_copy.pop(i) - fields_copy.append('domain.com') - self.assertRaises( - errors.SatoriShellException, - self.run_shell, - ' '.join(fields_copy), - exitcodes=[0, 2] - ) - - def test_netloc_parser(self): - self.assertEqual(shell.netloc_parser("localhost"), - (None, 'localhost')) - - def test_netloc_parser_both(self): - self.assertEqual(shell.netloc_parser("name@address"), - ('name', 'address')) - - def test_netloc_parser_edge(self): - self.assertEqual(shell.netloc_parser("@address"), - (None, 'address')) - self.assertEqual(shell.netloc_parser("root@"), - ('root', None)) - self.assertEqual(shell.netloc_parser(""), - (None, None)) - - -if __name__ == '__main__': - unittest.main() diff --git a/satori/tests/test_ssh.py b/satori/tests/test_ssh.py deleted file mode 100644 index 5e3d3e3..0000000 --- a/satori/tests/test_ssh.py +++ /dev/null @@ -1,666 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# pylint: disable=C0111, C0103, W0212, R0904 -"""Satori SSH Module Tests.""" - -import os -import unittest - -import mock -import paramiko - -from satori import errors -from satori import ssh -from satori.tests import utils - - -class TestTTYRequired(utils.TestCase): - - """Test response to tty demand.""" - - def setUp(self): - - super(TestTTYRequired, self).setUp() - self.client = ssh.SSH('123.456.789.0', password='test_password') - self.stdout = mock.MagicMock() - self.stdin = mock.MagicMock() - - def test_valid_demand(self): - """Ensure that anticipated requests for tty's return True.""" - for substring in ssh.TTY_REQUIRED: - results = {'stdout': "xyz" + substring + "zyx"} - self.assertTrue(self.client._handle_tty_required(results, False)) - - def test_normal_response(self): - """Ensure standard response returns False.""" - examples = ["hello", "#75-Ubuntu SMP Tue Jun 18 17:59:38 UTC 2013", - ("fatal: Not a git repository " - "(or any of the parent directories): .git")] - for substring in examples: - results = {'stderr': '', 'stdout': substring} - self.assertFalse(self.client._handle_tty_required(results, False)) - - def test_no_recurse(self): - """Avoid infinte loop by raising GetPTYRetryFailure. - - When retrying with get_pty in response to one of TTY_REQUIRED - """ - for substring in ssh.TTY_REQUIRED: - results = {'stdout': substring} - self.assertRaises(errors.GetPTYRetryFailure, - self.client._handle_tty_required, - results, True) - - -class TestConnectHelper(utils.TestCase): - - def test_connect_helper(self): - self.assertIsInstance(ssh.connect("123.456.789.0"), ssh.SSH) - - def test_throws_typeerror_well(self): - self.assertRaises(TypeError, ssh.connect, - ("123.456.789.0",), invalidkey="bad") - - def test_throws_typeerror_well_with_message(self): - try: - ssh.connect("123.456.789.0", invalidkey="bad") - except TypeError as exc: - self.assertEqual("connect() got an unexpected keyword " - "argument 'invalidkey'", str(exc)) - - def test_throws_error_no_host(self): - self.assertRaises(TypeError, ssh.connect) - - -class TestPasswordPrompt(utils.TestCase): - - def setUp(self): - super(TestPasswordPrompt, self).setUp() - ssh.LOG = mock.MagicMock() - self.client = ssh.SSH('123.456.789.0', password='test_password') - self.stdout = mock.MagicMock() - self.stdin = mock.MagicMock() - - def test_channel_closed(self): - """If the channel is closed, there's no prompt.""" - self.stdout.channel.closed = True - self.assertFalse( - self.client._handle_password_prompt(self.stdin, self.stdout)) - - def test_password_prompt_buflen_too_short(self): - """Stdout chan buflen is too short to be a password prompt.""" - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = "a" * (ssh.MIN_PASSWORD_PROMPT_LEN - 1) - self.assertFalse( - self.client._handle_password_prompt(self.stdin, self.stdout)) - - def test_password_prompt_buflen_too_long(self): - """Stdout chan buflen is too long to be a password prompt.""" - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = "a" * (ssh.MAX_PASSWORD_PROMPT_LEN + 1) - self.assertFalse( - self.client._handle_password_prompt(self.stdin, self.stdout)) - - def test_common_password_prompt(self): - """Ensure that a couple commonly seen prompts have success.""" - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = "[sudo] password for user:" - self.stdout.channel.recv.return_value = self.stdout.channel.in_buffer - self.assertTrue( - self.client._handle_password_prompt(self.stdin, self.stdout)) - self.stdout.channel.in_buffer = "Password:" - self.stdout.channel.recv.return_value = self.stdout.channel.in_buffer - self.assertTrue( - self.client._handle_password_prompt(self.stdin, self.stdout)) - - def test_password_prompt_other_prompt(self): - """Pass buflen check, fail on substring check.""" - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = "Welcome to :" - self.stdout.channel.recv.return_value = self.stdout.channel.in_buffer - self.assertFalse( - self.client._handle_password_prompt(self.stdin, self.stdout)) - - def test_logging_encountered_prompt(self): - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = "[sudo] password for user:" - self.stdout.channel.recv.return_value = self.stdout.channel.in_buffer - self.client._handle_password_prompt(self.stdin, self.stdout) - ssh.LOG.warning.assert_called_with( - '%s@%s encountered prompt! of length [%s] {%s}', "root", - '123.456.789.0', 25, '[sudo] password for user:') - - def test_logging_nearly_false_positive(self): - """Assert that a close-call on a false-positive logs a warning.""" - other_prompt = "Welcome to :" - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = other_prompt - self.stdout.channel.recv.return_value = self.stdout.channel.in_buffer - self.client._handle_password_prompt(self.stdin, self.stdout) - ssh.LOG.warning.assert_called_with( - 'Nearly a False-Positive on password prompt detection. [%s] {%s}', - 22, other_prompt) - - def test_password_given_to_prompt(self): - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = "[sudo] password for user:" - self.stdout.channel.recv.return_value = self.stdout.channel.in_buffer - self.client._handle_password_prompt(self.stdin, self.stdout) - self.stdin.write.assert_called_with(self.client.password + '\n') - - def test_password_given_returns_true(self): - self.stdout.channel.closed = False - self.stdout.channel.in_buffer = "[sudo] password for user:" - self.stdout.channel.recv.return_value = self.stdout.channel.in_buffer - self.assertTrue( - self.client._handle_password_prompt(self.stdin, self.stdout)) - - -class SSHTestBase(utils.TestCase): - - """Base class with a set of test ssh keys and paramiko.SSHClient mock.""" - - def setUp(self): - super(SSHTestBase, self).setUp() - - self.invalidkey = """ - -----BEGIN RSA PRIVATE KEY----- - MJK7hkKYHUNJKDHNF)980BN456bjnkl_0Hj08,l$IRJSDLKjhkl/jFJVSLx2doRZ - -----END RSA PRIVATE KEY----- - """ - - self.ecdsakey = """ - -----BEGIN EC PRIVATE KEY----- - MHcCAQEEIIiZdMfDf+lScOkujN1+zAKDJ9PQRquCVZoXfS+6hDlToAoGCCqGSM49 - AwEHoUQDQgAE/qUj+vxnhIrkTR/ayYx9ZC/9JanJGyXkOe3Oe6WT/FJ9vBbfThTF - U9+i43I3TONq+nWbhFKBj8XR4NKReaYeBw== - -----END EC PRIVATE KEY----- - """ - - self.rsakey = """ - -----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEAwbbaH5m0yLIVAi1i4aJ5uKprPM93x6b/KkH5N4QmZoXGOFId - v0G64Sanz1VZkCWXiyivgkT6/y0+M0Ok8UK24UO6YNBSFGKboan/OMNETTIqXzmV - liVYkQTf2zrBPWofjeDnzMndy7AD5iylJ6cNAksFM+sLt0MQcOeCmbOX8E6+AGZr - JLj8orJgGJKU9jN5tnMlgtDP9BVrrbi7wX0kqb42OMtM6AuMUBDtAM2QSpTJa0JL - mFOLfe6PYOLdQaJsnaoV+Wu4eBdY91h8COmhOKZv5VMYalOSDQnsKgngDW9iOoFs - Uou7W8Wk3FXusbDwAvakWKmQtDF8SIgMLqygTwIDAQABAoIBAQCe5FkuKmm7ZTcO - PiQpZ5fn/QFRM+vP/A64nrzI6MCGv5vDfreftU6Qd6CV1DBOqEcRgiHT/LjUrkui - yQ12R36yb1dlKfrpdaiqhkIuURypJUjUKuuj6KYo7ZKgxCTVN0MCoUQBGmOvO4U3 - O8+MIt3sz5RI7bcCbyQBOCRL5p/uH3soWoG+6u2W17M4otLT0xJGX5eU0AoCYfOi - Vd9Ot3j687k6KtZajy2hZIccuGNRwFeKSIAN9U7FEy4fgxkIMrc/wqArKmZLNui1 - SkVP3UHlbGVAI5ZDLzdcyxXPRWz1FBtJYiITtQCVKTv5LFCxFjlIWML2qJMB2GTW - 0+t1WhEhAoGBAOFdh14qn0i5v7DztpkS665vQ9F8n7RN0b17yK5wNmfhd4gYK/ym - hCPUk0+JfPNQuhhhzoDXWICiCHRqNVT0ZzkyY0E2aTYLYxbeKkiCOccqJXxtxiI+ - 6KneRMV3mKaJXJLz8G0YepB2Qhv4JkNsR1yiA5EqIs0Cr9Jafg9tHQsrAoGBANwL - 5lYjNHu51WVdjv2Db4oV26fRqAloc0//CBCl9IESM7m9A7zPTboMMijSqEuz3qXJ - Fd5++B/b1Rkt4EunJNcE+XRJ9cI7MKE1kYKz6oiSN4X4eHQSDmlpS9DBcAEjTJ8r - c+5DsPMSkz6qMxbG+FZB1SvVflFZe9dO8Ba7oR1tAoGAa+97keIf/5jW8k0HOzEQ - p66qcH6bjqNmvLW4W7Nqmz4lHY1WI98skmyRURqsOWyEdIEDgjmhLZptKjRj7phP - h9lWKDmDEltJzf4BilC0k2rgIUQCDQzMKe9GSL0K41gOemNS1y1OJjo9V1/2E3yc - gQUnaDMiD8Ylpz2n+oNr0ZkCgYBqDK4g+2yS6JgI91MvqQW7lhc7xRZoGlfgyPe5 - FlJFVmFpdcf0WjCKptARzpzfhzuZyNTqW2T37bnBHdQIgfCGVFZpDjAMQPyJ5UhQ - pqc01Ms/nOVogz9A3Ed2v5NcaQfHemiv/x2ruFsQi3R92LzczXOQYZ80U50Uwm2B - d0IJ7QKBgD39jFiz7U4XEK/knRWUBUNq8QSGF5UuzO404z/+6J2KlFeNiDe+aH0c - cdi+/PhkDkMXfW6eQdvgFYs277uss4M+4F8fWb2KVvPTuZXmTf6qntFoZNuL1oIv - kn+fI2noF0ET7ktofoPEeD2/ya0B9/XecUqDJcVofoVO2pxMn12A - -----END RSA PRIVATE KEY----- - """ - - self.dsakey = """ - -----BEGIN DSA PRIVATE KEY----- - MIIBuwIBAAKBgQC+WvLRuPNDPVfZwKYqJYuD6XXjrUU4KIdLWmRO9qOtq0UR1kOQ - /4rhjgb2TyujW6RzPnqPc9eUv84Z3gKawAdZv5/vKbp6tpMn86Y42r0Ohy63DEgM - XyBfWxbZm0RBmLy3bCUefMOBngnODIhrTt2o+ip5ve5JMctDvjkWBVnZiQIVAMlh - 6gd7IC68FwynC4f/p8+zpx9pAoGARjTQeKxBBDDfxySYDN0maXHMR21RF/gklecO - x6sH1MEDtOupQk0/uIPvolH0Jh+PK+NAv0GBZ96PDrF5z0S6MyQ5eHWGtwW4NFqk - ZGHTriy+8qc4OhtyS3dpXQu40Ad2o1ap1v806RwM8iw1OfBa94h/vreedO0ij2Fe - 7aKEci4CgYAITw+ySCskHakn1GTG952MKxlMo7Mx++dYnCoFxsMwXFlwIrpzyhhC - Qk11sEgcAOZ2HiRVhwaz4BivNV5iuwUeIeKJc12W4+FU+Lh533hFOcSAYbBr1Crl - e+YpaOHRjLel0Nb5Cil4qEQaWQDmWvQb958IQQgzC9NhnR7NRNkfrgIVAKfMMZKz - 57plimt3W9YoDAATyr6i - -----END DSA PRIVATE KEY----- - """ - - patcher = mock.patch.object(paramiko.SSHClient, "connect") - self.mock_connect = patcher.start() - self.addCleanup(patcher.stop) - - patcher = mock.patch.object(paramiko.SSHClient, - "load_system_host_keys") - self.mock_load = patcher.start() - self.addCleanup(patcher.stop) - - -class TestSSHKeyConversion(SSHTestBase): - - """Test ssh key conversion reoutines.""" - - def test_invalid_key_raises_sshexception(self): - self.assertRaises( - paramiko.SSHException, ssh.make_pkey, self.invalidkey) - - def test_valid_ecdsa_returns_pkey_obj(self): - self.assertIsInstance(ssh.make_pkey(self.ecdsakey), paramiko.PKey) - - def test_valid_rsa_returns_pkey_obj(self): - self.assertIsInstance(ssh.make_pkey(self.rsakey), paramiko.PKey) - - def test_valid_ds_returns_pkey_obj(self): - self.assertIsInstance(ssh.make_pkey(self.dsakey), paramiko.PKey) - - @mock.patch.object(ssh, 'LOG') - def test_valid_ecdsa_logs_key_class(self, mock_LOG): - ssh.make_pkey(self.ecdsakey) - mock_LOG.info.assert_called_with( - 'Valid SSH Key provided (%s)', 'ECDSAKey') - - @mock.patch.object(ssh, 'LOG') - def test_valid_rsa_logs_key_class(self, mock_LOG): - ssh.make_pkey(self.rsakey) - mock_LOG.info.assert_called_with( - 'Valid SSH Key provided (%s)', 'RSAKey') - - @mock.patch.object(ssh, 'LOG') - def test_valid_dsa_logs_key_class(self, mock_LOG): - ssh.make_pkey(self.dsakey) - mock_LOG.info.assert_called_with( - 'Valid SSH Key provided (%s)', 'DSSKey') - - -class TestSSHLocalKeys(SSHTestBase): - - def setUp(self): - super(TestSSHLocalKeys, self).setUp() - self.host = '123.456.789.0' - self.client = ssh.SSH(self.host, username='test-user') - - @mock.patch.object(ssh, 'LOG') - def test_connect_host_keys(self, mock_LOG): - self.client.connect_with_host_keys() - self.mock_connect.assert_called_once_with( - '123.456.789.0', username='test-user', pkey=None, timeout=20, - sock=None, port=22, look_for_keys=True, allow_agent=False) - mock_LOG.debug.assert_called_with( - "Trying to connect with local host keys") - - -class TestSSHPassword(SSHTestBase): - - def setUp(self): - super(TestSSHPassword, self).setUp() - self.host = '123.456.789.0' - self.client = ssh.SSH(self.host, username='test-user') - - @mock.patch.object(ssh, 'LOG') - def test_connect_with_password(self, mock_LOG): - self.client.password = "pxwd" - self.client.connect_with_password() - self.mock_connect.assert_called_once_with( - '123.456.789.0', username='test-user', pkey=None, timeout=20, - sock=None, port=22, allow_agent=False, look_for_keys=False, - password="pxwd") - mock_LOG.debug.assert_called_with("Trying to connect with password") - - def test_connect_with_no_password(self): - self.client.password = None - self.assertRaises(paramiko.PasswordRequiredException, - self.client.connect_with_password) - - @mock.patch.object(ssh, 'getpass') - def test_connect_with_no_password_interactive(self, mock_getpass): - self.client.password = None - self.client.interactive = True - mock_getpass.getpass.return_value = "in-pxwd" - self.client.connect_with_password() - self.mock_connect.assert_called_once_with( - '123.456.789.0', username='test-user', pkey=None, timeout=20, - sock=None, port=22, allow_agent=False, look_for_keys=False, - password="in-pxwd") - - @mock.patch.object(ssh, 'LOG') - @mock.patch.object(ssh, 'getpass') - def test_connect_with_interactive_cancel(self, mock_getpass, mock_LOG): - self.client.password = None - self.client.interactive = True - mock_getpass.getpass.side_effect = KeyboardInterrupt() - self.assertRaises(paramiko.PasswordRequiredException, - self.client.connect_with_password) - mock_LOG.debug.assert_any_call("User cancelled at password prompt") - mock_LOG.debug.assert_any_call("Prompting for password " - "(interactive=%s)", True) - - -class TestSSHKeyFile(SSHTestBase): - - def setUp(self): - super(TestSSHKeyFile, self).setUp() - self.host = '123.456.789.0' - self.client = ssh.SSH(self.host, username='test-user') - - @mock.patch.object(ssh, 'LOG') - def test_key_filename(self, mock_LOG): - self.client.key_filename = "~/not/a/real/path" - expanded_path = os.path.expanduser(self.client.key_filename) - self.client.connect_with_key_file() - self.mock_connect.assert_called_once_with( - '123.456.789.0', username='test-user', pkey=None, timeout=20, - look_for_keys=False, allow_agent=False, sock=None, port=22, - key_filename=expanded_path) - mock_LOG.debug.assert_any_call("Trying to connect with key file") - - def test_bad_key_filename(self): - self.client.key_filename = None - self.assertRaises(paramiko.AuthenticationException, - self.client.connect_with_key_file) - - -class TestSSHKeyString(SSHTestBase): - - def setUp(self): - super(TestSSHKeyString, self).setUp() - self.host = '123.456.789.0' - self.client = ssh.SSH(self.host, username='test-user') - - def test_connect_invalid_private_key_string(self): - self.client.private_key = self.invalidkey - self.assertRaises(paramiko.SSHException, self.client.connect_with_key) - - def test_connect_valid_private_key_string(self): - validkeys = [self.rsakey, self.dsakey, self.ecdsakey] - for key in validkeys: - self.client.private_key = key - self.client.connect_with_key() - pkey_kwarg_value = (paramiko.SSHClient. - connect.call_args[1]['pkey']) - self.assertIsInstance(pkey_kwarg_value, paramiko.PKey) - self.mock_connect.assert_called_with( - '123.456.789.0', username='test-user', allow_agent=False, - look_for_keys=False, sock=None, port=22, timeout=20, - pkey=pkey_kwarg_value) - - def test_connect_no_private_key_string(self): - self.client.private_key = None - self.assertRaises(paramiko.AuthenticationException, - self.client.connect_with_key) - - -class TestSSHPrivateConnect(SSHTestBase): - - """Test _connect call.""" - - def setUp(self): - super(TestSSHPrivateConnect, self).setUp() - self.host = '123.456.789.0' - self.client = ssh.SSH(self.host, username='test-user') - - @mock.patch.object(ssh, 'LOG') - def test_connect_no_auth_attrs(self, mock_LOG): - """Test connect call without auth attributes.""" - self.client._connect() - self.mock_connect.assert_called_once_with( - '123.456.789.0', username='test-user', pkey=None, sock=None, - port=22, timeout=20) - - @mock.patch.object(ssh, 'LOG') - def test_connect_with_password(self, mock_LOG): - self.client._connect(password='test-password') - self.mock_connect.assert_called_once_with( - '123.456.789.0', username='test-user', timeout=20, pkey=None, - password='test-password', sock=None, port=22) - - @mock.patch.object(ssh, 'LOG') - def test_use_password_on_exc_negative(self, mock_LOG): - """Do this without self.password. """ - self.mock_connect.side_effect = ( - paramiko.PasswordRequiredException) - self.assertRaises(paramiko.PasswordRequiredException, - self.client._connect) - - @mock.patch.object(ssh, 'LOG') - def test_default_user_is_root(self, mock_LOG): - self.client = ssh.SSH('123.456.789.0') - self.client._connect() - default = self.mock_connect.call_args[1]['username'] - self.assertEqual(default, 'root') - - @mock.patch.object(ssh, 'LOG') - def test_missing_host_key_policy(self, mock_LOG): - client = ssh.connect( - "123.456.789.0", options={'StrictHostKeyChecking': 'no'}) - client._connect() - self.assertIsInstance(client._policy, ssh.AcceptMissingHostKey) - - @mock.patch.object(ssh, 'LOG') - def test_adds_missing_host_key(self, mock_LOG): - client = ssh.connect( - "123.456.789.0", options={'StrictHostKeyChecking': 'no'}) - client._connect() - pkey = ssh.make_pkey(self.rsakey) - client._policy.missing_host_key( - client, - "123.456.789.0", - pkey) - expected = {'123.456.789.0': { - 'ssh-rsa': pkey}} - self.assertEqual(expected, client._host_keys) - - -class TestSSHConnect(SSHTestBase): - - def setUp(self): - super(TestSSHConnect, self).setUp() - self.host = '123.456.789.0' - self.client = ssh.SSH(self.host, username='test-user') - - @mock.patch.object(ssh, 'LOG') - def test_logging_when_badhostkey(self, mock_LOG): - """Test when raising BadHostKeyException.""" - self.client.password = "foo" - self.client.private_key = self.rsakey - exc = paramiko.BadHostKeyException(None, None, None) - self.mock_connect.side_effect = exc - try: - self.client.connect() - except paramiko.BadHostKeyException: - pass - mock_LOG.info.assert_called_with( - "ssh://%s@%s:%d failed: %s. " - "You might have a bad key entry on your server, " - "but this is a security issue and won't be handled " - "automatically. To fix this you can remove the " - "host entry for this host from the /.ssh/known_hosts file", - 'test-user', '123.456.789.0', 22, exc) - - @mock.patch.object(ssh, 'LOG') - def test_logging_when_reraising_other_exc(self, mock_LOG): - self.client.password = "foo" - exc = paramiko.SSHException() - self.mock_connect.side_effect = exc - self.assertRaises(paramiko.SSHException, self.client.connect) - mock_LOG.info.assert_any_call( - 'ssh://%s@%s:%d failed. %s', - 'test-user', '123.456.789.0', 22, exc) - - def test_reraising_bad_host_key_exc(self): - self.client.password = "foo" - self.client.private_key = self.rsakey - exc = paramiko.BadHostKeyException(None, None, None) - self.mock_connect.side_effect = exc - self.assertRaises(paramiko.BadHostKeyException, - self.client.connect) - - @mock.patch.object(ssh, 'LOG') - def test_logging_use_password_on_exc_positive(self, mock_LOG): - self.client.password = 'test-password' - self.mock_connect.side_effect = paramiko.PasswordRequiredException - self.assertRaises(paramiko.PasswordRequiredException, - self.client.connect) - mock_LOG.debug.assert_any_call('Trying to connect with password') - - def test_connect_with_key(self): - self.client.key_filename = '/some/path' - self.client.connect() - self.mock_connect.assert_called_with( - '123.456.789.0', username='test-user', pkey=None, - allow_agent=False, key_filename='/some/path', sock=None, - look_for_keys=False, timeout=20, port=22) - - -class TestTestConnection(SSHTestBase): - - def setUp(self): - super(TestTestConnection, self).setUp() - self.host = '123.456.789.0' - self.client = ssh.SSH(self.host, username='test-user') - - def test_test_connection(self): - self.assertTrue(self.client.test_connection()) - - @mock.patch.object(ssh.SSH, "connect_with_host_keys") - def test_test_connection_fail_invalid_key(self, mock_keys): - mock_keys.side_effect = Exception() - self.client.private_key = self.invalidkey - self.assertFalse(self.client.test_connection()) - - def test_test_connection_valid_key(self): - self.client.private_key = self.dsakey - self.assertTrue(self.client.test_connection()) - - def test_test_connection_fail_other(self): - self.mock_connect.side_effect = Exception - self.assertFalse(self.client.test_connection()) - - @mock.patch.object(ssh, 'LOG') - def test_test_connection_logging(self, mock_LOG): - self.client.test_connection() - mock_LOG.debug.assert_any_call( - "Trying to connect with local host keys") - mock_LOG.debug.assert_any_call( - 'ssh://%s@%s:%d is up.', 'test-user', self.host, 22) - - -class TestRemoteExecute(SSHTestBase): - - def setUp(self): - super(TestRemoteExecute, self).setUp() - self.proxy_patcher = mock.patch.object(paramiko, "ProxyCommand") - self.proxy_patcher.start() - self.client = ssh.SSH('123.456.789.0', username='client-user') - self.client._handle_password_prompt = mock.Mock(return_value=False) - - self.mock_chan = mock.MagicMock() - mock_transport = mock.MagicMock() - mock_transport.open_session.return_value = self.mock_chan - self.client.get_transport = mock.MagicMock( - return_value=mock_transport) - - self.mock_chan.exec_command = mock.MagicMock() - self.mock_chan.makefile.side_effect = self.mkfile - self.mock_chan.makefile_stderr.side_effect = ( - lambda x: self.mkfile(x, err=True)) - - self.example_command = 'echo hello' - self.example_output = 'hello' - - def tearDown(self): - self.proxy_patcher.stop() - super(TestRemoteExecute, self).tearDown() - - def mkfile(self, arg, err=False, stdoutput=None): - if arg == 'rb' and not err: - stdout = mock.MagicMock() - stdout.read.return_value = stdoutput or self.example_output - stdout.read.return_value += "\n" - return stdout - if arg == 'wb' and not err: - stdin = mock.MagicMock() - stdin.read.return_value = '' - return stdin - if err is True: - stderr = mock.MagicMock() - stderr.read.return_value = '' - return stderr - - def test_remote_execute_proper_primitive(self): - self.client._handle_tty_required = mock.Mock(return_value=False) - commands = ['echo hello', 'uname -a', 'rev ~/.bash*'] - for cmd in commands: - self.client.remote_execute(cmd) - self.mock_chan.exec_command.assert_called_with(cmd) - - def test_remote_execute_no_exit_code(self): - self.client._handle_tty_required = mock.Mock(return_value=False) - self.mock_chan.recv_exit_status.return_value = 0 - actual_output = self.client.remote_execute(self.example_command) - expected_output = {'stdout': self.example_output, - 'stderr': ''} - self.assertEqual(expected_output, actual_output) - - def test_remote_execute_with_exit_code(self): - self.client._handle_tty_required = mock.Mock(return_value=False) - self.mock_chan.recv_exit_status.return_value = 0 - actual_output = self.client.remote_execute( - self.example_command, with_exit_code=True) - expected_output = {'stdout': self.example_output, - 'stderr': '', - 'exit_code': 0} - self.assertEqual(expected_output, actual_output) - - def test_remote_execute_tty_required(self): - for i, substring in enumerate(ssh.TTY_REQUIRED): - self.mock_chan.makefile.side_effect = lambda x: self.mkfile( - x, stdoutput="xyz" + substring + "zyx") - self.assertRaises( - errors.GetPTYRetryFailure, - self.client.remote_execute, - 'sudo echo_hello') - self.assertEqual(i + 1, self.mock_chan.get_pty.call_count) - - def test_get_platform_info(self): - platinfo = ['Ubuntu', '12.04', 'precise', 'x86_64'] - fields = ['dist', 'version', 'remove', 'arch'] - expected_result = dict(zip(fields, [v.lower() for v in platinfo])) - expected_result.pop('remove') - self.mock_chan.makefile.side_effect = lambda x: self.mkfile( - x, stdoutput=str(expected_result)) - self.assertEqual(expected_result, self.client.platform_info) - - -class TestProxy(SSHTestBase): - - """self.client in this class is instantiated with a proxy.""" - - def setUp(self): - super(TestProxy, self).setUp() - self.gateway = ssh.SSH('gateway.address', username='gateway-user') - - def tearDown(self): - super(TestProxy, self).tearDown() - - def test_test_connection_fail_other(self): - self.client = ssh.SSH( - '123.456.789.0', username='client-user', gateway=self.gateway) - self.mock_connect.side_effect = Exception - self.assertFalse(self.client.test_connection()) - - def test_connect_with_proxy_no_host_raises(self): - gateway = {'this': 'is not a gateway'} - self.assertRaises( - TypeError, - ssh.SSH, ('123.456.789.0',), - username='client-user', gateway=gateway) - - -if __name__ == "__main__": - unittest.main() diff --git a/satori/tests/test_sysinfo_ohai_solo.py b/satori/tests/test_sysinfo_ohai_solo.py deleted file mode 100644 index 7a5d8b3..0000000 --- a/satori/tests/test_sysinfo_ohai_solo.py +++ /dev/null @@ -1,261 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Test Ohai-Solo Plugin.""" - -import unittest - -import mock - -from satori import errors -from satori.sysinfo import ohai_solo -from satori.tests import utils - - -class TestOhaiSolo(utils.TestCase): - - @mock.patch.object(ohai_solo, 'bash') - @mock.patch.object(ohai_solo, 'system_info') - @mock.patch.object(ohai_solo, 'perform_install') - def test_connect_and_run(self, mock_install, mock_sysinfo, mock_bash): - address = "192.0.2.2" - config = { - 'host_key': 'foo', - 'host_username': 'bar', - } - mock_sysinfo.return_value = {} - result = ohai_solo.get_systeminfo(address, config) - self.assertTrue(result is mock_sysinfo.return_value) - - mock_install.assert_called_once_with( - mock_bash.RemoteShell().__enter__.return_value) - - mock_bash.RemoteShell.assert_any_call( - address, username="bar", - private_key="foo", - interactive=False) - mock_sysinfo.assert_called_with( - mock_bash.RemoteShell().__enter__.return_value) - - -class TestOhaiInstall(utils.TestCase): - - def setUp(self): - super(TestOhaiInstall, self).setUp() - self.mock_remotesshclient = mock.MagicMock() - self.mock_remotesshclient.is_windows.return_value = False - - def test_perform_install_fedora(self): - response = {'exit_code': 0, 'stdout': 'installed remote'} - self.mock_remotesshclient.execute.return_value = response - result = ohai_solo.perform_install(self.mock_remotesshclient) - self.assertEqual(result, response) - self.assertEqual(self.mock_remotesshclient.execute.call_count, 3) - self.mock_remotesshclient.execute.assert_has_calls([ - mock.call('wget -N http://readonly.configdiscovery.rackspace.com/install.sh', cwd='/tmp', - escalate=True, allow_many=False), - mock.call('bash install.sh', cwd='/tmp', with_exit_code=True, - escalate=True, allow_many=False), - mock.call('rm install.sh', cwd='/tmp', escalate=True, - allow_many=False)]) - - def test_perform_install_with_install_dir(self): - response = {'exit_code': 0, 'stdout': 'installed remote'} - self.mock_remotesshclient.execute.return_value = response - result = ohai_solo.perform_install(self.mock_remotesshclient, - install_dir='/home/bob') - self.assertEqual(result, response) - self.assertEqual(self.mock_remotesshclient.execute.call_count, 3) - self.mock_remotesshclient.execute.assert_has_calls([ - mock.call('wget -N http://readonly.configdiscovery.' - 'rackspace.com/install.sh', cwd='/tmp', - escalate=True, allow_many=False), - mock.call('bash install.sh -t -i /home/bob', cwd='/tmp', - with_exit_code=True, escalate=True, allow_many=False), - mock.call('rm install.sh', cwd='/tmp', escalate=True, - allow_many=False)]) - - def test_perform_install_with_install_dir_and_spaces(self): - response = {'exit_code': 0, 'stdout': 'installed remote'} - self.mock_remotesshclient.execute.return_value = response - result = ohai_solo.perform_install(self.mock_remotesshclient, - install_dir='/srv/a diff * dir') - self.assertEqual(result, response) - self.assertEqual(self.mock_remotesshclient.execute.call_count, 3) - self.mock_remotesshclient.execute.assert_has_calls([ - mock.call('wget -N http://readonly.configdiscovery.' - 'rackspace.com/install.sh', cwd='/tmp', - escalate=True, allow_many=False), - mock.call('bash install.sh -t -i \'/srv/a diff * dir\'', - cwd='/tmp', with_exit_code=True, escalate=True, - allow_many=False), - mock.call('rm install.sh', cwd='/tmp', escalate=True, - allow_many=False)]) - - def test_install_linux_remote_failed(self): - response = {'exit_code': 1, 'stdout': "", "stderr": "FAIL"} - self.mock_remotesshclient.execute.return_value = response - self.assertRaises(errors.SystemInfoCommandInstallFailed, - ohai_solo.perform_install, self.mock_remotesshclient) - - -class TestOhaiRemove(utils.TestCase): - - def setUp(self): - super(TestOhaiRemove, self).setUp() - self.mock_remotesshclient = mock.MagicMock() - self.mock_remotesshclient.is_windows.return_value = False - - def test_remove_remote_fedora(self): - self.mock_remotesshclient.is_debian.return_value = False - self.mock_remotesshclient.is_fedora.return_value = True - response = {'exit_code': 0, 'foo': 'bar'} - self.mock_remotesshclient.execute.return_value = response - result = ohai_solo.remove_remote(self.mock_remotesshclient) - self.assertEqual(result, response) - self.mock_remotesshclient.execute.assert_called_once_with( - 'yum -y erase ohai-solo', cwd='/tmp', escalate=True) - - def test_remove_remote_debian(self): - self.mock_remotesshclient.is_debian.return_value = True - self.mock_remotesshclient.is_fedora.return_value = False - response = {'exit_code': 0, 'foo': 'bar'} - self.mock_remotesshclient.execute.return_value = response - result = ohai_solo.remove_remote(self.mock_remotesshclient) - self.assertEqual(result, response) - self.mock_remotesshclient.execute.assert_called_once_with( - 'dpkg --purge ohai-solo', cwd='/tmp', escalate=True) - - def test_remove_remote_unsupported(self): - self.mock_remotesshclient.is_debian.return_value = False - self.mock_remotesshclient.is_fedora.return_value = False - self.assertRaises(errors.UnsupportedPlatform, - ohai_solo.remove_remote, self.mock_remotesshclient) - - def test_remove_remote_with_install_dir(self): - self.mock_remotesshclient.is_debian.return_value = True - self.mock_remotesshclient.is_fedora.return_value = False - response = {'exit_code': 0, 'foo': 'bar'} - self.mock_remotesshclient.execute.return_value = response - result = ohai_solo.remove_remote(self.mock_remotesshclient, - install_dir='/home/srv') - self.assertEqual(result, response) - self.mock_remotesshclient.execute.assert_called_once_with( - 'rm -rf /home/srv/ohai-solo/', cwd='/tmp', escalate=True) - - def test_remove_remote_with_install_dir_and_spaces(self): - self.mock_remotesshclient.is_debian.return_value = True - self.mock_remotesshclient.is_fedora.return_value = False - response = {'exit_code': 0, 'foo': 'bar'} - self.mock_remotesshclient.execute.return_value = response - result = ohai_solo.remove_remote(self.mock_remotesshclient, - install_dir='/srv/a dir') - self.assertEqual(result, response) - self.mock_remotesshclient.execute.assert_called_once_with( - 'rm -rf \'/srv/a dir/ohai-solo/\'', cwd='/tmp', escalate=True) - - -class TestSystemInfo(utils.TestCase): - - def setUp(self): - super(TestSystemInfo, self).setUp() - self.mock_remotesshclient = mock.MagicMock() - self.mock_remotesshclient.is_windows.return_value = False - - def test_system_info(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 0, - 'stdout': "{}", - 'stderr': "" - } - ohai_solo.system_info(self.mock_remotesshclient) - self.mock_remotesshclient.execute.assert_called_with( - "unset GEM_CACHE GEM_HOME GEM_PATH && " - "sudo /opt/ohai-solo/bin/ohai-solo", - escalate=True, allow_many=False) - - def test_system_info_with_install_dir(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 0, - 'stdout': "{}", - 'stderr': "" - } - ohai_solo.system_info(self.mock_remotesshclient, - install_dir='/home/user') - self.mock_remotesshclient.execute.assert_called_with( - "unset GEM_CACHE GEM_HOME GEM_PATH && " - "sudo /home/user/ohai-solo/bin/ohai-solo", - escalate=True, allow_many=False) - - def test_system_info_with_install_dir_with_spaces(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 0, - 'stdout': "{}", - 'stderr': "" - } - ohai_solo.system_info(self.mock_remotesshclient, - install_dir='/sys/omg * " lol/') - self.mock_remotesshclient.execute.assert_called_with( - "unset GEM_CACHE GEM_HOME GEM_PATH && " - 'sudo \'/sys/omg * " lol//ohai-solo/bin/ohai-solo\'', - escalate=True, allow_many=False) - - def test_system_info_with_motd(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 0, - 'stdout': "Hello world\n {}", - 'stderr': "" - } - ohai_solo.system_info(self.mock_remotesshclient) - self.mock_remotesshclient.execute.assert_called_with( - "unset GEM_CACHE GEM_HOME GEM_PATH && " - "sudo /opt/ohai-solo/bin/ohai-solo", - escalate=True, allow_many=False) - - def test_system_info_bad_json(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 0, - 'stdout': "{Not JSON!}", - 'stderr': "" - } - self.assertRaises(errors.SystemInfoNotJson, ohai_solo.system_info, - self.mock_remotesshclient) - - def test_system_info_missing_json(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 0, - 'stdout': "No JSON!", - 'stderr': "" - } - self.assertRaises(errors.SystemInfoMissingJson, ohai_solo.system_info, - self.mock_remotesshclient) - - def test_system_info_command_not_found(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 1, - 'stdout': "", - 'stderr': "ohai-solo command not found" - } - self.assertRaises(errors.SystemInfoCommandMissing, - ohai_solo.system_info, self.mock_remotesshclient) - - def test_system_info_could_not_find(self): - self.mock_remotesshclient.execute.return_value = { - 'exit_code': 1, - 'stdout': "", - 'stderr': "Could not find ohai-solo." - } - self.assertRaises(errors.SystemInfoCommandMissing, - ohai_solo.system_info, self.mock_remotesshclient) - -if __name__ == "__main__": - unittest.main() diff --git a/satori/tests/test_sysinfo_posh_ohai.py b/satori/tests/test_sysinfo_posh_ohai.py deleted file mode 100644 index f3b0d26..0000000 --- a/satori/tests/test_sysinfo_posh_ohai.py +++ /dev/null @@ -1,85 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Test PoSh-Ohai Plugin.""" - -import doctest -import unittest - -import mock - -from satori import errors -from satori.sysinfo import posh_ohai -from satori.tests import utils - - -def load_tests(loader, tests, ignore): - """Include doctests as unit tests.""" - tests.addTests(doctest.DocTestSuite(posh_ohai)) - return tests - - -class TestSystemInfo(utils.TestCase): - - def setUp(self): - super(TestSystemInfo, self).setUp() - self.client = mock.MagicMock() - self.client.is_windows.return_value = True - - def test_system_info(self): - self.client.execute.return_value = "{}" - posh_ohai.system_info(self.client) - self.client.execute.assert_called_with("Import-Module -Name Posh-Ohai;" - "Get-ComputerConfiguration") - - def test_system_info_json(self): - self.client.execute.return_value = '{"foo": 123}' - self.assertEqual(posh_ohai.system_info(self.client), {'foo': 123}) - - def test_system_info_json_with_motd(self): - self.client.execute.return_value = "Hello world\n {}" - self.assertEqual(posh_ohai.system_info(self.client), {}) - - def test_system_info_xml(self): - valid_xml = '''" - " - platform_family - Windows - - ''' - self.client.execute.return_value = valid_xml - self.assertEqual(posh_ohai.system_info(self.client), - {'platform_family': 'Windows'}) - - def test_system_info_bad_json(self): - self.client.execute.return_value = "{Not JSON!}" - self.assertRaises(errors.SystemInfoInvalid, - posh_ohai.system_info, self.client) - - def test_system_info_bad_xml(self): - self.client.execute.return_value = "" - self.assertRaises(errors.SystemInfoInvalid, - posh_ohai.system_info, self.client) - - def test_system_info_bad_xml(self): - self.client.execute.return_value = "bad structure" - self.assertRaises(errors.SystemInfoInvalid, - posh_ohai.system_info, self.client) - - def test_system_info_invalid(self): - self.client.execute.return_value = "No JSON and not XML!" - self.assertRaises(errors.SystemInfoInvalid, - posh_ohai.system_info, self.client) - - -if __name__ == "__main__": - unittest.main() diff --git a/satori/tests/test_utils.py b/satori/tests/test_utils.py deleted file mode 100644 index cd3465a..0000000 --- a/satori/tests/test_utils.py +++ /dev/null @@ -1,131 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Tests for utils module.""" - -import datetime -import time -import unittest - -import mock - -from satori import utils - - -class SomeTZ(datetime.tzinfo): - - """A random timezone.""" - - def utcoffset(self, dt): - return datetime.timedelta(minutes=45) - - def tzname(self, dt): - return "STZ" - - def dst(self, dt): - return datetime.timedelta(0) - - -class TestTimeUtils(unittest.TestCase): - - """Test time formatting functions.""" - - def test_get_formatted_time_string(self): - some_time = time.gmtime(0) - with mock.patch.object(utils.time, 'gmtime') as mock_gmt: - mock_gmt.return_value = some_time - result = utils.get_time_string() - self.assertEqual(result, "1970-01-01 00:00:00 +0000") - - def test_get_formatted_time_string_time_struct(self): - result = utils.get_time_string(time_obj=time.gmtime(0)) - self.assertEqual(result, "1970-01-01 00:00:00 +0000") - - def test_get_formatted_time_string_datetime(self): - result = utils.get_time_string( - time_obj=datetime.datetime(1970, 2, 1, 1, 2, 3, 0)) - self.assertEqual(result, "1970-02-01 01:02:03 +0000") - - def test_get_formatted_time_string_datetime_tz(self): - result = utils.get_time_string( - time_obj=datetime.datetime(1970, 2, 1, 1, 2, 3, 0, SomeTZ())) - self.assertEqual(result, "1970-02-01 01:47:03 +0000") - - def test_parse_time_string(self): - result = utils.parse_time_string("1970-02-01 01:02:03 +0000") - self.assertEqual(result, datetime.datetime(1970, 2, 1, 1, 2, 3, 0)) - - def test_parse_time_string_with_tz(self): - result = utils.parse_time_string("1970-02-01 01:02:03 +1000") - self.assertEqual(result, datetime.datetime(1970, 2, 1, 11, 2, 3, 0)) - - -class TestGetSource(unittest.TestCase): - - def setUp(self): - self.function_signature = "def get_my_source_oneline_docstring(self):" - self.function_oneline_docstring = '"""A beautiful docstring."""' - self.function_multiline_docstring = ('"""A beautiful docstring.\n\n' - 'Is a terrible thing to ' - 'waste.\n"""') - self.function_body = ['the_problem = "not the problem"', - 'return the_problem'] - - def get_my_source_oneline_docstring(self): - """A beautiful docstring.""" - the_problem = "not the problem" - return the_problem - - def get_my_source_multiline_docstring(self): - """A beautiful docstring. - - Is a terrible thing to waste. - """ - the_problem = "not the problem" - return the_problem - - def test_get_source(self): - nab = utils.get_source_body(self.get_my_source_oneline_docstring) - self.assertEqual("\n".join(self.function_body), nab) - - def test_get_source_with_docstring(self): - nab = utils.get_source_body(self.get_my_source_oneline_docstring, - with_docstring=True) - copy = self.function_oneline_docstring + "\n" + "\n".join( - self.function_body) - self.assertEqual(copy, nab) - - def test_get_source_with_multiline_docstring(self): - nab = utils.get_source_body(self.get_my_source_multiline_docstring, - with_docstring=True) - copy = (self.function_multiline_docstring + "\n" + "\n".join( - self.function_body)) - self.assertEqual(copy, nab) - - def test_get_definition(self): - nab = utils.get_source_definition( - self.get_my_source_oneline_docstring) - copy = "%s\n \n %s" % (self.function_signature, - "\n ".join(self.function_body)) - self.assertEqual(copy, nab) - - def test_get_definition_with_docstring(self): - nab = utils.get_source_definition( - self.get_my_source_oneline_docstring, with_docstring=True) - copy = "%s\n %s\n %s" % (self.function_signature, - self.function_oneline_docstring, - "\n ".join(self.function_body)) - self.assertEqual(copy, nab) - - -if __name__ == '__main__': - unittest.main() diff --git a/satori/tests/utils.py b/satori/tests/utils.py deleted file mode 100644 index 2d7e544..0000000 --- a/satori/tests/utils.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2012-2013 OpenStack Foundation -# Copyright 2013 Nebula Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -import os -import sys - -import fixtures -import testtools - - -class TestCase(testtools.TestCase): - def setUp(self): - testtools.TestCase.setUp(self) - - if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or - os.environ.get("OS_STDOUT_CAPTURE") == "1"): - stdout = self.useFixture(fixtures.StringStream("stdout")).stream - self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout)) - - if (os.environ.get("OS_STDERR_CAPTURE") == "True" or - os.environ.get("OS_STDERR_CAPTURE") == "1"): - stderr = self.useFixture(fixtures.StringStream("stderr")).stream - self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) - - # 2.6 doesn't have the assert dict equals so make sure that it exists - if tuple(sys.version_info)[0:2] < (2, 7): - - def assertIsInstance(self, obj, cls, msg=None): - """Same as self.assertTrue(isinstance(obj, cls)), with a nicer - default message - """ - if not isinstance(obj, cls): - standardMsg = '%s is not an instance of %r' % (obj, cls) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertDictEqual(self, d1, d2, msg=None): - # Simple version taken from 2.7 - self.assertIsInstance(d1, dict, - 'First argument is not a dictionary') - self.assertIsInstance(d2, dict, - 'Second argument is not a dictionary') - if d1 != d2: - if msg: - self.fail(msg) - else: - standardMsg = '%r != %r' % (d1, d2) - self.fail(standardMsg) diff --git a/satori/tunnel.py b/satori/tunnel.py deleted file mode 100644 index 43041b0..0000000 --- a/satori/tunnel.py +++ /dev/null @@ -1,167 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -"""SSH tunneling module. - -Set up a forward tunnel across an SSH server, using paramiko. A local port -(given with -p) is forwarded across an SSH session to an address:port from -the SSH server. This is similar to the openssh -L option. -""" -try: - import eventlet - eventlet.monkey_patch() - from eventlet.green import threading - from eventlet.green import time -except ImportError: - import threading - import time - pass - -import logging -import select -import socket -try: - import SocketServer -except ImportError: - import socketserver as SocketServer - -import paramiko - -LOG = logging.getLogger(__name__) - - -class TunnelServer(SocketServer.ThreadingTCPServer): - - """Serve on a local ephemeral port. - - Clients will connect to that port/server. - """ - - daemon_threads = True - allow_reuse_address = True - - -class TunnelHandler(SocketServer.BaseRequestHandler): - - """Handle forwarding of packets.""" - - def handle(self): - """Do all the work required to service a request. - - The request is available as self.request, the client address as - self.client_address, and the server instance as self.server, in - case it needs to access per-server information. - - This implementation will forward packets. - """ - try: - chan = self.ssh_transport.open_channel('direct-tcpip', - self.target_address, - self.request.getpeername()) - except Exception as exc: - LOG.error('Incoming request to %s:%s failed', - self.target_address[0], - self.target_address[1], - exc_info=exc) - return - if chan is None: - LOG.error('Incoming request to %s:%s was rejected ' - 'by the SSH server.', - self.target_address[0], - self.target_address[1]) - return - - while True: - r, w, x = select.select([self.request, chan], [], []) - if self.request in r: - data = self.request.recv(1024) - if len(data) == 0: - break - chan.send(data) - if chan in r: - data = chan.recv(1024) - if len(data) == 0: - break - self.request.send(data) - - try: - peername = None - peername = str(self.request.getpeername()) - except socket.error as exc: - LOG.warning("Couldn't fetch peername.", exc_info=exc) - chan.close() - self.request.close() - LOG.info("Tunnel closed from '%s'", peername or 'unnamed peer') - - -class Tunnel(object): # pylint: disable=R0902 - - """Create a TCP server which will use TunnelHandler.""" - - def __init__(self, target_host, target_port, - sshclient, tunnel_host='localhost', - tunnel_port=0): - """Constructor.""" - if not isinstance(sshclient, paramiko.SSHClient): - raise TypeError("'sshclient' must be an instance of " - "paramiko.SSHClient.") - - self.target_host = target_host - self.target_port = target_port - self.target_address = (target_host, target_port) - self.address = (tunnel_host, tunnel_port) - - self._tunnel = None - self._tunnel_thread = None - self.sshclient = sshclient - self._ssh_transport = self.get_sshclient_transport( - self.sshclient) - - TunnelHandler.target_address = self.target_address - TunnelHandler.ssh_transport = self._ssh_transport - - self._tunnel = TunnelServer(self.address, TunnelHandler) - # reset attribute to the port it has actually been set to - self.address = self._tunnel.server_address - tunnel_host, self.tunnel_port = self.address - - def get_sshclient_transport(self, sshclient): - """Get the sshclient's transport. - - Connect the sshclient, that has been passed in and return its - transport. - """ - sshclient.connect() - return sshclient.get_transport() - - def serve_forever(self, async=True): - """Serve the tunnel forever. - - if async is True, this will be done in a background thread - """ - if not async: - self._tunnel.serve_forever() - else: - self._tunnel_thread = threading.Thread( - target=self._tunnel.serve_forever) - self._tunnel_thread.start() - # cooperative yield - time.sleep(0) - - def shutdown(self): - """Stop serving the tunnel. - - Also close the socket. - """ - self._tunnel.shutdown() - self._tunnel.socket.close() diff --git a/satori/utils.py b/satori/utils.py deleted file mode 100644 index 42f9a0c..0000000 --- a/satori/utils.py +++ /dev/null @@ -1,221 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""General utilities. - -- Class and module import/export -- Time utilities (we standardize on UTC) -""" - -import datetime -import inspect -import logging -import platform -import socket -import sys -import time - -import iso8601 - -LOG = logging.getLogger(__name__) -STRING_FORMAT = "%Y-%m-%d %H:%M:%S +0000" - - -def import_class(import_str): - """Return a class from a string including module and class.""" - mod_str, _, class_str = import_str.rpartition('.') - try: - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - except (ImportError, ValueError, AttributeError) as exc: - LOG.debug('Inner Exception: %s', exc) - raise - - -def import_object(import_str, *args, **kw): - """Return an object including a module or module and class.""" - try: - __import__(import_str) - return sys.modules[import_str] - except ImportError: - cls = import_class(import_str) - return cls(*args, **kw) - - -def get_time_string(time_obj=None): - """The canonical time string format (in UTC). - - :param time_obj: an optional datetime.datetime or timestruct (defaults to - gm_time) - - Note: Changing this function will change all times that this project uses - in the returned data. - """ - if isinstance(time_obj, datetime.datetime): - if time_obj.tzinfo: - offset = time_obj.tzinfo.utcoffset(time_obj) - utc_dt = time_obj + offset - return datetime.datetime.strftime(utc_dt, STRING_FORMAT) - return datetime.datetime.strftime(time_obj, STRING_FORMAT) - elif isinstance(time_obj, time.struct_time): - return time.strftime(STRING_FORMAT, time_obj) - elif time_obj is not None: - raise TypeError("get_time_string takes only a time_struct, none, or a " - "datetime. It was given a %s" % type(time_obj)) - return time.strftime(STRING_FORMAT, time.gmtime()) - - -def parse_time_string(time_string): - """Return naive datetime object from string in standard time format.""" - parsed = time_string.replace(" +", "+").replace(" -", "-") - dt_with_tz = iso8601.parse_date(parsed) - offset = dt_with_tz.tzinfo.utcoffset(dt_with_tz) - result = dt_with_tz + offset - return result.replace(tzinfo=None) - - -def is_valid_ipv4_address(address): - """Check if the address supplied is a valid IPv4 address.""" - try: - socket.inet_pton(socket.AF_INET, address) - except AttributeError: # no inet_pton here, sorry - try: - socket.inet_aton(address) - except socket.error: - return False - return address.count('.') == 3 - except socket.error: # not a valid address - return False - return True - - -def is_valid_ipv6_address(address): - """Check if the address supplied is a valid IPv6 address.""" - try: - socket.inet_pton(socket.AF_INET6, address) - except socket.error: # not a valid address - return False - return True - - -def is_valid_ip_address(address): - """Check if the address supplied is a valid IP address.""" - return is_valid_ipv4_address(address) or is_valid_ipv6_address(address) - - -def get_local_ips(): - """Return local ipaddress(es).""" - # pylint: disable=W0703 - list1 = [] - list2 = [] - defaults = ["127.0.0.1", r"fe80::1%lo0"] - - hostname = None - try: - hostname = socket.gethostname() - except Exception as exc: - LOG.debug("Error in gethostbyname_ex: %s", exc) - - try: - _, _, addresses = socket.gethostbyname_ex(hostname) - list1 = [ip for ip in addresses] - except Exception as exc: - LOG.debug("Error in gethostbyname_ex: %s", exc) - - try: - list2 = [info[4][0] for info in socket.getaddrinfo(hostname, None)] - except Exception as exc: - LOG.debug("Error in getaddrinfo: %s", exc) - - return list(set(list1 + list2 + defaults)) - - -def get_platform_info(): - """Return a dictionary with distro, version, and system architecture. - - Requires >= Python 2.4 (2004) - - Supports most Linux distros, Mac OSX, and Windows. - - Example return value on Mac OSX: - - {'arch': '64bit', 'version': '10.8.5', 'dist': 'darwin'} - - """ - pin = list(platform.dist() + (platform.machine(),)) - pinfodict = {'dist': pin[0], 'version': pin[1], 'arch': pin[3]} - if not pinfodict['dist'] or not pinfodict['version']: - pinfodict['dist'] = sys.platform.lower() - pinfodict['arch'] = platform.architecture()[0] - if 'darwin' in pinfodict['dist']: - pinfodict['version'] = platform.mac_ver()[0] - elif pinfodict['dist'].startswith('win'): - pinfodict['version'] = str(platform.platform()) - - return pinfodict - - -def get_source_definition(function, with_docstring=False): - """Get the entire body of a function, including the signature line. - - :param with_docstring: Include docstring in return value. - Default is False. Supports docstrings in - triple double-quotes or triple single-quotes. - """ - thedoc = inspect.getdoc(function) - definition = inspect.cleandoc( - inspect.getsource(function)) - if thedoc and not with_docstring: - definition = definition.replace(thedoc, '') - doublequotes = definition.find('"""') - doublequotes = float("inf") if doublequotes == -1 else doublequotes - singlequotes = definition.find("'''") - singlequotes = float("inf") if singlequotes == -1 else singlequotes - if doublequotes != singlequotes: - triplet = '"""' if doublequotes < singlequotes else "'''" - definition = definition.replace(triplet, '', 2) - while definition.find('\n\n\n') != -1: - definition = definition.replace('\n\n\n', '\n\n') - - definition_copy = [] - for line in definition.split('\n'): - # pylint: disable=W0141 - if not any(map(line.strip().startswith, ("@", "def"))): - line = " " * 4 + line - definition_copy.append(line) - - return "\n".join(definition_copy).strip() - - -def get_source_body(function, with_docstring=False): - """Get the body of a function (i.e. no definition line, unindented). - - :param with_docstring: Include docstring in return value. - Default is False. - """ - lines = get_source_definition( - function, with_docstring=with_docstring).split('\n') - - # Find body - skip decorators and definition - start = 0 - for number, line in enumerate(lines): - # pylint: disable=W0141 - if any(map(line.strip().startswith, ("@", "def"))): - start = number + 1 - - lines = lines[start:] - - # Unindent body - indent = len(lines[0]) - len(lines[0].lstrip()) - for index, line in enumerate(lines): - lines[index] = line[indent:] - return '\n'.join(lines).strip() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4412a31..0000000 --- a/setup.cfg +++ /dev/null @@ -1,43 +0,0 @@ -[metadata] -name = satori -summary = OpenStack Configuration Discovery -description-file = - README.rst -author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = http://wiki.openstack.org/Satori -classifier = - Environment :: OpenStack - Intended Audience :: Information Technology - Intended Audience :: System Administrators - License :: OSI Approved :: Apache Software License - Operating System :: POSIX :: Linux - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 - Programming Language :: Python :: Implementation - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: Implementation :: PyPy - -[files] -packages = - satori - -[entry_points] -console_scripts = - satori = satori.shell:main - - -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 - -[upload_sphinx] -upload-dir = doc/build/html - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 70c2b3f..0000000 --- a/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT -import setuptools - -setuptools.setup( - setup_requires=['pbr'], - pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 9e1a4b6..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -# Includes fixes for handling egg-linked libraries -# and detection of stdlibs in virtualenvs --e git://github.com/samstav/hacking.git@satori#egg=hacking - -coverage>=3.6 -discover -flake8_docstrings>=0.2.0 # patched for py33 -fixtures>=0.3.14 -freezegun -mock>=1.0 -pep8>=1.5.7,<1.6 -pep257>=0.3.2 # patched for py33 -sphinx>=1.2.2 -testrepository>=0.0.17 -testtools>=0.9.32 diff --git a/tools/install_venv.py b/tools/install_venv.py deleted file mode 100644 index 268d581..0000000 --- a/tools/install_venv.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -""" -Installation script for satori's development virtualenv -""" - -import os -import sys - -import install_venv_common as install_venv - - -def print_help(): - help = """ - satori development environment setup is complete. - - satori development uses virtualenv to track and manage Python - dependencies while in development and testing. - - To activate the satori virtualenv for the extent of your current - shell session you can run: - - $ source .venv/bin/activate - - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: - - $ tools/with_venv.sh - - Also, make test will automatically use the virtualenv. - """ - print help - - -def main(argv): - root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - venv = os.path.join(root, ".venv") - pip_requires = os.path.join(root, "requirements.txt") - test_requires = os.path.join(root, "test-requirements.txt") - py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - project = "satori" - install = install_venv.InstallVenv(root, venv, pip_requires, test_requires, - py_version, project) - options = install.parse_args(argv) - install.check_python_version() - install.check_dependencies() - install.create_virtualenv(no_site_packages=options.no_site_packages) - install.install_dependencies() - print_help() - - -if __name__ == "__main__": - main(sys.argv) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py deleted file mode 100644 index 1bab88a..0000000 --- a/tools/install_venv_common.py +++ /dev/null @@ -1,174 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 OpenStack Foundation -# Copyright 2013 IBM Corp. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Provides methods needed by installation script for OpenStack development -virtual environments. - -Since this script is used to bootstrap a virtualenv from the system's Python -environment, it should be kept strictly compatible with Python 2.6. - -Synced in from openstack-common -""" - -from __future__ import print_function - -import optparse -import os -import subprocess -import sys - - -class InstallVenv(object): - - def __init__(self, root, venv, requirements, - test_requirements, py_version, - project): - self.root = root - self.venv = venv - self.requirements = requirements - self.test_requirements = test_requirements - self.py_version = py_version - self.project = project - - def die(self, message, *args): - print(message % args, file=sys.stderr) - sys.exit(1) - - def check_python_version(self): - if sys.version_info < (2, 6): - self.die("Need Python Version >= 2.6") - - def run_command_with_code(self, cmd, redirect_output=True, - check_exit_code=True): - """Runs a command in an out-of-process shell. - - Returns the output of that command. Working directory is self.root. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - def run_command(self, cmd, redirect_output=True, check_exit_code=True): - return self.run_command_with_code(cmd, redirect_output, - check_exit_code)[0] - - def get_distro(self): - if (os.path.exists('/etc/fedora-release') or - os.path.exists('/etc/redhat-release')): - return Fedora( - self.root, self.venv, self.requirements, - self.test_requirements, self.py_version, self.project) - else: - return Distro( - self.root, self.venv, self.requirements, - self.test_requirements, self.py_version, self.project) - - def check_dependencies(self): - self.get_distro().install_virtualenv() - - def create_virtualenv(self, no_site_packages=True): - """Creates the virtual environment and installs PIP. - - Creates the virtual environment and installs PIP only into the - virtual environment. - """ - if not os.path.isdir(self.venv): - print('Creating venv...', end=' ') - if no_site_packages: - self.run_command(['virtualenv', '-q', '--no-site-packages', - self.venv]) - else: - self.run_command(['virtualenv', '-q', self.venv]) - print('done.') - else: - print("venv already exists...") - pass - - def pip_install(self, *args): - self.run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - def install_dependencies(self): - print('Installing dependencies with pip (this can take a while)...') - - # First things first, make sure our venv has the latest pip and - # setuptools and pbr - self.pip_install('pip>=1.4') - self.pip_install('setuptools') - self.pip_install('pbr') - - self.pip_install('-r', self.requirements, '-r', self.test_requirements) - - def parse_args(self, argv): - """Parses command-line arguments.""" - parser = optparse.OptionParser() - parser.add_option('-n', '--no-site-packages', - action='store_true', - help="Do not inherit packages from global Python " - "install") - return parser.parse_args(argv[1:])[0] - - -class Distro(InstallVenv): - - def check_cmd(self, cmd): - return bool(self.run_command(['which', cmd], - check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print('Installing virtualenv via easy_install...', end=' ') - if self.run_command(['easy_install', 'virtualenv']): - print('Succeeded') - return - else: - print('Failed') - - self.die('ERROR: virtualenv not found.\n\n%s development' - ' requires virtualenv, please install it using your' - ' favorite package management tool' % self.project) - - -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux - """ - - def check_pkg(self, pkg): - return self.run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.die("Please install 'python-virtualenv'.") - - super(Fedora, self).install_virtualenv() diff --git a/tools/with_venv.sh b/tools/with_venv.sh deleted file mode 100755 index c8d2940..0000000 --- a/tools/with_venv.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -TOOLS=`dirname $0` -VENV=$TOOLS/../.venv -source $VENV/bin/activate && $@ diff --git a/tox.ini b/tox.ini deleted file mode 100644 index e961cab..0000000 --- a/tox.ini +++ /dev/null @@ -1,52 +0,0 @@ -[tox] -minversion = 1.6 -envlist = py26,py27,py33,py34,pep8,pypy -skipdist = True - -[testenv] -usedevelop = True -install_command = pip install -U {opts} {packages} -setenv = - VIRTUAL_ENV={envdir} -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt -commands = python setup.py testr --testr-args='{posargs}' - -[testenv:py26] -setenv = - VIRTUAL_ENV={envdir} - CFLAGS=-Qunused-arguments - CPPFLAGS=-Qunused-arguments - -[testenv:py33] -deps = -r{toxinidir}/requirements-py3.txt - -r{toxinidir}/test-requirements.txt - -[testenv:py34] -deps = -r{toxinidir}/requirements-py3.txt - -r{toxinidir}/test-requirements.txt - -[testenv:pep8] -commands = flake8 - -[testenv:venv] -commands = {posargs} - -[testenv:cover] -commands = python setup.py test --coverage --testr-args='^(?!.*test.*coverage).*$' - -[testenv:docs] -deps = - -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt - sphinxcontrib-httpdomain -commands = python setup.py build_sphinx - -[tox:jenkins] -downloadcache = ~/cache/pip - -[flake8] -ignore = H102 -show-source = True -exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools,*satori/contrib*,*.ropeproject,*satori/tests*,setup.py -max-complexity = 16