├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── VERSION ├── _config.yml ├── docs ├── Makefile ├── conf.py ├── faq.rst ├── img │ ├── readme-1-simple-usage.png │ ├── readme-2-context-manager.png │ ├── readme-3-ssh-access.png │ ├── readme-4-error-message.png │ └── sultan-logo.png ├── index.rst ├── install-sultan.rst ├── make.bat ├── requirements.txt ├── sultan-examples.rst └── sultan-ssh-examples.rst ├── img └── sultan-logo.png ├── index.rst ├── readthedocs.yml ├── requirements ├── dev.txt └── test.txt ├── setup.py ├── src └── sultan │ ├── __init__.py │ ├── api.py │ ├── config.py │ ├── core.py │ ├── echo │ ├── __init__.py │ └── colorlog │ │ ├── __init__.py │ │ ├── colorlog.py │ │ ├── escape_codes.py │ │ ├── logging.py │ │ └── notests │ │ ├── conftest.py │ │ ├── test_colorlog.py │ │ ├── test_config.ini │ │ ├── test_config.py │ │ ├── test_escape_codes.py │ │ ├── test_example.py │ │ ├── test_exports.py │ │ └── test_logging.py │ ├── exceptions.py │ └── result.py └── test ├── integration ├── __init__.py └── sultan │ ├── __init__.py │ └── test_api.py └── unit ├── __init__.py └── sultan ├── __init__.py ├── test_api.py ├── test_config.py └── test_result.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,virtualenv 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | man/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # IPython Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv/ 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | 98 | ### VirtualEnv ### 99 | # Virtualenv 100 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 101 | .Python 102 | [Bb]in 103 | [Ii]nclude 104 | [Ll]ib 105 | [Ll]ib64 106 | [Ll]ocal 107 | [Ss]cripts 108 | pyvenv.cfg 109 | .venv 110 | pip-selfcheck.json 111 | 112 | ### Sphinx ### 113 | _build 114 | _static 115 | _templates 116 | 117 | ### VSCode ### 118 | .vscode 119 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.3' 5 | - '3.4' 6 | - '3.5' 7 | - '3.6' 8 | install: 9 | - pip install -r requirements/test.txt 10 | - pip install -r docs/requirements.txt 11 | script: 12 | - make tests 13 | - cd docs && make html 14 | - cd .. 15 | deploy: 16 | provider: pypi 17 | distributions: bdist_wheel 18 | user: davydany 19 | password: 20 | secure: nhaBAgq1o53xSMM/PEUa2j2LDxBI67kCTcSLYgIE5wVUe7EU8Sc48Id9Yj+i0fzxWcFgxPCZChfuH0OQo+4y9or5oiEJF0jeOYg2UeipQo2AGIpvV1mk5jAn5/uknczAap0ugpuUyIGmkYJJb5GhrEa12+goucxVPJ7kajGAEe1RL1gE3HAR/YkEu9H+LWix5YbA0lE7B1a/lm9+T4MkvheQeuYPGTRFqioiV5SA1iiGrgvLDuiAt0ZUzoNTWA/jTpiFTXOUAXGbVEGyrscOit2mknXPqs2JFAHl5GJhKDjsDOhgNB5s+M1ChlQCSrSvED3PIsQ4brRBYPTV/TOGziDrDJA5nOAHhTgTv+s/PBQ2Y62vEZfxZrks/IWlQvuZnyqB77tXSKLY87z/ID8KCajzsIoLpzv1E9p4IoZ1RZOkSYMyps7cFJtUMWqfIsyxfDZ4ZaNNdq7gbAkcJsHlP5C5QWRl/zo0X6txfdNvkTcpM365/P9fo5NtkUdsxcYRGlkK0guJpjSzGpwm+yrgtV+4OCEms11lVq1P9I81zFoAYSyF8bzA4Y7CliBkVi2GDRHI1n2tOdEIHxV9ROGyJFW+LgSshbwLarvFNbTEOukEjIrE/FDrfwhxBahhwWXlt/6Q1a3+34qz0DEIIDPui4a9OkYuLTaAiBmgQAqI0IM= 21 | on: 22 | repo: aeroxis/sultan 23 | branch: master 24 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # 0.6.4 (released September, 2017) 2 | - BUG FIXED: Fixed a bug where environment variable was being set to {} instead of None 3 | 4 | # 0.6.3 (released September, 2017) 5 | - Argh, I need to update the damn changelog 6 | - FEATURE: Added support for dashes '-' in commands like 'apt-get' 7 | - FEATURE: Added a Result object that contains STDOUT, STDERR and TRACEBACK. 8 | - FEATURE: Added 'SSHConfig' class for configuring SSH Sessions. Includes config for port and identity file. 9 | - BUG-FIXED: Fixed bug where we were calling 'sudo su - root' instead of just 'sudo' 10 | - DOCS: Added documentation for SSH commands. 11 | - DOCS: Added documentation for getting STDOUT, STDERR and TRACEBACK from a command. 12 | 13 | 14 | 15 | # 0.2.3 (released October 17, 2016) 16 | - Fixed issue where Sultan could not be installed. This is because VERSION file was 17 | not included with the PyPi artifact. Fix was to add `MANIFEST.in` to resolve this. 18 | 19 | # 0.2.2 (released October 17, 2016) 20 | - Minor bug fixes 21 | 22 | # 0.2.1 (released October 17, 2016) 23 | - Fixed bug where too many file descriptors were openned when `run()` was called. 24 | 25 | # 0.2 (released October 15, 2016) 26 | 27 | - Better color schemes. Right now DEBUG is just white and it makes it odd to read. 28 | The color schemes should reflect what the level implies. 29 | - Errors and Stdout should be better formatted with logging. If an exception happens, 30 | we see a traceback, which is useless. The stderr and stdout should be more nicely 31 | formatted and presented to the user when an error occurs. 32 | - We can use `raw_input` to get input via `stdin`, but it would be nice if the 33 | Sultan API also supported it. 34 | - Context Management: 35 | - `sudo=`: Set a command should be run as sudo with Context Management. 36 | - `user=`: Set a user to run command as 37 | - `host=`: Should be able to run a command on a remote host, by setting a 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Aeroxis, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG 2 | include LICENSE 3 | include VERSION 4 | include README.rst 5 | graft docs 6 | graft src 7 | graft requirements 8 | global-exclude __pycache__ 9 | global-exclude *.py[co] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | tests: 2 | nosetests -v --with-coverage --cover-erase --cover-package=sultan 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://raw.githubusercontent.com/aeroxis/sultan/master/docs/img/sultan-logo.png 2 | :alt: sultan logo 3 | :align: right 4 | 5 | **Command and Rule over your Shell** 6 | 7 | .. image:: https://badge.fury.io/py/sultan.svg 8 | :alt: PyPI Version 9 | :target: https://badge.fury.io/py/sultan 10 | 11 | .. image:: https://travis-ci.org/aeroxis/sultan.svg?branch=master 12 | :alt: Travis Build Status 13 | :target: https://travis-ci.org/aeroxis/sultan 14 | 15 | .. image:: http://img.shields.io/:license-mit-blue.svg 16 | :alt: MIT License 17 | :target: http://doge.mit-license.org 18 | 19 | .. image:: https://readthedocs.org/projects/sultan/badge/?version=latest 20 | :alt: Documentation Status 21 | :target: http://sultan.readthedocs.io/en/latest/?badge=latest 22 | 23 | Sultan now supports Python 2.7+ and 3.0+ 24 | 25 | ---- 26 | Note 27 | ---- 28 | Your input is welcome! Please provide your feedback by creating 29 | `issues on Github `_ 30 | 31 | ------- 32 | Install 33 | ------- 34 | 35 | .. code:: bash 36 | 37 | pip install --upgrade sultan 38 | 39 | ------------- 40 | Documentation 41 | ------------- 42 | 43 | .. image:: https://readthedocs.org/projects/sultan/badge/?version=latest 44 | :alt: Documentation Status 45 | :target: http://sultan.readthedocs.io/en/latest/?badge=latest 46 | 47 | Documentation is available on ReadTheDocs: http://sultan.readthedocs.io/en/latest/ 48 | 49 | --------------- 50 | What is Sultan? 51 | --------------- 52 | 53 | Sultan is a Python package for interfacing with command-line utilities, like 54 | `yum`, `apt-get`, or `ls`, in a Pythonic manner. It lets you run command-line 55 | utilities using simple function calls. 56 | 57 | The simplest way to use Sultan is to just call it: 58 | 59 | .. code:: python 60 | 61 | from sultan.api import Sultan 62 | s = Sultan() 63 | s.sudo("yum install -y tree").run() 64 | 65 | **Runs:** 66 | 67 | .. code:: bash 68 | 69 | sudo yum install -y tree; 70 | 71 | ------------ 72 | 73 | The recommended way of using Sultan is to use it in Context Management mode. 74 | Here is how to use Sultan with Context Management: 75 | 76 | .. code:: python 77 | 78 | from sultan.api import Sultan 79 | 80 | with Sultan.load(sudo=True) as s: 81 | s.yum("install -y tree").run() 82 | 83 | **Runs:** 84 | 85 | .. code:: bash 86 | 87 | sudo su - root -c 'yum install -y tree;' 88 | 89 | ------------ 90 | 91 | What if we want to install this command on a remote machine? You can easily 92 | achieve this using context management: 93 | 94 | .. code:: python 95 | 96 | from sultan.api import Sultan 97 | 98 | with Sultan.load(sudo=True, hostname="myserver.com") as sultan: 99 | sultan.yum("install -y tree").run() 100 | 101 | **Runs:** 102 | 103 | .. code:: bash 104 | 105 | ssh root@myserver.com 'sudo su - root -c 'yum install -y tree;'' 106 | 107 | ------------ 108 | 109 | If you enter a wrong command, Sultan will print out details you need to debug and 110 | find the problem quickly. 111 | 112 | Here, the same command was run on a Mac: 113 | 114 | .. code:: python 115 | 116 | from sultan.api import Sultan 117 | 118 | with Sultan.load(sudo=True, hostname="myserver.com") as sultan: 119 | sultan.yum("install -y tree").run() 120 | 121 | 122 | **Yields:** 123 | 124 | .. code:: bash 125 | 126 | [sultan]: sudo su - root -c 'yum install -y tree;' 127 | Password: 128 | [sultan]: --{ STDERR }------------------------------------------------------------------------------------------------------- 129 | [sultan]: | -sh: yum: command not found 130 | [sultan]: ------------------------------------------------------------------------------------------------------------------- 131 | 132 | ------------ 133 | 134 | Want to get started? Simply install Sultan, and start writing your clean code:: 135 | 136 | pip install --upgrade sultan 137 | 138 | If you have more questions, check the docs! http://sultan.readthedocs.io/en/latest/ 139 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.9.1 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: Sultan 2 | description: Command and Rule over your Shell 3 | google_analytics: 4 | show_downloads: true 5 | theme: jekyll-theme-cayman 6 | 7 | gems: 8 | - jekyll-mentions 9 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Sultan.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Sultan.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Sultan" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Sultan" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Sultan documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Oct 13 05:54:04 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | import alabaster 22 | 23 | sys.path.insert(0, os.path.abspath('../src')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.coverage', 37 | 'sphinx.ext.viewcode', 38 | 'sphinx.ext.githubpages', 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = ['.rst'] 49 | 50 | # The encoding of source files. 51 | # 52 | # source_encoding = 'utf-8-sig' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = u'Sultan' 59 | copyright = u'2017, Aeroxis, LLC' 60 | author = u'Aeroxis, LLC' 61 | 62 | 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | MAJOR, MINOR, RELEASE = None, None, None 68 | with open("../VERSION") as f: 69 | line = f.read() 70 | if line.count('.') == 1: 71 | MAJOR, MINOR = line.split(".") 72 | elif line.count('.') == 2: 73 | MAJOR, MINOR, RELEASE = line.split(".") 74 | 75 | # The short X.Y version. 76 | version = u'.'.join([MAJOR, MINOR]) 77 | 78 | # The full version, including alpha/beta/rc tags. 79 | release = u'.'.join([MAJOR, MINOR, RELEASE]) 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # 84 | # This is also used if you do content translation via gettext catalogs. 85 | # Usually you set "language" from the command line for these cases. 86 | language = 'en' 87 | 88 | # There are two options for replacing |today|: either, you set today to some 89 | # non-false value, then it is used: 90 | # 91 | # today = '' 92 | # 93 | # Else, today_fmt is used as the format for a strftime call. 94 | # 95 | # today_fmt = '%B %d, %Y' 96 | 97 | # List of patterns, relative to source directory, that match files and 98 | # directories to ignore when looking for source files. 99 | # This patterns also effect to html_static_path and html_extra_path 100 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 101 | 102 | # The reST default role (used for this markup: `text`) to use for all 103 | # documents. 104 | # 105 | # default_role = None 106 | 107 | # If true, '()' will be appended to :func: etc. cross-reference text. 108 | # 109 | # add_function_parentheses = True 110 | 111 | # If true, the current module name will be prepended to all description 112 | # unit titles (such as .. function::). 113 | # 114 | # add_module_names = True 115 | 116 | # If true, sectionauthor and moduleauthor directives will be shown in the 117 | # output. They are ignored by default. 118 | # 119 | # show_authors = False 120 | 121 | # The name of the Pygments (syntax highlighting) style to use. 122 | pygments_style = 'sphinx' 123 | 124 | # A list of ignored prefixes for module index sorting. 125 | # modindex_common_prefix = [] 126 | 127 | # If true, keep warnings as "system message" paragraphs in the built documents. 128 | # keep_warnings = False 129 | 130 | # If true, `todo` and `todoList` produce output, else they produce nothing. 131 | todo_include_todos = False 132 | 133 | 134 | # -- Options for HTML output ---------------------------------------------- 135 | 136 | # The theme to use for HTML and HTML Help pages. See the documentation for 137 | # a list of builtin themes. 138 | # 139 | html_theme_path = [alabaster.get_path()] 140 | 141 | extensions = ['alabaster'] 142 | 143 | html_theme = 'alabaster' 144 | 145 | # Theme options are theme-specific and customize the look and feel of a theme 146 | # further. For a list of options available for each theme, see the 147 | # documentation. 148 | # 149 | html_theme_options = { 150 | 'logo': 'sultan-logo.png', 151 | 'logo_name': 'foobar', 152 | 'github_banner': True, 153 | 'github_user': 'aeroxis', 154 | 'github_repo': 'sultan', 155 | 'logo': 'sultan-logo.png', 156 | 'github_button': True, 157 | 'github_count': True, 158 | 'sidebar_includehidden': True, 159 | 'show_related': True, 160 | 'fixed_sidebar': True, 161 | 'font_size': '14px', 162 | 'font_family': ','.join([ 163 | 'Consolas', 164 | 'Menlo', 165 | 'Deja Vu Sans Mono', 166 | 'Bitstream Vera Sans Mono', 167 | 'monospace']), 168 | } 169 | 170 | # Add any paths that contain custom themes here, relative to this directory. 171 | # html_theme_path = [] 172 | 173 | # The name for this set of Sphinx documents. 174 | # " v documentation" by default. 175 | # 176 | # html_title = u'Sultan v0.1.13' 177 | 178 | # A shorter title for the navigation bar. Default is the same as html_title. 179 | # 180 | # html_short_title = None 181 | 182 | # The name of an image file (relative to this directory) to place at the top 183 | # of the sidebar. 184 | # 185 | # html_logo = None 186 | 187 | # The name of an image file (relative to this directory) to use as a favicon of 188 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 189 | # pixels large. 190 | # 191 | # html_favicon = None 192 | 193 | # Add any paths that contain custom static files (such as style sheets) here, 194 | # relative to this directory. They are copied after the builtin static files, 195 | # so a file named "default.css" will overwrite the builtin "default.css". 196 | html_static_path = ['_static'] 197 | 198 | # Add any extra paths that contain custom files (such as robots.txt or 199 | # .htaccess) here, relative to this directory. These files are copied 200 | # directly to the root of the documentation. 201 | # 202 | # html_extra_path = [] 203 | 204 | # If not None, a 'Last updated on:' timestamp is inserted at every page 205 | # bottom, using the given strftime format. 206 | # The empty string is equivalent to '%b %d, %Y'. 207 | # 208 | # html_last_updated_fmt = None 209 | 210 | # If true, SmartyPants will be used to convert quotes and dashes to 211 | # typographically correct entities. 212 | # 213 | # html_use_smartypants = True 214 | 215 | # Custom sidebar templates, maps document names to template names. 216 | # 217 | # html_sidebars = {} 218 | 219 | # Additional templates that should be rendered to pages, maps page names to 220 | # template names. 221 | # 222 | # html_additional_pages = {} 223 | 224 | # If false, no module index is generated. 225 | # 226 | # html_domain_indices = True 227 | 228 | # If false, no index is generated. 229 | # 230 | # html_use_index = True 231 | 232 | # If true, the index is split into individual pages for each letter. 233 | # 234 | # html_split_index = False 235 | 236 | # If true, links to the reST sources are added to the pages. 237 | # 238 | # html_show_sourcelink = True 239 | 240 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 241 | # 242 | # html_show_sphinx = True 243 | 244 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 245 | # 246 | # html_show_copyright = True 247 | 248 | # If true, an OpenSearch description file will be output, and all pages will 249 | # contain a tag referring to it. The value of this option must be the 250 | # base URL from which the finished HTML is served. 251 | # 252 | # html_use_opensearch = '' 253 | 254 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 255 | # html_file_suffix = None 256 | 257 | # Language to be used for generating the HTML full-text search index. 258 | # Sphinx supports the following languages: 259 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 260 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 261 | # 262 | # html_search_language = 'en' 263 | 264 | # A dictionary with options for the search language support, empty by default. 265 | # 'ja' uses this config value. 266 | # 'zh' user can custom change `jieba` dictionary path. 267 | # 268 | # html_search_options = {'type': 'default'} 269 | 270 | # The name of a javascript file (relative to the configuration directory) that 271 | # implements a search results scorer. If empty, the default will be used. 272 | # 273 | # html_search_scorer = 'scorer.js' 274 | 275 | # Output file base name for HTML help builder. 276 | htmlhelp_basename = 'Sultandoc' 277 | 278 | # -- Options for LaTeX output --------------------------------------------- 279 | 280 | latex_elements = { 281 | # The paper size ('letterpaper' or 'a4paper'). 282 | # 283 | # 'papersize': 'letterpaper', 284 | 285 | # The font size ('10pt', '11pt' or '12pt'). 286 | # 287 | # 'pointsize': '10pt', 288 | 289 | # Additional stuff for the LaTeX preamble. 290 | # 291 | # 'preamble': '', 292 | 293 | # Latex figure (float) alignment 294 | # 295 | # 'figure_align': 'htbp', 296 | } 297 | 298 | # Grouping the document tree into LaTeX files. List of tuples 299 | # (source start file, target name, title, 300 | # author, documentclass [howto, manual, or own class]). 301 | latex_documents = [ 302 | (master_doc, 'Sultan.tex', u'Sultan Documentation', 303 | u'Aeroxis, LLC', 'manual'), 304 | ] 305 | 306 | # The name of an image file (relative to this directory) to place at the top of 307 | # the title page. 308 | # 309 | # latex_logo = None 310 | 311 | # For "manual" documents, if this is true, then toplevel headings are parts, 312 | # not chapters. 313 | # 314 | # latex_use_parts = False 315 | 316 | # If true, show page references after internal links. 317 | # 318 | # latex_show_pagerefs = False 319 | 320 | # If true, show URL addresses after external links. 321 | # 322 | # latex_show_urls = False 323 | 324 | # Documents to append as an appendix to all manuals. 325 | # 326 | # latex_appendices = [] 327 | 328 | # It false, will not define \strong, \code, itleref, \crossref ... but only 329 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 330 | # packages. 331 | # 332 | # latex_keep_old_macro_names = True 333 | 334 | # If false, no module index is generated. 335 | # 336 | # latex_domain_indices = True 337 | 338 | 339 | # -- Options for manual page output --------------------------------------- 340 | 341 | # One entry per manual page. List of tuples 342 | # (source start file, name, description, authors, manual section). 343 | man_pages = [ 344 | (master_doc, 'sultan', u'Sultan Documentation', 345 | [author], 1) 346 | ] 347 | 348 | # If true, show URL addresses after external links. 349 | # 350 | # man_show_urls = False 351 | 352 | 353 | # -- Options for Texinfo output ------------------------------------------- 354 | 355 | # Grouping the document tree into Texinfo files. List of tuples 356 | # (source start file, target name, title, author, 357 | # dir menu entry, description, category) 358 | texinfo_documents = [ 359 | (master_doc, 'Sultan', u'Sultan Documentation', 360 | author, 'Sultan', 'One line description of project.', 361 | 'Miscellaneous'), 362 | ] 363 | 364 | # Documents to append as an appendix to all manuals. 365 | # 366 | # texinfo_appendices = [] 367 | 368 | # If false, no module index is generated. 369 | # 370 | # texinfo_domain_indices = True 371 | 372 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 373 | # 374 | # texinfo_show_urls = 'footnote' 375 | 376 | # If true, do not generate a @detailmenu in the "Top" node's menu. 377 | # 378 | # texinfo_no_detailmenu = False 379 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Frequently Asked Questions 3 | ========================== 4 | 5 | What is Sultan? 6 | --------------- 7 | 8 | Sultan allows you to interface with command-line utilities from Python without 9 | having to write your scripts in Bash. 10 | 11 | Why use Sultan? 12 | --------------- 13 | 14 | Leverage the Power of Bash from Python 15 | ************************************** 16 | 17 | Bash, while it seems arcane, actually is quite powerful! Creating a Tar Archive 18 | of a directory requires us to read up on the tar API or check StackOverflow, but 19 | if you know how to do it in Bash, you'd be done before you finish reading the 20 | StackOverflow post! 21 | 22 | This promotes simplicity, and follows the KISS principle 23 | (Keep It Simple Stupid!) 24 | 25 | Better Syntax: 26 | ************** 27 | 28 | Bash's syntax for loops, conditionals, and functions work well, but require a 29 | lot of nuances that we're just not used to with a modern language like Python. 30 | 31 | Sultan allows you to use Python Syntax, and never touch Bash's arcaine syntax 32 | ever again! 33 | 34 | Project Management: 35 | ******************* 36 | 37 | Sultan was designed because Bash just does not scale well with most projects. 38 | As much as we can try, Bash does not have a proper package management system. 39 | So, we're left with script files that are sourced, and called, but it gets quiet 40 | complicated as soon as you have more than 10 scripts. 41 | 42 | Python has a great package and module system that allows us to create complex 43 | projects using the Python language, but also leverage a lot of great tools and 44 | functionality that we've grown to love and expect from Bash. 45 | 46 | This promotes reusability, with the DRY (Don't Repeat Yourself) principle. If 47 | you create a great solution with Sultan, publish it on **PyPi** and others will 48 | use it. 49 | 50 | Unit Testing 51 | ************ 52 | 53 | Out of the Box, Bash does not come with any Unittesting frameworks, but Python 54 | does! You can build unittests and integration tests with Sultan to ensure your 55 | code does *EXACTLY* what you want it to do. 56 | 57 | Why can't I use `subprocess`? 58 | ----------------------------- 59 | 60 | Python's standard library offers the subprocess library, but it isn't very 61 | "Pythonic". The 'subprocess' module has a bunch of methods for writing commands 62 | to the shell, but the code is overly verbose, and tough to read. 63 | 64 | Any reason to use this over ansible or saltstack? 65 | ------------------------------------------------- 66 | 67 | Sultan is just a simpler interface to command line utilities. It helps bypass 68 | the arcane language constructs of Bash (among other things. 69 | See **Why use Sultan?** above). 70 | 71 | Sultan was created to help with scripts that we create with Bash, that tend to get 72 | complex. When these scripts get complex, Bash just gets to be a pain to deal 73 | with, since it lacks proper package management, it lacks unit testing, and 74 | . 75 | So Sultan allows scripts to be reusable and tested with standard Python. 76 | 77 | Ansible and Salt are powerful for provisioning a system. Sultan can't compete 78 | in that realm, but it does help with complex scripts. Even if you want Ansible 79 | or Salt to perform something on a remote box, like installing a package, it 80 | requires some overhead in setting them up. Sultan is simple with no external 81 | dependencies, and installs itself with just "pip install sultan". 82 | 83 | Sultan simply wraps the subprocess module in Python's standard library, but it 84 | also provides a nice to read logging system, and provides you with relevant 85 | information when a command fails. 86 | 87 | All in all, it can't compete with standard DevOps tools used for provisioning. 88 | It does help with not having to use Bash heavily, if you're a Python programmer. 89 | -------------------------------------------------------------------------------- /docs/img/readme-1-simple-usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/docs/img/readme-1-simple-usage.png -------------------------------------------------------------------------------- /docs/img/readme-2-context-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/docs/img/readme-2-context-manager.png -------------------------------------------------------------------------------- /docs/img/readme-3-ssh-access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/docs/img/readme-3-ssh-access.png -------------------------------------------------------------------------------- /docs/img/readme-4-error-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/docs/img/readme-4-error-message.png -------------------------------------------------------------------------------- /docs/img/sultan-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/docs/img/sultan-logo.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: img/sultan-logo.png 3 | :alt: Sultan logo 4 | :align: center 5 | ------------------------------- 6 | 7 | **Command and Rule Over Your Shell** 8 | 9 | ------------------------------- 10 | 11 | ====== 12 | Sultan 13 | ====== 14 | .. automodule:: sultan.api 15 | 16 | .. toctree:: 17 | :caption: Table of Contents 18 | :name: mastertoc 19 | :maxdepth: 2 20 | 21 | install-sultan 22 | faq 23 | sultan-examples 24 | sultan-ssh-examples 25 | 26 | ------------------------------- 27 | 28 | --------------- 29 | What is Sultan? 30 | --------------- 31 | 32 | Sultan is a Python package for interfacing with command-line utilities, like 33 | `yum`, `apt-get`, or `ls`, in a Pythonic manner. It lets you run command-line 34 | utilities using simple function calls. 35 | 36 | The simplest way to use Sultan is to just call it: 37 | 38 | .. code:: python 39 | 40 | from sultan.api import Sultan 41 | s = Sultan() 42 | s.sudo("yum install -y tree").run() 43 | 44 | **Runs:** 45 | 46 | .. code:: bash 47 | 48 | sudo yum install -y tree; 49 | 50 | The recommended way of using Sultan is to use it in Context Management mode. 51 | Here is how to use Sultan with Context Management: 52 | 53 | .. code:: python 54 | 55 | from sultan.api import Sultan 56 | 57 | with Sultan.load(sudo=True) as s: 58 | s.yum("install -y tree").run() 59 | 60 | **Runs:** 61 | 62 | .. code:: bash 63 | 64 | sudo su - root -c 'yum install -y tree;' 65 | 66 | What if we want to install this command on a remote machine? You can easily 67 | achieve this using context management: 68 | 69 | .. code:: python 70 | 71 | from sultan.api import Sultan 72 | 73 | with Sultan.load(sudo=True, hostname="myserver.com") as sultan: 74 | sultan.yum("install -y tree").run() 75 | 76 | **Runs:** 77 | 78 | .. code:: bash 79 | 80 | ssh root@myserver.com 'sudo su - root -c 'yum install -y tree;'' 81 | 82 | If you enter a wrong command, Sultan will print out details you need to debug and 83 | find the problem quickly. 84 | 85 | Here, the same command was run on a Mac: 86 | 87 | .. code:: python 88 | 89 | from sultan.api import Sultan 90 | 91 | with Sultan.load(sudo=True, hostname="myserver.com") as sultan: 92 | sultan.yum("install -y tree").run() 93 | 94 | 95 | **Yields:** 96 | 97 | .. code:: bash 98 | 99 | [sultan]: sudo su - root -c 'yum install -y tree;' 100 | Password: 101 | [sultan]: --{ STDERR }------------------------------------------------------------------------------------------------------- 102 | [sultan]: | -sh: yum: command not found 103 | [sultan]: ------------------------------------------------------------------------------------------------------------------- 104 | 105 | Want to get started? Simply install Sultan, and start writing your clean code:: 106 | 107 | pip install --upgrade sultan 108 | 109 | If you have more questions, check the rest of the docs, or reach out at 110 | Github: https://github.com/aeroxis/sultan 111 | 112 | 113 | WARNING * WARNING * WARNING 114 | --------------------------- 115 | 116 | When you're using Sultan, you are running commands directly on your local shell, 117 | so please, do not run untested and untrusted code. You are taking the risk if 118 | you are running untrusted code. 119 | 120 | Sultan runs *POpen* with *shell=True*, and according to Python documentation, 121 | this can be a security hazard if combined with untrusted input. More information 122 | can be found here: 123 | 124 | * Python 2: https://docs.python.org/2/library/subprocess.html#frequently-used-arguments 125 | * Python 3: https://docs.python.org/3/library/subprocess.html#frequently-used-arguments -------------------------------------------------------------------------------- /docs/install-sultan.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Installing Sultan 3 | ================= 4 | 5 | Sultan is simple and lightweight. To install Sultan, simply run the following 6 | in your command line:: 7 | 8 | pip install --upgrade sultan 9 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Sultan.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Sultan.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.9 2 | Babel==2.3.4 3 | docutils==0.12 4 | future==0.17 5 | imagesize==0.7.1 6 | Jinja2==2.8 7 | MarkupSafe==0.23 8 | Pygments==2.1.3 9 | pytz==2016.7 10 | six==1.10.0 11 | snowballstemmer==1.2.1 12 | Sphinx==1.4.8 13 | sphinx-rtd-theme==0.1.9 14 | -------------------------------------------------------------------------------- /docs/sultan-examples.rst: -------------------------------------------------------------------------------- 1 | 2 | =============== 3 | Sultan Examples 4 | =============== 5 | 6 | This tutorial will go through various examples to help in better understanding 7 | how to use Sultan. Each example will build on the lessons learned from the 8 | previous examples. 9 | 10 | WARNING * WARNING * WARNING 11 | --------------------------- 12 | 13 | When you're using Sultan, you are running commands directly on your local shell, 14 | so please, do not run untested and untrusted code. You are taking the risk if 15 | you are running untrusted code. 16 | 17 | Sultan runs *POpen* with *shell=True*, and according to Python documentation, 18 | this can be a security hazard if combined with untrusted input. More information 19 | can be found here: 20 | 21 | * Python 2: https://docs.python.org/2/library/subprocess.html#frequently-used-arguments 22 | * Python 3: https://docs.python.org/3/library/subprocess.html#frequently-used-arguments 23 | 24 | 25 | Example 1: Getting Started 26 | -------------------------- 27 | 28 | We typically use `yum` or `apt-get` to install a package on our system. 29 | This example installs a package on our system using Sultan. Here is how 30 | to get started:: 31 | 32 | from sultan.api import Sultan 33 | 34 | s = Sultan() 35 | s.yum("install", "-y", "tree").run() 36 | 37 | Sultan allows multiple syntaxes depending on what your first command is. 38 | Suppose you want to not use separate tokens, and instead you want to use 39 | one string, you can write the above example as such:: 40 | 41 | 42 | from sultan.api import Sultan 43 | 44 | s = Sultan() 45 | s.yum("install -y tree").run() 46 | 47 | Suppose your user is not a root-user, and you want to call to sudo to install 48 | the `tree` package. You'd do the following:: 49 | 50 | from sultan.api import Sultan 51 | 52 | with Sultan.load(sudo=True) as s: 53 | s.yum('install -y tree').run() 54 | 55 | **NOTE:** For the sake of brevity, this tutorial will now start to assume that 56 | `Sultan` has been imported from `sultan.api` and, the variable `s` has been 57 | instantiated as an instance of `Sultan` (`s = Sultan()`). This will change in 58 | situations where the documentation requires a different usage. 59 | 60 | Example 2: Sultan with Context Management 61 | ----------------------------------------- 62 | 63 | There are times when we want to manage the context of where Sultan executes 64 | your code. To aid with this, we use Sultan in Context Management mode. 65 | 66 | Suppose we want to cat out the contents of `/etc/hosts`, we'd do the following:: 67 | 68 | with Sultan.load(cwd="/etc") as s: 69 | s.cat("hosts").run() 70 | 71 | Example 3: Compounding with And (&&) and Or (||) 72 | ------------------------------------------------ 73 | 74 | There are times when we need multiple commands to run at once. We use the 75 | `and_()` command to get through this. Here is an example:: 76 | 77 | # runs: 'cd /tmp && touch foobar.txt' 78 | with Sultan.load() as s: 79 | s.cd("/tmp").and_().touch("foobar.txt").run() 80 | 81 | There are also times that we want to run 2 commands, but run the 2nd command 82 | even if the first command fails. For this, you will need to use the `or_()` 83 | command. Here is an example:: 84 | 85 | # runs: 'mkdir /tmp || mkdir /bar' 86 | with Sultan.load() as s: 87 | s.mkdir('/tmp').or_().mkdir('/bar').run() 88 | 89 | Example 4: Redirecting with Pipes (|) 90 | ------------------------------------- 91 | 92 | In Bash, we use the pipe `|` operator to redirect the output of the call to a 93 | command to another command. We do this in Sultan with the `pipe` command. Here 94 | is an example:: 95 | 96 | # runs: 'ls -l | sed -e "s/[aeio]/u/g"' 97 | with Sultan.load() as s: 98 | s.ls('-l').pipe().sed('-e', '"s/[aeio]/u/g"').run() 99 | 100 | Example 5: Redirecting Output to File 101 | ------------------------------------- 102 | 103 | In Bash, we often want to redirect the output of a command to file. Whether 104 | the output is in `stdout` or `stderr`, we can redirect it to a file with 105 | Sultan. Here is an example:: 106 | 107 | # runs: 'cat /etc/hosts > ~/hosts' 108 | with Sultan.load() as s: 109 | s.cat("/etc/hosts").redirect( 110 | "~/hosts", 111 | append=False, 112 | stdout=True, 113 | stderr=False).run() 114 | 115 | In the example above, we redirected the output of `/etc/hosts` to `~/hosts`. 116 | We only outputted the `stdout`, and didn't append to the file if it existed. 117 | Feel free to customize this method as it fits your needs. 118 | 119 | Example 6: Read from Standard Input 120 | ----------------------------------- 121 | 122 | Python has the `raw_input` built-in to read from standard input. Sultan's API 123 | wraps around `raw_input` to ask the user for their input from the command line 124 | and returns the value. 125 | 126 | Here is the example:: 127 | 128 | name = s.stdin("What is your name?") 129 | print "Hello %s" % name 130 | 131 | Example 7: Running as Another User 132 | ---------------------------------- 133 | 134 | Sultan can run commands as another user. You need to enable `sudo` 135 | mode to do this. 136 | 137 | Here is an example:: 138 | 139 | # runs: sudo su - hodor -c 'cd /home/hodor && ls -lah .;' 140 | with Sultan.load(sudo=True, user='hodor', cwd='/home/hodor') as s: 141 | sultan.ls('-lah', '.').run() 142 | 143 | Example 8: Running as Root 144 | -------------------------- 145 | 146 | Sultan can run commands as the `root` user. You need to only enable `sudo` 147 | mode to do this. 148 | 149 | Here is an example:: 150 | 151 | # runs: sudo su - root -c 'ls -lah /root;' 152 | with Sultan.load(sudo=True) as sultan: 153 | sultan.ls('-lah', '/root').run() 154 | 155 | Example 9: Disable Logging 156 | -------------------------- 157 | 158 | If you need to disable logging all together, simply add set 'logging' to False 159 | while loading Sultan with Context. 160 | 161 | Here is an example:: 162 | 163 | # runs without logging 164 | with Sultan.load(logging=False) as sultan: 165 | sultan.ls('-lah', '/tmp').run() 166 | 167 | Example 10: Commands with Hyphones (i.e.: apt-get) 168 | -------------------------------------------------- 169 | 170 | There are commands that are available in the shell that use hyphens which 171 | conflict with the function naming conventions of Python, for example 172 | **apt-get**. To get around this, use double underscores (__). 173 | 174 | Here is an example:: 175 | 176 | with Sultan.load(sudo=True) as sultan: 177 | sultan.apt__get('install', 'httpd').run() 178 | 179 | which runs:: 180 | 181 | sudo apt-get install httpd; 182 | 183 | Example 11: Sourcing a File before Running a Command 184 | ---------------------------------------------------- 185 | 186 | This is rare, but there are times that we would like to source a file before 187 | running a command. We can manually do this with the Bash And Operator (&&) but 188 | Sultan has the ability to do this automatically for you. 189 | 190 | Here is an example:: 191 | 192 | with Sultan.load( 193 | cwd='/home/davydany/projects/sultan', 194 | src='/home/davydany/.virtualenv/sultan/bin/activate') as s: 195 | s.pip('install', '-r', 'requirements.txt').run() 196 | 197 | which runs:: 198 | 199 | source /home/davydany/.virtualenv/sultan/bin/activate && cd /home/davydany/projects/sultan && pip install -r requirements.txt; 200 | 201 | Example 12: Results from a Command 202 | ---------------------------------- 203 | 204 | When you run a command, your shell gives back results in stdout and stderr. 205 | Sultan returns a Result object which has **stdout**, **stderr**, 206 | **traceback** and **rc** attributes. 207 | 208 | Here is an example that shows how to get the results of a command:: 209 | 210 | with Sultan.load() as s: 211 | result = s.yum('install', '-y', 'postgresql').run() 212 | result.stdout # the stdout 213 | result.stderr # the stderr 214 | result.traceback # the traceback 215 | result.rc # the return code 216 | 217 | **stdout** and **stderr** returns a list, where each element is a line from 218 | **stdout** and **stderr**; **rc** is an integer. 219 | 220 | Most times, you don't need to access the results of a command, but there are 221 | times that you need to do so. For that, the **Result** object will be how you 222 | access it. 223 | 224 | Example 13: Streaming Results from a Command 225 | ---------------------------------- 226 | 227 | Here is an example that shows how to get real-time output from a command:: 228 | 229 | with Sultan.load() as s: 230 | result = s.yum('install', '-y', 'postgresql').run(streaming=True) 231 | while True: 232 | # if full output is needed, read the pipes one last time 233 | # after `is_complete == True` to avoid a race condition 234 | complete = result.is_complete 235 | for line in result.stdout: 236 | print(line) 237 | for line in result.stderr: 238 | print(line) 239 | if complete: 240 | break 241 | time.sleep(1) 242 | 243 | Example 14: Custom Executable 244 | ---------------------------------- 245 | 246 | By default python's `subprocess ` 247 | executes the program through **/bin/sh** on POSIX systems. In the rare circumstances 248 | when that's not desired, you can change it with the 'executable' argument while 249 | loading Sultan with Context. 250 | 251 | Here is an example:: 252 | 253 | 254 | Here is an example that shows how to get the results of a command:: 255 | 256 | with Sultan.load(executable='/bin/bash') as sultan_bash: 257 | 258 | result = sultan_bash.ps('-p', '$$', '-ocomm=') 259 | assert result == 'bash' 260 | 261 | with Sultan.load(executable='/bin/dash') as sultan_other: 262 | 263 | result = sultan_other.ps('-p', '$$', '-ocomm=') 264 | assert result == 'dash' 265 | -------------------------------------------------------------------------------- /docs/sultan-ssh-examples.rst: -------------------------------------------------------------------------------- 1 | 2 | =================== 3 | Sultan SSH Examples 4 | =================== 5 | 6 | This tutorial will go through various examples to help in better understanding 7 | how to use Sultan over SSH. Each example will build on the lessons learned from the 8 | previous examples. 9 | 10 | WARNING * WARNING * WARNING 11 | --------------------------- 12 | 13 | When you're using Sultan, you are running commands directly on your local shell, 14 | so please, do not run untested and untrusted code. You are taking the risk if 15 | you are running untrusted code. 16 | 17 | Sultan runs *POpen* with *shell=True*, and according to Python documentation, 18 | this can be a security hazard if combined with untrusted input. More information 19 | can be found here: 20 | 21 | * Python 2: https://docs.python.org/2/library/subprocess.html#frequently-used-arguments 22 | * Python 3: https://docs.python.org/3/library/subprocess.html#frequently-used-arguments 23 | 24 | Example 1: SSH to Remote Host as the Current User 25 | ------------------------------------------------- 26 | 27 | By default, you can simply specify the host to sultan, and calling the commands 28 | like you normally do. This uses the username of the user who is executing the 29 | script, and connects you to the remote host. :: 30 | 31 | from sultan.api import Sultan 32 | 33 | with Sultan.load(hostname='aeroxis.com') as sultan: 34 | s.yum('install', '-y', 'tree').run() 35 | 36 | Sultan will connect to the remote host, and run `yum install -y tree`. This is 37 | what is passed to your shell to execute the command 38 | (assuming your username is `davydany`):: 39 | 40 | ssh davydany@aeroxis.com 'yum install -y tree;' 41 | 42 | Example 2: SSH to Remote Host as a Different User 43 | ------------------------------------------------- 44 | 45 | You can specify a different user to execute the remote commands by using the 46 | `user` parameter, like this:: 47 | 48 | with Sultan.load(user='elon.musk', hostname='aeroxis.com') as s: 49 | s.yum('install', '-y', 'tree').run() 50 | 51 | And this will execute:: 52 | 53 | ssh elon.musk@aeroxis.com 'yum install -y tree;' 54 | 55 | Example 3: Passing Additional Options (Port) 56 | -------------------------------------------- 57 | 58 | **Added in v0.6** 59 | 60 | If you need to pass additional options for the port, use the `SSHConfig` class 61 | to configure the SSH Connection.:: 62 | 63 | from sultan.api import Sultan, SSHConfig 64 | 65 | port = 2222 66 | config = SSHConfig(port=port) 67 | with Sultan.load(user='elon.musk', 68 | hostname='aeroxis.com', 69 | ssh_config=config) as s: 70 | s.yum('install', '-y', 'tree').run() 71 | 72 | which will yield:: 73 | 74 | ssh -p 2222 elon.musk@aeroxis.com 'yum install -y tree;' 75 | 76 | 77 | Example 4: Passing Additional Options (Identity File) 78 | ----------------------------------------------------- 79 | 80 | **Added in v0.6** 81 | 82 | If you need to pass additional options for the port, use the `SSHConfig` class 83 | to configure the SSH Connection.:: 84 | 85 | from sultan.api import Sultan, SSHConfig 86 | 87 | path_to_identity_file = '/home/elon.musk/keys/elon.musk.identity' 88 | config = SSHConfig(identity_file=path_to_identity_file) 89 | with Sultan.load(user='elon.musk', 90 | hostname='aeroxis.com', 91 | ssh_config=config) as s: 92 | s.yum('install', '-y', 'tree').run() 93 | 94 | which will yield:: 95 | 96 | ssh -i /home/elon.musk/keys/elon.musk.identity elon.musk@aeroxis.com 'yum install -y tree;' 97 | 98 | -------------------------------------------------------------------------------- /img/sultan-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/img/sultan-logo.png -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://raw.githubusercontent.com/aeroxis/sultan/master/docs/img/sultan-logo.png 2 | :alt: sultan logo 3 | :align: right 4 | 5 | **Command and Rule over your Shell** 6 | 7 | .. image:: https://badge.fury.io/py/sultan.svg 8 | :alt: PyPI Version 9 | :target: https://badge.fury.io/py/sultan 10 | 11 | .. image:: https://travis-ci.org/aeroxis/sultan.svg?branch=master 12 | :alt: Travis Build Status 13 | :target: https://travis-ci.org/aeroxis/sultan 14 | 15 | .. image:: http://img.shields.io/:license-mit-blue.svg 16 | :alt: MIT License 17 | :target: http://doge.mit-license.org 18 | 19 | .. image:: https://readthedocs.org/projects/sultan/badge/?version=latest 20 | :alt: Documentation Status 21 | :target: http://sultan.readthedocs.io/en/latest/?badge=latest 22 | 23 | Sultan now supports Python 2.7+ and 3.0+ 24 | 25 | ---- 26 | Note 27 | ---- 28 | Your input is welcome! Please provide your feedback by creating 29 | `issues on Github `_ 30 | 31 | ------- 32 | Install 33 | ------- 34 | 35 | .. code:: bash 36 | 37 | pip install --upgrade sultan 38 | 39 | ------------- 40 | Documentation 41 | ------------- 42 | 43 | .. image:: https://readthedocs.org/projects/sultan/badge/?version=latest 44 | :alt: Documentation Status 45 | :target: http://sultan.readthedocs.io/en/latest/?badge=latest 46 | 47 | Documentation is available on ReadTheDocs: http://sultan.readthedocs.io/en/latest/ 48 | 49 | --------------- 50 | What is Sultan? 51 | --------------- 52 | 53 | Sultan is a Python package for interfacing with command-line utilities, like 54 | `yum`, `apt-get`, or `ls`, in a Pythonic manner. It lets you run command-line 55 | utilities using simple function calls. 56 | 57 | The simplest way to use Sultan is to just call it: 58 | 59 | .. code:: python 60 | 61 | from sultan.api import Sultan 62 | s = Sultan() 63 | s.sudo("yum install -y tree").run() 64 | 65 | **Runs:** 66 | 67 | .. code:: bash 68 | 69 | sudo yum install -y tree; 70 | 71 | ------------ 72 | 73 | The recommended way of using Sultan is to use it in Context Management mode. 74 | Here is how to use Sultan with Context Management: 75 | 76 | .. code:: python 77 | 78 | from sultan.api import Sultan 79 | 80 | with Sultan.load(sudo=True) as s: 81 | s.yum("install -y tree").run() 82 | 83 | **Runs:** 84 | 85 | .. code:: bash 86 | 87 | sudo su - root -c 'yum install -y tree;' 88 | 89 | ------------ 90 | 91 | What if we want to install this command on a remote machine? You can easily 92 | achieve this using context management: 93 | 94 | .. code:: python 95 | 96 | from sultan.api import Sultan 97 | 98 | with Sultan.load(sudo=True, hostname="myserver.com") as sultan: 99 | sultan.yum("install -y tree").run() 100 | 101 | **Runs:** 102 | 103 | .. code:: bash 104 | 105 | ssh root@myserver.com 'sudo su - root -c 'yum install -y tree;'' 106 | 107 | ------------ 108 | 109 | If you enter a wrong command, Sultan will print out details you need to debug and 110 | find the problem quickly. 111 | 112 | Here, the same command was run on a Mac: 113 | 114 | .. code:: python 115 | 116 | from sultan.api import Sultan 117 | 118 | with Sultan.load(sudo=True, hostname="myserver.com") as sultan: 119 | sultan.yum("install -y tree").run() 120 | 121 | 122 | **Yields:** 123 | 124 | .. code:: bash 125 | 126 | [sultan]: sudo su - root -c 'yum install -y tree;' 127 | Password: 128 | [sultan]: --{ STDERR }------------------------------------------------------------------------------------------------------- 129 | [sultan]: | -sh: yum: command not found 130 | [sultan]: ------------------------------------------------------------------------------------------------------------------- 131 | 132 | ------------ 133 | 134 | Want to get started? Simply install Sultan, and start writing your clean code:: 135 | 136 | pip install --upgrade sultan 137 | 138 | If you have more questions, check the docs! http://sultan.readthedocs.io/en/latest/ 139 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | formats: 3 | - htmlzip 4 | - epub 5 | - pdf 6 | requirements: docs/requirements.txt 7 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | ipython==5.1.0 2 | ipython-genutils==0.1.0 3 | ipdb==0.10.1 4 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | coverage==4.2 2 | nose==1.3.7 3 | mock==2.0.0 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("./README.rst") as f: 4 | LONG_DESCRIPTION = f.read() 5 | 6 | with open("./VERSION") as f: 7 | VERSION = f.read().strip() 8 | 9 | setup( 10 | name='sultan', 11 | description='Command and Rule over your Shell', 12 | long_description=LONG_DESCRIPTION, 13 | version=VERSION, 14 | author='David Daniel', 15 | author_email='davydany@aeroxis.com', 16 | license='MIT', 17 | packages=find_packages('src'), 18 | package_dir={'': 'src'}, 19 | url='http://github.com/aeroxis/sultan', 20 | install_requires=[], 21 | classifiers=[ 22 | "Development Status :: 5 - Production/Stable", 23 | "Environment :: Console", 24 | "Operating System :: POSIX :: Linux", 25 | "Operating System :: Microsoft :: Windows", 26 | "Operating System :: MacOS", 27 | "Operating System :: MacOS :: MacOS X", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 2", 30 | "Programming Language :: Python :: 2.7", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.3", 33 | "Programming Language :: Python :: 3.4", 34 | "Programming Language :: Python :: 3.5", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Unix Shell", 37 | "License :: OSI Approved :: MIT License"] 38 | ) 39 | -------------------------------------------------------------------------------- /src/sultan/__init__.py: -------------------------------------------------------------------------------- 1 | from sultan.api import Sultan -------------------------------------------------------------------------------- /src/sultan/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sultan is a Python package for interfacing with command-line utilities, like 3 | `yum`, `apt-get`, or `ls`, in a Pythonic manner. It lets you run command-line 4 | utilities using simple function calls. 5 | 6 | Here is how you'd use Sultan:: 7 | 8 | from sultan.api import Sultan 9 | 10 | # simple way 11 | s = Sultan() 12 | s.sudo("yum install -y tree").run() 13 | 14 | # with context management (recommended) 15 | with Sultan.load(sudo=True) as s: 16 | s.yum("install -y tree").run() 17 | 18 | What if we want to install this command on a remote machine? You can easily 19 | achieve this using context management:: 20 | 21 | with open(sudo=True, hostname="myserver.com") as s: 22 | s.yum("install -y tree").run() 23 | 24 | If you enter a wrong command, Sultan will print out details you need to debug and 25 | find the problem quickly. 26 | 27 | Here, the same command was run on a Mac:: 28 | 29 | In [1]: with Sultan.load(sudo=True) as s: 30 | ...: s.yum("install -y tree").run() 31 | ...: 32 | [sultan]: sudo su - root -c 'yum install -y tree;' 33 | Password: 34 | [sultan]: Unable to run 'sudo su - root -c 'yum install -y tree;'' 35 | [sultan]: --{ TRACEBACK }---------------------------------------------------------------------------------------------------- 36 | [sultan]: | Traceback (most recent call last): 37 | [sultan]: | File "/Users/davydany/projects/aeroxis/sultan/src/sultan/api.py", line 159, in run 38 | [sultan]: | stdout = subprocess.check_output(commands, shell=True, stderr=stderr) 39 | [sultan]: | File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 573, in check_output 40 | [sultan]: | raise CalledProcessError(retcode, cmd, output=output) 41 | [sultan]: | CalledProcessError: Command 'sudo su - root -c 'yum install -y tree;'' returned non-zero exit status 127 42 | [sultan]: ------------------------------------------------------------------------------------------------------------------- 43 | [sultan]: --{ STDERR }------------------------------------------------------------------------------------------------------- 44 | [sultan]: | -sh: yum: command not found 45 | [sultan]: ------------------------------------------------------------------------------------------------------------------- 46 | 47 | Want to get started? Simply install Sultan, and start writing your clean code:: 48 | 49 | pip install --upgrade sultan 50 | 51 | If you have more questions, check the docs! http://sultan.readthedocs.io/en/latest/ 52 | """ 53 | 54 | import getpass 55 | import os 56 | import subprocess 57 | import sys 58 | 59 | from .core import Base 60 | from .config import Settings 61 | from .echo import Echo 62 | from .exceptions import InvalidContextError 63 | from .result import Result 64 | 65 | __all__ = ['Sultan'] 66 | 67 | if sys.version_info < (3, 0): 68 | input = raw_input 69 | 70 | 71 | class Sultan(Base): 72 | """ 73 | The Pythonic interface to Bash. 74 | """ 75 | 76 | @classmethod 77 | def load(cls, 78 | cwd=None, sudo=False, user=None, 79 | hostname=None, env=None, logging=True, 80 | executable=None, 81 | ssh_config=None, src=None, 82 | **kwargs): 83 | 84 | # initial checks 85 | if ssh_config and not isinstance(ssh_config, SSHConfig): 86 | msg = "The config passed (%s) must be an instance of SSHConfig." % \ 87 | ssh_config 88 | raise ValueError(msg) 89 | 90 | if src and not os.path.exists(src): 91 | raise IOError("The Source File provided (%s) does not exist" % src) 92 | 93 | if executable and not os.path.exists(executable): 94 | raise IOError("The set Executable (%s) does not exist" % src) 95 | 96 | context = {} 97 | context['cwd'] = cwd 98 | context['sudo'] = sudo 99 | context['hostname'] = hostname 100 | context['ssh_config'] = str(ssh_config) if ssh_config else '' 101 | context['env'] = env or None # must be None, for Python to get the current process's env. 102 | context['logging'] = logging 103 | context['src'] = src 104 | context['executable'] = executable 105 | 106 | # determine user 107 | if user: 108 | context['user'] = user 109 | else: 110 | context['user'] = getpass.getuser() 111 | context.update(kwargs) 112 | 113 | return cls(context=context) 114 | 115 | def __init__(self, context=None): 116 | 117 | self.commands = [] 118 | self._context = [context] if context is not None else [] 119 | self.logging_activated = context.get('logging') if context else False 120 | self._echo = Echo(activated=self.logging_activated) 121 | self.settings = Settings() 122 | 123 | @property 124 | def current_context(self): 125 | """ 126 | Returns the context that Sultan is running on 127 | """ 128 | return self._context[-1] if len(self._context) > 0 else {} 129 | 130 | def __enter__(self): 131 | """ 132 | Sultan can be used with context using `with` blocks, as such: 133 | 134 | ```python 135 | 136 | with Sultan.load(cwd="/tmp") as s: 137 | s.ls("-lah").run() 138 | ``` 139 | 140 | This is easier to manage than doing the following:: 141 | 142 | s = Sultan() 143 | s.cd("/tmp").and_().ls("-lah").run() 144 | 145 | There are one-off times when running `s.cd("/tmp").and_().ls("-lah").run()` works better. However, 146 | if you have multiple commands to run in a given directory, using Sultan with context, allows your 147 | code to be easy to manage. 148 | """ 149 | # do nothing since we got 'current_context' and '_context' are doing the work 150 | # however, we do want to alert the user that they're using contexts badly. 151 | if len(self._context) == 0: 152 | raise InvalidContextError("You're using the 'with' block to load Sultan, but didn't provide a context with 'Sultan.context(...)'") 153 | return self 154 | 155 | def __exit__(self, type, value, traceback): 156 | """ 157 | Restores the context to previous context. 158 | """ 159 | if len(self._context) > 0: 160 | self._context.pop() 161 | 162 | def __call__(self): 163 | 164 | if self.commands: 165 | 166 | # run commands 167 | self.run() 168 | 169 | # clear the commands buffer 170 | self.clear() 171 | 172 | def __getattr__(self, name): 173 | 174 | if name == "redirect": 175 | return Redirect(self, name) 176 | else: 177 | # When calling Bash Commands from Python with Sultan, we encounter 178 | # an issue where the Python doesn't allow special characters like 179 | # dashes (i.e.: apt-get). To get around this, we will use 2 180 | # underscores one after another to indicate that we want it to be a 181 | # dash, and replace it accordingly before calling Command 182 | name = name.replace('__', '-') 183 | 184 | # call Command() 185 | return Command(self, name) 186 | 187 | def exc(self, command, halt_on_nonzero=True, quiet=False, q=False, streaming=False): 188 | 189 | self.commands = command.split(' ') 190 | return self.run(halt_on_nonzero=halt_on_nonzero, quiet=quiet, q=q, streaming=streaming) 191 | 192 | def run(self, halt_on_nonzero=True, quiet=False, q=False, streaming=False): 193 | """ 194 | After building your commands, call `run()` to have your code executed. 195 | """ 196 | commands = str(self) 197 | if not (quiet or q): 198 | self._echo.cmd(commands) 199 | 200 | env = self._context[0].get('env', {}) if len(self._context) > 0 else os.environ 201 | executable = self.current_context.get('executable') 202 | try: 203 | process = subprocess.Popen(commands, 204 | bufsize=1, 205 | shell=True, 206 | env=env, 207 | stdin=subprocess.PIPE, 208 | stdout=subprocess.PIPE, 209 | stderr=subprocess.PIPE, 210 | executable=executable, 211 | universal_newlines=True) 212 | result = Result(process, commands, self._context, streaming, halt_on_nonzero=halt_on_nonzero) 213 | 214 | except Exception as e: 215 | result = Result(None, commands, self._context, exception=e) 216 | result.dump_exception() 217 | if halt_on_nonzero: 218 | raise e 219 | 220 | finally: 221 | self.clear() 222 | 223 | return result 224 | 225 | def _add(self, command): 226 | """ 227 | Private method that adds a custom command (see `pipe` and `and_`). 228 | 229 | NOT FOR PUBLIC USE 230 | """ 231 | self.commands.append(command) 232 | return self 233 | 234 | def clear(self): 235 | 236 | del self.commands[:] 237 | return self 238 | 239 | def __str__(self): 240 | """ 241 | Returns the chained commands that were built as a string. 242 | """ 243 | context = self.current_context 244 | SPECIAL_CASES = (Pipe, And, Redirect, Or) 245 | output = "" 246 | for i, cmd in enumerate(self.commands): 247 | 248 | if (i == 0): 249 | separator = "" 250 | else: 251 | if isinstance(cmd, SPECIAL_CASES): 252 | separator = " " 253 | else: 254 | if isinstance(self.commands[i - 1], SPECIAL_CASES): 255 | separator = " " 256 | else: 257 | separator = "; " 258 | 259 | cmd_str = str(cmd) 260 | output += separator + cmd_str 261 | 262 | output = output.strip() + ";" 263 | 264 | # update with 'cwd' context 265 | cwd = context.get('cwd') 266 | if cwd: 267 | prepend = "cd %s && " % (cwd) 268 | output = prepend + output 269 | 270 | # update with 'src' context 271 | src = context.get('src') 272 | if src: 273 | prepend = "source %s && " % (src) 274 | output = prepend + output 275 | 276 | # update with 'sudo' context 277 | sudo = context.get('sudo') 278 | user = context.get('user') 279 | if sudo: 280 | if user != getpass.getuser(): 281 | output = "sudo su - %s -c '%s'" % (user, output) 282 | elif getpass.getuser() == 'root': 283 | output = "su - %s -c '%s'" % (user, output) 284 | else: 285 | output = "sudo %s" % (output) 286 | 287 | # if we have to ssh, prepare for the SSH command 288 | ssh_config = context.get('ssh_config') 289 | hostname = context.get('hostname') 290 | if hostname: 291 | params = { 292 | 'user': user, 293 | 'hostname': hostname, 294 | 'command': output, 295 | 'ssh_config': ' %s ' % ssh_config if ssh_config else ' ' 296 | } 297 | output = "ssh%(ssh_config)s%(user)s@%(hostname)s '%(command)s'" % (params) 298 | 299 | return output 300 | 301 | def spit(self): 302 | """ 303 | Logs to the logger the command. 304 | """ 305 | self._echo.log(str(self)) 306 | 307 | def pipe(self): 308 | """ 309 | Pipe commands in Sultan. 310 | 311 | Usage:: 312 | 313 | # runs: 'cat /var/log/foobar.log | grep 192.168.1.1' 314 | s = Sultan() 315 | s.cat("/var/log/foobar.log").pipe().grep("192.168.1.1").run() 316 | """ 317 | self._add(Pipe(self, '|')) 318 | return self 319 | 320 | def and_(self): 321 | """ 322 | Combines multiple commands using `&&`. 323 | 324 | Usage:: 325 | 326 | # runs: 'cd /tmp && touch foobar.txt' 327 | s = Sultan() 328 | s.cd("/tmp").and_().touch("foobar.txt").run() 329 | """ 330 | self._add(And(self, "&&")) 331 | return self 332 | 333 | def or_(self): 334 | """ 335 | Combines multiple commands using `||`. 336 | 337 | Usage:: 338 | 339 | # runs: 'touch /tmp/foobar || echo "Step Completed"' 340 | s = Sultan() 341 | s.touch('/tmp/foobar').or_().echo("Step Completed").run() 342 | """ 343 | self._add(Or(self, '||')) 344 | return self 345 | 346 | def stdin(self, message): 347 | 348 | return input(message) 349 | 350 | 351 | class BaseCommand(Base): 352 | """ 353 | The Base class for all commands. 354 | """ 355 | 356 | command = None 357 | args = None 358 | kwargs = None 359 | context = None 360 | 361 | def __init__(self, sultan, name, context=None): 362 | 363 | self.sultan = sultan 364 | self.command = name 365 | self.args = [] 366 | self.kwargs = {} 367 | self.context = context if context else {} 368 | 369 | 370 | class Command(BaseCommand): 371 | """ 372 | The class that all commands are based off. Essentially, when we run 373 | `Sultan().foo()`, `foo` is represented as an instance of `Command`. 374 | 375 | """ 376 | def __call__(self, *args, **kwargs): 377 | 378 | # check for 'where' in kwargs 379 | if 'where' in kwargs: 380 | where = kwargs.pop('where') 381 | if not os.path.exists(where): 382 | raise IOError("The value for 'where' (%s), for '%s' does not exist." % (where, self.command)) 383 | 384 | cmd = os.path.join(where, self.command) 385 | if not os.path.exists(cmd): 386 | raise IOError("Command '%s' does not exist in '%s'." % (cmd, where)) 387 | 388 | self.command = os.path.join(where, cmd) 389 | 390 | if "sudo" in kwargs: 391 | kwargs.pop("sudo") 392 | self.command = "sudo " + self.command 393 | 394 | self.args = [str(a) for a in args] 395 | self.kwargs = kwargs 396 | self.sultan._add(self) 397 | return self.sultan 398 | 399 | def __str__(self): 400 | 401 | args_str = (" ".join(self.args)).strip() 402 | kwargs_list = [] 403 | for k, v in self.kwargs.items(): 404 | value = v 405 | if len(k) == 1: 406 | key = "-%s" % k 407 | else: 408 | key = "--%s" % k 409 | kwargs_list.append("%s=%s" % (key, value)) 410 | kwargs_str = " ".join(kwargs_list).strip() 411 | 412 | # prep and return the output 413 | output = self.command 414 | if len(kwargs_str) > 0: 415 | output += " " + kwargs_str 416 | if len(args_str) > 0: 417 | output += " " + args_str 418 | 419 | return output 420 | 421 | 422 | class Pipe(BaseCommand): 423 | """ 424 | Representation of the Pipe `|` operator. 425 | """ 426 | def __call__(self): 427 | 428 | pass # do nothing 429 | 430 | def __str__(self): 431 | 432 | return self.command 433 | 434 | 435 | class And(BaseCommand): 436 | """ 437 | Representation of the And `&&` operator. 438 | """ 439 | def __call__(self): 440 | 441 | pass # do nothing 442 | 443 | def __str__(self): 444 | 445 | return self.command 446 | 447 | 448 | class Or(BaseCommand): 449 | """ 450 | Representation of the Or `||` operator. 451 | """ 452 | def __call__(self): 453 | 454 | pass # do nothing 455 | 456 | def __str__(self): 457 | 458 | return self.command 459 | 460 | 461 | class Redirect(BaseCommand): 462 | """ 463 | Representation of the Redirect (`>`, `>>`, ...) operator. 464 | """ 465 | def __call__(self, to_file, append=False, stdout=False, stderr=False): 466 | if stdout and stderr: 467 | descriptor = "&" 468 | else: 469 | if stdout and not stderr: 470 | descriptor = "1" 471 | elif stderr and not stdout: 472 | descriptor = "2" 473 | else: 474 | raise ValueError("You chose redirect to stdout and stderr to be false. This is not valid.") 475 | 476 | descriptor = descriptor + ">" + (">" if append else "") 477 | self.command = "%s %s" % (descriptor, to_file) 478 | self.sultan._add(self) 479 | return self.sultan 480 | 481 | def __str__(self): 482 | 483 | return self.command 484 | 485 | class Config(object): 486 | 487 | params_map = {} 488 | 489 | def __init__(self, **config): 490 | 491 | self.config = config or {} 492 | self.validate_config() 493 | 494 | def __str__(self): 495 | 496 | output = [] 497 | for key, value in self.config.items(): 498 | 499 | shorthand = self.params_map[key]['shorthand'] 500 | output.append(shorthand) 501 | output.append(str(value)) 502 | 503 | return ' '.join(output) 504 | 505 | def validate_config(self): 506 | ''' 507 | Validates the provided config to make sure all the required fields are 508 | there. 509 | ''' 510 | # first ensure that all the required fields are there 511 | for key, key_config in self.params_map.items(): 512 | if key_config['required']: 513 | if key not in self.config: 514 | raise ValueError("Invalid Configuration! Required parameter '%s' was not provided to Sultan.") 515 | 516 | # second ensure that the fields that were pased were actually fields that 517 | # can be used 518 | for key in self.config.keys(): 519 | if key not in self.params_map: 520 | raise ValueError("Invalid Configuration! The parameter '%s' provided is not used by Sultan!" % key) 521 | 522 | 523 | 524 | class SSHConfig(Config): 525 | 526 | params_map = { 527 | 'identity_file': { 528 | 'shorthand': '-i', 529 | 'required': False 530 | }, 531 | 'port': { 532 | 'shorthand': '-p', 533 | 'required': False 534 | }, 535 | } 536 | -------------------------------------------------------------------------------- /src/sultan/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .core import Base 3 | 4 | DEFAULT_SETTINGS = { 5 | "HALT_ON_ERROR": True, 6 | "LOG_FORMAT": '%(log_color)s[%(name)s]: %(message)s', 7 | "LOG_COLORS": { 8 | 'DEBUG': 'cyan', 9 | 'INFO': 'green', 10 | 'WARNING': 'yellow', 11 | 'ERROR': 'red', 12 | 'CRITICAL': 'bold_red', 13 | } 14 | } 15 | 16 | 17 | SULTAN_SETTINGS_MODULE_ENV = 'SULTAN_SETTINGS_MODULE' 18 | 19 | 20 | class Settings(Base): 21 | 22 | def __init__(self): 23 | super(Settings, self).__init__() 24 | self._settings = DEFAULT_SETTINGS.copy() 25 | self._load_setting_module() 26 | 27 | def _load_setting_module(self): 28 | if SULTAN_SETTINGS_MODULE_ENV in os.environ: 29 | settings = __import__(os.environ[SULTAN_SETTINGS_MODULE_ENV]) 30 | for k, v in settings.items(): 31 | self._settings[k] = v 32 | 33 | def __getattr__(self, attr): 34 | try: 35 | return self._settings[attr] 36 | except KeyError: 37 | raise ValueError("Invalid Setting '%s'." % (attr)) 38 | 39 | 40 | settings = Settings() 41 | -------------------------------------------------------------------------------- /src/sultan/core.py: -------------------------------------------------------------------------------- 1 | class Base(object): 2 | pass 3 | -------------------------------------------------------------------------------- /src/sultan/echo/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from sultan.core import Base 3 | from sultan.echo.colorlog import StreamHandler, ColoredFormatter 4 | from sultan.config import settings 5 | 6 | handler = StreamHandler() 7 | handler.setFormatter(ColoredFormatter( 8 | settings.LOG_FORMAT, 9 | log_colors=settings.LOG_COLORS 10 | )) 11 | 12 | 13 | def getLogger(name='', level=logging.DEBUG): 14 | logger = logging.getLogger(name) 15 | logger.addHandler(handler) 16 | logger.setLevel(level) 17 | return logger 18 | 19 | 20 | class Echo(Base): 21 | 22 | def __init__(self, activated=True): 23 | 24 | self.logger = getLogger(name='sultan') 25 | self.activated = activated 26 | 27 | def log(self, msg): 28 | 29 | if self.activated: 30 | self.logger.info(msg) 31 | 32 | def cmd(self, msg): 33 | 34 | if self.activated: 35 | self.logger.debug(msg) 36 | 37 | def stdout(self, msg): 38 | 39 | if self.activated: 40 | self.logger.info(msg) 41 | 42 | def stderr(self, msg): 43 | 44 | if self.activated: 45 | self.logger.critical(msg) 46 | 47 | def debug(self, msg): 48 | 49 | if self.activated: 50 | self.logger.debug(msg) 51 | 52 | def info(self, msg): 53 | 54 | if self.activated: 55 | self.logger.info(msg) 56 | 57 | def warn(self, msg): 58 | 59 | if self.activated: 60 | self.logger.warning(msg) 61 | 62 | def error(self, msg): 63 | 64 | if self.activated: 65 | self.logger.error(msg) 66 | 67 | def critical(self, msg): 68 | 69 | if self.activated: 70 | self.logger.critical(msg) 71 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | """ 3 | Python-Colorlog is a library created by 'borntyping', and hosted on Github at 4 | https://github.com/borntyping/python-colorlog using the MIT License. In 5 | order to keep Sultan non-dependent on other libraries, python-colorlog was 6 | copied here. 7 | 8 | Please check out Python-Colorlog for your projects! 9 | """ 10 | 11 | """A logging formatter for colored output.""" 12 | 13 | 14 | from sultan.echo.colorlog.colorlog import ( 15 | ColoredFormatter, escape_codes, default_log_colors, 16 | LevelFormatter) 17 | 18 | from sultan.echo.colorlog.logging import ( 19 | basicConfig, root, getLogger, log, 20 | debug, info, warning, error, exception, critical, StreamHandler) 21 | 22 | __all__ = ('ColoredFormatter', 'default_log_colors', 'escape_codes', 23 | 'basicConfig', 'root', 'getLogger', 'debug', 'info', 'warning', 24 | 'error', 'exception', 'critical', 'log', 'exception', 25 | 'StreamHandler', 'LevelFormatter') 26 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/colorlog.py: -------------------------------------------------------------------------------- 1 | """The ColoredFormatter class.""" 2 | 3 | from __future__ import absolute_import 4 | 5 | 6 | import logging 7 | import collections 8 | import sys 9 | 10 | from sultan.echo.colorlog.escape_codes import escape_codes, parse_colors 11 | 12 | __all__ = ('escape_codes', 'default_log_colors', 'ColoredFormatter', 13 | 'LevelFormatter') 14 | 15 | # The default colors to use for the debug levels 16 | default_log_colors = { 17 | 'DEBUG': 'white', 18 | 'INFO': 'green', 19 | 'WARNING': 'yellow', 20 | 'ERROR': 'red', 21 | 'CRITICAL': 'bold_red', 22 | } 23 | 24 | # The default format to use for each style 25 | default_formats = { 26 | '%': '%(log_color)s%(levelname)s:%(name)s:%(message)s', 27 | '{': '{log_color}{levelname}:{name}:{message}', 28 | '$': '${log_color}${levelname}:${name}:${message}' 29 | } 30 | 31 | 32 | class ColoredRecord(object): 33 | """ 34 | Wraps a LogRecord and attempts to parse missing keys as escape codes. 35 | 36 | When the record is formatted, the logging library uses ``record.__dict__`` 37 | directly - so this class replaced the dict with a ``defaultdict`` that 38 | checks if a missing key is an escape code. 39 | """ 40 | 41 | class __dict(collections.defaultdict): 42 | def __missing__(self, name): 43 | try: 44 | return parse_colors(name) 45 | except Exception: 46 | raise KeyError("{} is not a valid record attribute " 47 | "or color sequence".format(name)) 48 | 49 | def __init__(self, record): 50 | # Replace the internal dict with one that can handle missing keys 51 | self.__dict__ = self.__dict() 52 | self.__dict__.update(record.__dict__) 53 | 54 | # Keep a refrence to the original refrence so ``__getattr__`` can 55 | # access functions that are not in ``__dict__`` 56 | self.__record = record 57 | 58 | def __getattr__(self, name): 59 | return getattr(self.__record, name) 60 | 61 | 62 | class ColoredFormatter(logging.Formatter): 63 | """ 64 | A formatter that allows colors to be placed in the format string. 65 | 66 | Intended to help in creating more readable logging output. 67 | """ 68 | 69 | def __init__(self, fmt=None, datefmt=None, style='%', 70 | log_colors=None, reset=True, 71 | secondary_log_colors=None): 72 | """ 73 | Set the format and colors the ColoredFormatter will use. 74 | 75 | The ``fmt``, ``datefmt`` and ``style`` args are passed on to the 76 | ``logging.Formatter`` constructor. 77 | 78 | The ``secondary_log_colors`` argument can be used to create additional 79 | ``log_color`` attributes. Each key in the dictionary will set 80 | ``{key}_log_color``, using the value to select from a different 81 | ``log_colors`` set. 82 | 83 | :Parameters: 84 | - fmt (str): The format string to use 85 | - datefmt (str): A format string for the date 86 | - log_colors (dict): 87 | A mapping of log level names to color names 88 | - reset (bool): 89 | Implictly append a color reset to all records unless False 90 | - style ('%' or '{' or '$'): 91 | The format style to use. (*No meaning prior to Python 3.2.*) 92 | - secondary_log_colors (dict): 93 | Map secondary ``log_color`` attributes. (*New in version 2.6.*) 94 | """ 95 | if fmt is None: 96 | if sys.version_info > (3, 2): 97 | fmt = default_formats[style] 98 | else: 99 | fmt = default_formats['%'] 100 | 101 | if sys.version_info > (3, 2): 102 | super(ColoredFormatter, self).__init__(fmt, datefmt, style) 103 | elif sys.version_info > (2, 7): 104 | super(ColoredFormatter, self).__init__(fmt, datefmt) 105 | else: 106 | logging.Formatter.__init__(self, fmt, datefmt) 107 | 108 | self.log_colors = ( 109 | log_colors if log_colors is not None else default_log_colors) 110 | self.secondary_log_colors = secondary_log_colors 111 | self.reset = reset 112 | 113 | def color(self, log_colors, name): 114 | """Return escape codes from a ``log_colors`` dict.""" 115 | return parse_colors(log_colors.get(name, "")) 116 | 117 | def format(self, record): 118 | """Format a message from a record object.""" 119 | record = ColoredRecord(record) 120 | record.log_color = self.color(self.log_colors, record.levelname) 121 | 122 | # Set secondary log colors 123 | if self.secondary_log_colors: 124 | for name, log_colors in list(self.secondary_log_colors.items()): 125 | color = self.color(log_colors, record.levelname) 126 | setattr(record, name + '_log_color', color) 127 | 128 | # Format the message 129 | if sys.version_info > (2, 7): 130 | message = super(ColoredFormatter, self).format(record) 131 | else: 132 | message = logging.Formatter.format(self, record) 133 | 134 | # Add a reset code to the end of the message 135 | # (if it wasn't explicitly added in format str) 136 | if self.reset and not message.endswith(escape_codes['reset']): 137 | message += escape_codes['reset'] 138 | 139 | return message 140 | 141 | 142 | class LevelFormatter(ColoredFormatter): 143 | """An extension of ColoredFormatter that uses per-level format strings.""" 144 | 145 | def __init__(self, fmt=None, datefmt=None, style='%', 146 | log_colors=None, reset=True, 147 | secondary_log_colors=None): 148 | """ 149 | Set the per-loglevel format that will be used. 150 | 151 | Supports fmt as a dict. All other args are passed on to the 152 | ``colorlog.ColoredFormatter`` constructor. 153 | 154 | :Parameters: 155 | - fmt (dict): 156 | A mapping of log levels (represented as strings, e.g. 'WARNING') to 157 | different formatters. (*New in version 2.7.0) 158 | (All other parameters are the same as in colorlog.ColoredFormatter) 159 | 160 | Example: 161 | 162 | formatter = colorlog.LevelFormatter(fmt={ 163 | 'DEBUG':'%(log_color)s%(msg)s (%(module)s:%(lineno)d)', 164 | 'INFO': '%(log_color)s%(msg)s', 165 | 'WARNING': '%(log_color)sWARN: %(msg)s (%(module)s:%(lineno)d)', 166 | 'ERROR': '%(log_color)sERROR: %(msg)s (%(module)s:%(lineno)d)', 167 | 'CRITICAL': '%(log_color)sCRIT: %(msg)s (%(module)s:%(lineno)d)', 168 | }) 169 | """ 170 | if sys.version_info > (2, 7): 171 | super(LevelFormatter, self).__init__( 172 | fmt=fmt, datefmt=datefmt, style=style, log_colors=log_colors, 173 | reset=reset, secondary_log_colors=secondary_log_colors) 174 | else: 175 | ColoredFormatter.__init__( 176 | self, fmt=fmt, datefmt=datefmt, style=style, 177 | log_colors=log_colors, reset=reset, 178 | secondary_log_colors=secondary_log_colors) 179 | self.style = style 180 | self.fmt = fmt 181 | 182 | def format(self, record): 183 | """Customize the message format based on the log level.""" 184 | if isinstance(self.fmt, dict): 185 | self._fmt = self.fmt[record.levelname] 186 | if sys.version_info > (3, 2): 187 | # Update self._style because we've changed self._fmt 188 | # (code based on stdlib's logging.Formatter.__init__()) 189 | if self.style not in logging._STYLES: 190 | raise ValueError('Style must be one of: %s' % ','.join( 191 | list(logging._STYLES.keys()))) 192 | self._style = logging._STYLES[self.style][0](self._fmt) 193 | 194 | if sys.version_info > (2, 7): 195 | message = super(LevelFormatter, self).format(record) 196 | else: 197 | message = ColoredFormatter.format(self, record) 198 | 199 | return message 200 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/escape_codes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generates a dictionary of ANSI escape codes. 3 | 4 | http://en.wikipedia.org/wiki/ANSI_escape_code 5 | 6 | Uses colorama as an optional dependancy to support color on Windows 7 | """ 8 | 9 | try: 10 | import colorama 11 | except ImportError: 12 | pass 13 | else: 14 | colorama.init() 15 | 16 | __all__ = ('escape_codes', 'parse_colors') 17 | 18 | 19 | # Returns escape codes from format codes 20 | def esc(*x): 21 | return '\033[' + ';'.join(x) + 'm' 22 | 23 | # The initial list of escape codes 24 | escape_codes = { 25 | 'reset': esc('0'), 26 | 'bold': esc('01'), 27 | } 28 | 29 | # The color names 30 | COLORS = [ 31 | 'black', 32 | 'red', 33 | 'green', 34 | 'yellow', 35 | 'blue', 36 | 'purple', 37 | 'cyan', 38 | 'white' 39 | ] 40 | 41 | PREFIXES = [ 42 | # Foreground without prefix 43 | ('3', ''), ('01;3', 'bold_'), 44 | 45 | # Foreground with fg_ prefix 46 | ('3', 'fg_'), ('01;3', 'fg_bold_'), 47 | 48 | # Background with bg_ prefix - bold/light works differently 49 | ('4', 'bg_'), ('10', 'bg_bold_'), 50 | ] 51 | 52 | for prefix, prefix_name in PREFIXES: 53 | for code, name in enumerate(COLORS): 54 | escape_codes[prefix_name + name] = esc(prefix + str(code)) 55 | 56 | 57 | def parse_colors(sequence): 58 | """Return escape codes from a color sequence.""" 59 | return ''.join(escape_codes[n] for n in sequence.split(',') if n) 60 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/logging.py: -------------------------------------------------------------------------------- 1 | """Wrappers around the logging module.""" 2 | 3 | from __future__ import absolute_import 4 | 5 | import functools 6 | import logging 7 | 8 | from sultan.echo.colorlog.colorlog import ColoredFormatter 9 | 10 | 11 | BASIC_FORMAT = "%(log_color)s%(levelname)s%(reset)s:%(name)s:%(message)s" 12 | 13 | 14 | def basicConfig(**kwargs): 15 | """Call ``logging.basicConfig`` and override the formatter it creates.""" 16 | logging.basicConfig(**kwargs) 17 | logging._acquireLock() 18 | try: 19 | stream = logging.root.handlers[0] 20 | stream.setFormatter( 21 | ColoredFormatter( 22 | fmt=kwargs.get('format', BASIC_FORMAT), 23 | datefmt=kwargs.get('datefmt', None))) 24 | finally: 25 | logging._releaseLock() 26 | 27 | 28 | def ensure_configured(func): 29 | """Modify a function to call ``basicConfig`` first if no handlers exist.""" 30 | @functools.wraps(func) 31 | def wrapper(*args, **kwargs): 32 | if len(logging.root.handlers) == 0: 33 | basicConfig() 34 | return func(*args, **kwargs) 35 | return wrapper 36 | 37 | root = logging.root 38 | getLogger = logging.getLogger 39 | debug = ensure_configured(logging.debug) 40 | info = ensure_configured(logging.info) 41 | warning = ensure_configured(logging.warning) 42 | error = ensure_configured(logging.error) 43 | critical = ensure_configured(logging.critical) 44 | log = ensure_configured(logging.log) 45 | exception = ensure_configured(logging.exception) 46 | 47 | StreamHandler = logging.StreamHandler 48 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/conftest.py: -------------------------------------------------------------------------------- 1 | """Fixtures that can be used in other tests.""" 2 | 3 | from __future__ import absolute_import 4 | 5 | 6 | import inspect 7 | import logging 8 | import sys 9 | 10 | import colorlog 11 | 12 | import pytest 13 | 14 | 15 | def assert_log_message(log_function, message, capsys): 16 | """Call a log function and check the message has been output.""" 17 | log_function(message) 18 | out, err = capsys.readouterr() 19 | # Print the output so that py.test shows it when a test fails 20 | print(err, end='', file=sys.stderr) 21 | # Assert the message send to the logger was output 22 | assert message in err, 'Log message not output to STDERR' 23 | return err 24 | 25 | 26 | @pytest.fixture() 27 | def reset_loggers(): 28 | logging.root.handlers = list() 29 | logging.root.setLevel(logging.DEBUG) 30 | 31 | 32 | @pytest.fixture() 33 | def test_logger(reset_loggers, capsys): 34 | def function(logger, validator=None): 35 | lines = [ 36 | assert_log_message(logger.debug, 'a debug message', capsys), 37 | assert_log_message(logger.info, 'an info message', capsys), 38 | assert_log_message(logger.warning, 'a warning message', capsys), 39 | assert_log_message(logger.error, 'an error message', capsys), 40 | assert_log_message(logger.critical, 'a critical message', capsys) 41 | ] 42 | 43 | if validator is not None: 44 | for line in lines: 45 | valid = validator(line.strip()) 46 | assert valid, "{!r} did not validate".format(line.strip()) 47 | 48 | return lines 49 | return function 50 | 51 | 52 | @pytest.fixture() 53 | def create_and_test_logger(test_logger): 54 | def function(*args, **kwargs): 55 | validator = kwargs.pop('validator', None) 56 | formatter = colorlog.ColoredFormatter(*args, **kwargs) 57 | 58 | stream = logging.StreamHandler() 59 | stream.setLevel(logging.DEBUG) 60 | stream.setFormatter(formatter) 61 | 62 | logger = logging.getLogger(inspect.stack()[1][3]) 63 | logger.setLevel(logging.DEBUG) 64 | logger.addHandler(stream) 65 | 66 | return test_logger(logger, validator) 67 | return function 68 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/test_colorlog.py: -------------------------------------------------------------------------------- 1 | """Test the colorlog.colorlog module.""" 2 | 3 | import sys 4 | 5 | import pytest 6 | 7 | 8 | def test_colored_formatter(create_and_test_logger): 9 | create_and_test_logger() 10 | 11 | 12 | def test_custom_colors(create_and_test_logger): 13 | """Disable all colors and check no escape codes are output.""" 14 | create_and_test_logger( 15 | log_colors={}, reset=False, 16 | validator=lambda line: '\x1b[' not in line) 17 | 18 | 19 | def test_reset(create_and_test_logger): 20 | create_and_test_logger( 21 | reset=True, validator=lambda l: l.endswith('\x1b[0m')) 22 | 23 | 24 | def test_no_reset(create_and_test_logger): 25 | create_and_test_logger( 26 | fmt="%(reset)s%(log_color)s%(levelname)s:%(name)s:%(message)s", 27 | reset=False, 28 | # Check that each line does not end with an escape code 29 | validator=lambda line: not line.endswith('\x1b[0m')) 30 | 31 | 32 | def test_secondary_colors(create_and_test_logger): 33 | expected = ':\x1b[31mtest_secondary_colors:\x1b[34m' 34 | create_and_test_logger( 35 | fmt=( 36 | "%(log_color)s%(levelname)s:" 37 | "%(name_log_color)s%(name)s:" 38 | "%(message_log_color)s%(message)s" 39 | ), 40 | secondary_log_colors={ 41 | 'name': { 42 | 'DEBUG': 'red', 43 | 'INFO': 'red', 44 | 'WARNING': 'red', 45 | 'ERROR': 'red', 46 | 'CRITICAL': 'red', 47 | }, 48 | 'message': { 49 | 'DEBUG': 'blue', 50 | 'INFO': 'blue', 51 | 'WARNING': 'blue', 52 | 'ERROR': 'blue', 53 | 'CRITICAL': 'blue', 54 | } 55 | }, 56 | validator=lambda line: expected in line) 57 | 58 | 59 | def test_some_secondary_colors(create_and_test_logger): 60 | lines = create_and_test_logger( 61 | fmt="%(message_log_color)s%(message)s", 62 | secondary_log_colors={ 63 | 'message': { 64 | 'ERROR': 'red', 65 | 'CRITICAL': 'red' 66 | } 67 | }) 68 | # Check that only two lines are colored 69 | assert len([l for l in lines if '\x1b[31m' in l]) == 2 70 | 71 | 72 | @pytest.mark.skipif(sys.version_info < (3, 2), reason="requires python3.2") 73 | def test_braces_style(create_and_test_logger): 74 | create_and_test_logger( 75 | fmt='{log_color}{levelname}:{name}:{message}', style='{') 76 | 77 | 78 | @pytest.mark.skipif(sys.version_info < (3, 2), reason="requires python3.2") 79 | def test_template_style(create_and_test_logger): 80 | create_and_test_logger( 81 | fmt='${log_color}${levelname}:${name}:${message}', style='$') 82 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/test_config.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [logger_root] 5 | handlers=stream 6 | level=DEBUG 7 | 8 | [formatters] 9 | keys=color 10 | 11 | [formatter_color] 12 | class=colorlog.ColoredFormatter 13 | format=%(log_color)s%(levelname)s:%(name)s:%(message)s:test_config.ini 14 | datefmt=%H:%M:%S 15 | 16 | [handlers] 17 | keys=stream 18 | 19 | [handler_stream] 20 | class=StreamHandler 21 | formatter=color 22 | args=() 23 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/test_config.py: -------------------------------------------------------------------------------- 1 | """Test using colorlog with logging.config""" 2 | 3 | import logging 4 | import logging.config 5 | import os.path 6 | import sys 7 | 8 | import pytest 9 | 10 | 11 | def path(filename): 12 | """Return an absolute path to a file in the current directory.""" 13 | return os.path.join(os.path.dirname(os.path.realpath(__file__)), filename) 14 | 15 | 16 | def test_build_from_file(test_logger): 17 | logging.config.fileConfig(path("test_config.ini")) 18 | test_logger(logging.getLogger(), lambda l: ':test_config.ini' in l) 19 | 20 | 21 | @pytest.mark.skipif(sys.version_info < (2, 7), reason="requires python2.7") 22 | def test_build_from_dictionary(test_logger): 23 | logging.config.dictConfig({ 24 | 'version': 1, 25 | 'formatters': { 26 | 'colored': { 27 | '()': 'colorlog.ColoredFormatter', 28 | 'format': 29 | "%(log_color)s%(levelname)s:%(name)s:%(message)s:dict", 30 | } 31 | }, 32 | 'handlers': { 33 | 'stream': { 34 | 'class': 'logging.StreamHandler', 35 | 'formatter': 'colored', 36 | 'level': 'DEBUG' 37 | }, 38 | }, 39 | 'loggers': { 40 | '': { 41 | 'handlers': ['stream'], 42 | 'level': 'DEBUG', 43 | }, 44 | }, 45 | }) 46 | test_logger(logging.getLogger(), lambda l: ':dict' in l) 47 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/test_escape_codes.py: -------------------------------------------------------------------------------- 1 | """Test the colorlog.escape_codes module.""" 2 | 3 | from colorlog.escape_codes import escape_codes, esc, parse_colors 4 | 5 | import pytest 6 | 7 | 8 | def test_esc(): 9 | assert esc('1', '2', '3') == '\033[1;2;3m' 10 | 11 | 12 | def test_reset(): 13 | assert escape_codes['reset'] == '\033[0m' 14 | 15 | 16 | def test_bold_color(): 17 | assert escape_codes['bold_red'] == '\033[01;31m' 18 | 19 | 20 | def test_fg_color(): 21 | assert escape_codes['fg_bold_yellow'] == '\033[01;33m' 22 | 23 | 24 | def test_bg_color(): 25 | assert escape_codes['bg_bold_blue'] == '\033[104m' 26 | 27 | 28 | def test_rainbow(create_and_test_logger): 29 | """Test *all* escape codes, useful to ensure backwards compatibility.""" 30 | create_and_test_logger( 31 | "%(log_color)s%(levelname)s%(reset)s:%(bold_black)s%(name)s:" 32 | "%(message)s%(reset)s:" 33 | 34 | "%(bold_red)sr%(red)sa%(yellow)si%(green)sn%(bold_blue)sb" 35 | "%(blue)so%(purple)sw%(reset)s " 36 | 37 | "%(fg_bold_red)sr%(fg_red)sa%(fg_yellow)si%(fg_green)sn" 38 | "%(fg_bold_blue)sb%(fg_blue)so%(fg_purple)sw%(reset)s " 39 | 40 | "%(bg_red)sr%(bg_bold_red)sa%(bg_yellow)si%(bg_green)sn" 41 | "%(bg_bold_blue)sb%(bg_blue)so%(bg_purple)sw%(reset)s " 42 | ) 43 | 44 | 45 | def test_parse_colors(): 46 | assert parse_colors('reset') == '\033[0m' 47 | 48 | 49 | def test_parse_multiple_colors(): 50 | assert parse_colors('bold_red,bg_bold_blue') == '\033[01;31m\033[104m' 51 | 52 | 53 | def test_parse_invalid_colors(): 54 | with pytest.raises(KeyError): 55 | parse_colors('false') 56 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/test_example.py: -------------------------------------------------------------------------------- 1 | def test_example(): 2 | """Tests the usage example from the README""" 3 | import colorlog 4 | 5 | handler = colorlog.StreamHandler() 6 | handler.setFormatter(colorlog.ColoredFormatter( 7 | '%(log_color)s%(levelname)s:%(name)s:%(message)s')) 8 | logger = colorlog.getLogger('example') 9 | logger.addHandler(handler) 10 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/test_exports.py: -------------------------------------------------------------------------------- 1 | """Test that `from colorlog import *` works correctly.""" 2 | 3 | from colorlog import * # noqa 4 | 5 | 6 | def test_exports(): 7 | assert set(( 8 | 'ColoredFormatter', 'default_log_colors', 'escape_codes', 9 | 'basicConfig', 'root', 'getLogger', 'debug', 'info', 'warning', 10 | 'error', 'exception', 'critical', 'log', 'exception' 11 | )) < set(globals()) 12 | -------------------------------------------------------------------------------- /src/sultan/echo/colorlog/notests/test_logging.py: -------------------------------------------------------------------------------- 1 | """Test the colorlog.logging module.""" 2 | 3 | import logging 4 | 5 | import colorlog 6 | 7 | 8 | def test_logging_module(test_logger): 9 | test_logger(logging) 10 | 11 | 12 | def test_colorlog_module(test_logger): 13 | test_logger(colorlog) 14 | 15 | 16 | def test_colorlog_basicConfig(test_logger): 17 | colorlog.basicConfig() 18 | test_logger(colorlog.getLogger()) 19 | -------------------------------------------------------------------------------- /src/sultan/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidContextError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /src/sultan/result.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import time 4 | import traceback 5 | 6 | from queue import Queue 7 | from sultan.core import Base 8 | from sultan.echo import Echo 9 | from threading import Thread 10 | 11 | 12 | class Result(Base): 13 | """ 14 | Class that encompasses the result of a POpen command. 15 | """ 16 | 17 | def __init__(self, process, commands, context, streaming=False, exception=None, halt_on_nonzero=False): 18 | super(Result, self).__init__() 19 | self._process = process 20 | self._commands = commands 21 | self._context = context 22 | self._exception = exception 23 | self.__echo = Echo() 24 | self._streaming = streaming 25 | self.rc = None 26 | self._halt_on_nonzero=halt_on_nonzero 27 | 28 | if process and streaming: 29 | self.is_complete = False 30 | self.__stdout = Queue() 31 | self.__stderr = Queue() 32 | self.__stdin = Queue() 33 | 34 | self._stdout_t = Thread(target=self.read_output, args=(process.stdout, self.__stdout)) 35 | self._stderr_t = Thread(target=self.read_output, args=(process.stderr, self.__stderr)) 36 | self._stdin_t = Thread(target=self.write_input) 37 | self._wait_t = Thread(target=self.wait_on_process) 38 | 39 | for t in (self._stdout_t, self._stderr_t, self._stdin_t, self._wait_t): 40 | t.daemon = True 41 | t.start() 42 | 43 | else: 44 | self.is_complete = True 45 | try: 46 | stdout, stderr = process.communicate() 47 | except: 48 | stdout, stderr = None, None 49 | 50 | try: 51 | self.rc = process.returncode 52 | except: 53 | pass 54 | 55 | self.__stdout = stdout.strip().splitlines() if stdout else [] 56 | self.__stderr = stderr.strip().splitlines() if stderr else [] 57 | 58 | if self._halt_on_nonzero and self.rc != 0: 59 | self.dump_exception() 60 | 61 | 62 | def read_output(self, pipe, q): 63 | for line in iter(pipe.readline, b''): 64 | if line: 65 | q.put(line.strip()) 66 | elif self.is_complete: 67 | break 68 | else: 69 | time.sleep(0.1) 70 | pipe.close() 71 | 72 | 73 | def write_input(self): 74 | for line in iter(self.__stdin.get, None): 75 | if line.endswith("\n"): 76 | self._process.stdin.write(line) 77 | else: 78 | self._process.stdin.write(line + "\n") 79 | 80 | 81 | def wait_on_process(self): 82 | self.rc = self._process.wait() 83 | self.__stdin.put(None) 84 | self.is_complete = True 85 | for t in (self._stdout_t, self._stderr_t, self._stdin_t): 86 | t.join() 87 | if self._halt_on_nonzero and self.rc != 0: 88 | self.dump_exception() 89 | sys.exit() 90 | 91 | 92 | def dump_exception(self): 93 | if not self._exception: 94 | try: 95 | raise subprocess.CalledProcessError(self.rc, ''.join(self._commands), self.stderr) 96 | except subprocess.CalledProcessError as e: 97 | self._exception = e 98 | 99 | self.__echo.critical("Unable to run '%s'" % self._commands) 100 | 101 | # traceback 102 | self.print_traceback() 103 | 104 | # standard out 105 | self.print_stdout() 106 | 107 | # standard error 108 | self.print_stderr() 109 | 110 | # print debug information 111 | self.__display_exception_debug_information() 112 | 113 | if self._halt_on_nonzero: 114 | raise self._exception 115 | 116 | def __display_exception_debug_information(self): 117 | 118 | def echo_debug_info(key): 119 | if self._context and len(self._context) > 0: 120 | self.__echo.warn("\t - %s: %s" % (key, self._context[0].get(key, 'N/A'))) 121 | 122 | self.__echo.warn("The following are additional information that can be used to debug this exception.") 123 | self.__echo.warn("The following is the context used to run:") 124 | echo_debug_info('cwd') 125 | echo_debug_info('sudo') 126 | echo_debug_info('user') 127 | echo_debug_info('hostname') 128 | echo_debug_info('env') 129 | echo_debug_info('logging') 130 | echo_debug_info('executable') 131 | echo_debug_info('ssh_config') 132 | echo_debug_info('src') 133 | 134 | def __str__(self): 135 | return '\n'.join(self.stdout) 136 | 137 | def __format_line(self, msg): 138 | 139 | return '| %s' % msg 140 | 141 | def __format_lines_error(self, lines): 142 | 143 | for line in lines: 144 | self.__echo.critical(self.__format_line(line)) 145 | 146 | def __format_lines_info(self, lines): 147 | 148 | for line in lines: 149 | self.__echo.info(self.__format_line(line)) 150 | 151 | @property 152 | def stdout(self): 153 | """ 154 | Converts stdout string to a list. 155 | """ 156 | if self._streaming: 157 | stdout = [] 158 | while not self.__stdout.empty(): 159 | try: 160 | line = self.__stdout.get_nowait() 161 | stdout.append(line) 162 | except: 163 | pass 164 | else: 165 | stdout = self.__stdout 166 | return stdout 167 | 168 | @property 169 | def stderr(self): 170 | """ 171 | Converts stderr string to a list. 172 | """ 173 | if self._streaming: 174 | stderr = [] 175 | while not self.__stderr.empty(): 176 | try: 177 | line = self.__stderr.get_nowait() 178 | stderr.append(line) 179 | except: 180 | pass 181 | else: 182 | stderr = self.__stderr 183 | return stderr 184 | 185 | def stdin(self, line): 186 | """ 187 | Sends input to stdin. 188 | """ 189 | if self._streaming: 190 | self.__stdin.put(line) 191 | 192 | @property 193 | def traceback(self): 194 | """ 195 | Converts traceback string to a list. 196 | """ 197 | if self._exception: 198 | return traceback.format_exc().split("\n") 199 | else: 200 | return [] 201 | 202 | @property 203 | def is_success(self): 204 | """ 205 | Returns if the result of the command was a success. 206 | True for success, False for failure. 207 | """ 208 | return self.is_complete and self.rc == 0 209 | 210 | @property 211 | def is_failure(self): 212 | """ 213 | Returns if the result of the command was a failure. 214 | True for failure, False for succes. 215 | """ 216 | return self.is_complete and not self.rc == 0 217 | 218 | @property 219 | def has_exception(self): 220 | ''' 221 | Returns True if self._exception is not empty. 222 | ''' 223 | return bool(self._exception) 224 | 225 | def print_stdout(self, always_print=False): 226 | """ 227 | Prints the stdout to console - if there is any stdout, otherwise does nothing. 228 | :param always_print: print the stdout, even if there is nothing in the buffer (default: false) 229 | """ 230 | if self.__stdout or always_print: 231 | self.__echo.info("--{ STDOUT }---" + "-" * 100) 232 | self.__format_lines_info(self.stdout) 233 | self.__echo.info("---------------" + "-" * 100) 234 | 235 | def print_stderr(self, always_print=False): 236 | """ 237 | Prints the stderr to console - if there is any stdout, otherwise does nothing. 238 | :param always_print: print the stderr, even if there is nothing in the buffer (default: false) 239 | """ 240 | if self.__stderr or always_print: 241 | self.__echo.critical("--{ STDERR }---" + "-" * 100) 242 | self.__format_lines_error(self.stderr) 243 | self.__echo.critical("---------------" + "-" * 100) 244 | 245 | def print_traceback(self, always_print=False): 246 | """ 247 | Prints the traceback to console - if there is any traceback, otherwise does nothing. 248 | :param always_print: print the traceback, even if there is nothing in the buffer (default: false) 249 | """ 250 | if self._exception or always_print: 251 | self.__echo.critical("--{ TRACEBACK }" + "-" * 100) 252 | self.__format_lines_error(self.traceback) 253 | self.__echo.critical("---------------" + "-" * 100) 254 | -------------------------------------------------------------------------------- /test/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/test/integration/__init__.py -------------------------------------------------------------------------------- /test/integration/sultan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/test/integration/sultan/__init__.py -------------------------------------------------------------------------------- /test/integration/sultan/test_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import stat 4 | import tempfile 5 | import unittest 6 | from sultan.api import Sultan 7 | 8 | class SultanCreateFileTestCase(unittest.TestCase): 9 | """ 10 | Creates a test script, ensures that the contents are valid and attempts to 11 | run it, and check that it is valid. 12 | """ 13 | 14 | def setUp(self): 15 | 16 | self.f, self.path = tempfile.mkstemp() 17 | 18 | def test_create_script(self): 19 | 20 | s = Sultan() 21 | s.clear() 22 | self.assertEqual( 23 | str(s.echo("'ls -lah /tmp'").redirect(self.path, stdout=True)), 24 | "echo 'ls -lah /tmp' 1> %s;" % self.path) 25 | 26 | s.run() 27 | with open(self.path) as f: 28 | self.assertEqual(f.read(), 'ls -lah /tmp\n') 29 | 30 | class SultanEnvironment(unittest.TestCase): 31 | """ 32 | Tests if Sultan can set environment variables. 33 | """ 34 | 35 | def test_environment_setup(self): 36 | 37 | with Sultan.load(env={ 'FOOBAR': '/tmp' }) as s: 38 | 39 | response = s.env().run() 40 | self.assertIn('FOOBAR=/tmp', response.stdout) 41 | 42 | 43 | class SultanExecutable(unittest.TestCase): 44 | """ 45 | Tests for the executable being used 46 | """ 47 | 48 | def test_default_executable(self): 49 | 50 | with Sultan.load() as sultan: 51 | result = sultan.ps().pipe().grep('`echo $$`').pipe().awk("'{ print $4 }'").run() 52 | self.assertEqual(result.stdout[0], 'sh') 53 | 54 | def test_custom_executable(self): 55 | 56 | with Sultan.load(executable='/bin/bash') as sultan: 57 | result = sultan.ps().pipe().grep('`echo $$`').pipe().awk("'{ print $4 }'").run() 58 | self.assertEqual(result.stdout[0], 'bash') 59 | 60 | def test_nonexistent_executable(self): 61 | with self.assertRaises(IOError): 62 | with Sultan.load(executable='no_such_exe_cause_sultan') as sultan: 63 | sultan.ls().run() 64 | 65 | 66 | class SultanRunCustomScripts(unittest.TestCase): 67 | """ 68 | Run a custom script that we create 69 | """ 70 | 71 | def setUp(self): 72 | 73 | content = """ 74 | #!/usr/bin/env bash 75 | 76 | OUTPUT_FILENAME=/tmp/SultanRunScript/lorum.txt 77 | mkdir -p /tmp/SultanRunScript 78 | echo 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n' > $OUTPUT_FILENAME 79 | echo 'Nunc in enim dictum, consectetur ex vehicula, fermentum orci.\n' > $OUTPUT_FILENAME 80 | echo 'Donec sapien turpis, mattis vel urna sed, iaculis aliquam purus.\n' > $OUTPUT_FILENAME 81 | """ 82 | self.dir_path = '/tmp/SultanRunScript' 83 | self.script_filepath = '/tmp/SultanRunScript/myscript' 84 | self.output_filepath = '/tmp/SultanRunScript/lorum.txt' 85 | if os.path.exists(self.dir_path): shutil.rmtree(self.dir_path) 86 | 87 | s = Sultan() 88 | os.mkdir(self.dir_path) 89 | with open(self.script_filepath, 'w') as f: 90 | f.write(content) 91 | st = os.stat(self.script_filepath) 92 | os.chmod(self.script_filepath, st.st_mode | stat.S_IEXEC) 93 | 94 | 95 | def test_run_custom_script(self): 96 | try: 97 | s = Sultan() 98 | response = s.myscript(where=self.dir_path).run() 99 | self.assertEqual(len(response.stdout), 0) 100 | 101 | response = s.cat(self.output_filepath).run() 102 | self.assertEqual(response.stdout, ['Donec sapien turpis, mattis vel urna sed, iaculis aliquam purus.']) 103 | finally: 104 | shutil.rmtree(self.dir_path) 105 | 106 | 107 | class SultanReturnCode(unittest.TestCase): 108 | """ 109 | Checks on the rc (return code). 110 | """ 111 | 112 | def test_zero_rc(self): 113 | with Sultan.load() as s: 114 | response = s.ls().run() 115 | self.assertEqual(response.rc, 0) 116 | 117 | def test_non_zero_rc(self): 118 | with Sultan.load() as s: 119 | response = s.exit(22).run(halt_on_nonzero=False) 120 | self.assertEqual(response.rc, 22) 121 | -------------------------------------------------------------------------------- /test/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/test/unit/__init__.py -------------------------------------------------------------------------------- /test/unit/sultan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davydany/sultan/65b4271a161d6c19a9eb0170b5a95832a139ab7f/test/unit/sultan/__init__.py -------------------------------------------------------------------------------- /test/unit/sultan/test_api.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import os 3 | import shutil 4 | import tempfile 5 | import unittest 6 | import getpass 7 | 8 | from sultan.api import And, Or, Command, Pipe, Redirect, Sultan, SSHConfig 9 | from sultan.config import Settings 10 | from sultan.exceptions import InvalidContextError 11 | 12 | 13 | class SultanTestCase(unittest.TestCase): 14 | 15 | def test_construction(self): 16 | 17 | sultan = Sultan() 18 | self.assertEqual(sultan.commands, []) 19 | self.assertTrue(isinstance(sultan.settings, Settings)) 20 | 21 | def test_getattr(self): 22 | 23 | sultan = Sultan() 24 | self.assertTrue(isinstance(sultan.redirect, Redirect)) 25 | self.assertTrue(isinstance(sultan.foobar, Command)) 26 | 27 | @mock.patch("sultan.api.subprocess") 28 | def test_run_basic(self, m_subprocess): 29 | m_subprocess.Popen = mock.Mock() 30 | m_subprocess.Popen().communicate.return_value = ("sample_response", "") 31 | m_subprocess.Popen().returncode = 0 32 | sultan = Sultan() 33 | response = sultan.ls("-lah /tmp").run() 34 | self.assertTrue(m_subprocess.Popen().communicate.called) 35 | self.assertEqual(response.stdout, ["sample_response"]) 36 | 37 | def test_run_advanced(self): 38 | 39 | sultan = Sultan() 40 | try: 41 | sultan.mkdir("-p /tmp/mytestdir")\ 42 | .mkdir("-p /tmp/mytestdir/foobar")\ 43 | .touch("/tmp/mytestdir/a")\ 44 | .touch("/tmp/mytestdir/b")\ 45 | .run() 46 | 47 | response = sultan.ls("-1 /tmp/mytestdir/").run() 48 | self.assertEqual(response.stdout, ['a', 'b', 'foobar']) 49 | finally: 50 | if os.path.exists('/tmp/mytestdir'): 51 | shutil.rmtree('/tmp/mytestdir') 52 | 53 | @mock.patch('sultan.api.subprocess') 54 | def test_run_halt_on_nonzero(self, m_subprocess): 55 | 56 | m_subprocess.Popen.side_effect = OSError(1, "foobar") 57 | s = Sultan() 58 | with self.assertRaises(OSError): 59 | s.foobar("-qux").run() 60 | 61 | try: 62 | s.foobar("-qux").run(halt_on_nonzero=False) 63 | except Exception: 64 | self.fail( 65 | "s.foobar('-qux').run(halt_on_nonzero=False) should not raise any errors.") 66 | 67 | def test_basic_command_chains(self): 68 | 69 | sultan = Sultan() 70 | self.assertEqual(str(sultan.touch( 71 | "/tmp/foo").ls("-1 /tmp/foo").whoami()), "touch /tmp/foo; ls -1 /tmp/foo; whoami;") 72 | 73 | def test_command_generation(self): 74 | 75 | sultan = Sultan() 76 | self.assertEqual(str(sultan.yum("install", "gcc")), "yum install gcc;") 77 | 78 | sultan = Sultan() 79 | self.assertEqual( 80 | str(sultan.yum("install", "-y", "gcc")), "yum install -y gcc;") 81 | 82 | sultan = Sultan() 83 | self.assertEqual(str(sultan.yum("install -y gcc")), 84 | "yum install -y gcc;") 85 | 86 | def test_command_generation_for_chains(self): 87 | 88 | sultan = Sultan() 89 | self.assertEqual(str(sultan.touch( 90 | "/tmp/foo").and_().touch("/tmp/bar")), "touch /tmp/foo && touch /tmp/bar;") 91 | 92 | sultan = Sultan() 93 | self.assertEqual( 94 | str(sultan.yum( 95 | "install -y gcc").and_().ls("-lah /tmp").and_().find("/ -name gcc")), 96 | "yum install -y gcc && ls -lah /tmp && find / -name gcc;" 97 | ) 98 | 99 | def test_execution(self): 100 | 101 | sultan = Sultan() 102 | sultan.touch("/tmp/foo").run() 103 | response = sultan.ls("-1 /tmp/foo").run() 104 | self.assertEqual(response.stdout, ["/tmp/foo"]) 105 | 106 | def test_and(self): 107 | 108 | sultan = Sultan() 109 | self.assertEqual(str(sultan.touch( 110 | "/tmp/foo").and_().touch("/tmp/bar")), "touch /tmp/foo && touch /tmp/bar;") 111 | 112 | def test_or(self): 113 | 114 | sultan = Sultan() 115 | self.assertEqual(str(sultan.touch('/tmp/foobar').or_().echo('"Step Completed"')), 116 | "touch /tmp/foobar || echo \"Step Completed\";") 117 | 118 | @mock.patch('sultan.api.input') 119 | def test_stdin(self, mock_input): 120 | 121 | mock_input.return_value = 'Hodor' 122 | sultan = Sultan() 123 | self.assertEqual(sultan.stdin('What is your name?'), 'Hodor') 124 | 125 | def test_calling_context(self): 126 | 127 | sultan = Sultan.load(cwd='/tmp', test_key='test_val') 128 | self.assertEqual(sultan.current_context, { 129 | 'cwd': '/tmp', 130 | 'env': None, 131 | 'executable': None, 132 | 'sudo': False, 133 | 'logging': True, 134 | 'test_key': 'test_val', 135 | 'user': getpass.getuser(), 136 | 'hostname': None, 137 | 'ssh_config': '', 138 | 'src': None 139 | }) 140 | 141 | # cwd 142 | with Sultan.load(cwd='/tmp') as sultan: 143 | self.assertEqual(sultan.current_context, { 144 | 'cwd': '/tmp', 145 | 'env': None, 146 | 'executable': None, 147 | 'sudo': False, 148 | 'logging': True, 149 | 'user': getpass.getuser(), 150 | 'hostname': None, 151 | 'ssh_config': '', 152 | 'src': None 153 | }) 154 | 155 | # sudo 156 | with Sultan.load(cwd='/tmp', sudo=True) as sultan: 157 | self.assertEqual(sultan.current_context, { 158 | 'cwd': '/tmp', 159 | 'env': None, 160 | 'executable': None, 161 | 'sudo': True, 162 | 'logging': True, 163 | 'user': getpass.getuser(), 164 | 'hostname': None, 165 | 'ssh_config': '', 166 | 'src': None 167 | }) 168 | 169 | with Sultan.load(cwd='/tmp', sudo=False, user="hodor") as sultan: 170 | self.assertEqual(sultan.current_context, { 171 | 'cwd': '/tmp', 172 | 'env': None, 173 | 'executable': None, 174 | 'sudo': False, 175 | 'logging': True, 176 | 'user': 'hodor', 177 | 'hostname': None, 178 | 'ssh_config': '', 179 | 'src': None 180 | }) 181 | 182 | with Sultan.load(sudo=True) as sultan: 183 | 184 | self.assertEqual(sultan.current_context, { 185 | 'cwd': None, 186 | 'env': None, 187 | 'executable': None, 188 | 'sudo': True, 189 | 'logging': True, 190 | 'user': getpass.getuser(), 191 | 'hostname': None, 192 | 'ssh_config': '', 193 | 'src': None 194 | }) 195 | 196 | # hostname 197 | with Sultan.load(hostname='localhost') as sultan: 198 | 199 | self.assertEqual(sultan.current_context, { 200 | 'cwd': None, 201 | 'env': None, 202 | 'executable': None, 203 | 'sudo': False, 204 | 'logging': True, 205 | 'user': getpass.getuser(), 206 | 'hostname': 'localhost', 207 | 'ssh_config': '', 208 | 'src': None 209 | }) 210 | 211 | # set environment 212 | with Sultan.load(env={'path': ''}) as sultan: 213 | self.assertEqual(sultan.current_context, { 214 | 'cwd': None, 215 | 'env': {'path': ''}, 216 | 'executable': None, 217 | 'sudo': False, 218 | 'logging': True, 219 | 'user': getpass.getuser(), 220 | 'hostname': None, 221 | 'ssh_config': '', 222 | 'src': None 223 | }) 224 | 225 | # set port 226 | config = SSHConfig(port=2222) 227 | with Sultan.load(ssh_config=config) as sultan: 228 | self.assertEqual(sultan.current_context, { 229 | 'cwd': None, 230 | 'env': None, 231 | 'executable': None, 232 | 'sudo': False, 233 | 'logging': True, 234 | 'user': getpass.getuser(), 235 | 'hostname': None, 236 | 'ssh_config': '-p 2222', 237 | 'src': None 238 | }) 239 | 240 | # set src 241 | filehandle, filepath = tempfile.mkstemp() 242 | try: 243 | with Sultan.load(src=filepath) as s: 244 | self.assertEqual(s.current_context, { 245 | 'cwd': None, 246 | 'env': None, 247 | 'executable': None, 248 | 'sudo': False, 249 | 'logging': True, 250 | 'user': getpass.getuser(), 251 | 'hostname': None, 252 | 'ssh_config': '', 253 | 'src': filepath 254 | }) 255 | finally: 256 | if os.path.exists(filepath): 257 | os.unlink(filepath) 258 | 259 | # custom executable 260 | filehandle, filepath = tempfile.mkstemp() 261 | try: 262 | with Sultan.load(executable=filepath) as s: 263 | self.assertEqual(s.current_context, { 264 | 'cwd': None, 265 | 'env': None, 266 | 'executable': filepath, 267 | 'sudo': False, 268 | 'logging': True, 269 | 'user': getpass.getuser(), 270 | 'hostname': None, 271 | 'ssh_config': '', 272 | 'src': None 273 | }) 274 | finally: 275 | if os.path.exists(filepath): 276 | os.unlink(filepath) 277 | 278 | def test_context_for_pwd(self): 279 | 280 | with Sultan.load(cwd='/tmp') as sultan: 281 | self.assertEqual(str(sultan.ls('-lah')), 'cd /tmp && ls -lah;') 282 | 283 | def test_calling_context_sudo(self): 284 | 285 | # no sudo 286 | with Sultan.load(sudo=False) as sultan: 287 | self.assertEqual(str(sultan.ls('-lah', '/root')), 'ls -lah /root;') 288 | 289 | # sudo as current user 290 | with Sultan.load(sudo=True) as sultan: 291 | self.assertEqual(str(sultan.ls('-lah', '/root')), 292 | 'sudo ls -lah /root;') 293 | 294 | # sudo as another user 295 | with Sultan.load(sudo=True, user='hodor') as sultan: 296 | self.assertEqual(str(sultan.ls("/home/hodor")), 297 | "sudo su - hodor -c 'ls /home/hodor;'") 298 | 299 | # sudo as another user with cwd set 300 | with Sultan.load(sudo=True, user='hodor', cwd='/home/hodor') as sultan: 301 | self.assertEqual(str(sultan.ls('-lah', '.')), 302 | "sudo su - hodor -c 'cd /home/hodor && ls -lah .;'") 303 | 304 | def test_calling_context_hostname(self): 305 | 306 | # with no username specified 307 | with Sultan.load(hostname='google.com') as sultan: 308 | user = getpass.getuser() 309 | self.assertEqual(str(sultan.ls("-lah", "/home")), 310 | "ssh %s@google.com 'ls -lah /home;'" % user) 311 | 312 | # local user 313 | with Sultan.load(hostname='google.com', user=getpass.getuser()) as sultan: 314 | user = getpass.getuser() 315 | self.assertEqual(str(sultan.ls("-lah", "/home")), 316 | "ssh %s@google.com 'ls -lah /home;'" % user) 317 | 318 | # different user 319 | with Sultan.load(hostname='google.com', user="obama") as sultan: 320 | user = "obama" 321 | self.assertEqual(str(sultan.ls("-lah", "/home")), 322 | "ssh %s@google.com 'ls -lah /home;'" % user) 323 | 324 | # different user as sudo 325 | with Sultan.load(hostname='google.com', user="obama", sudo=True) as sultan: 326 | user = "obama" 327 | self.assertEqual(str(sultan.ls("-lah", "/home")), 328 | "ssh %s@google.com 'sudo su - obama -c \'ls -lah /home;\''" % user) 329 | 330 | # different port and different user as sudo 331 | config = SSHConfig(port=2345) 332 | with Sultan.load(hostname='google.com', user='obama', sudo=True, ssh_config=config) as sultan: 333 | user = 'obama' 334 | self.assertEqual(str(sultan.ls('-lah', '/home')), 335 | "ssh -p 2345 %s@google.com 'sudo su - obama -c \'ls -lah /home;\''" % user) 336 | 337 | def test_calling_context_wrongly(self): 338 | 339 | s = Sultan() 340 | with self.assertRaises(InvalidContextError): 341 | with Sultan() as s: 342 | pass 343 | 344 | def test_clear_buffer_on_error(self): 345 | 346 | s = Sultan() 347 | try: 348 | s.ls("/root").run(halt_on_nonzero=False) 349 | except: 350 | self.assertEqual(len(s.commands), 0) 351 | 352 | def test_dashes(self): 353 | 354 | with Sultan.load() as s: 355 | self.assertEqual(str(s.apt__get('install', 'httpd')), 356 | 'apt-get install httpd;') 357 | 358 | def test_src(self): 359 | 360 | handle, filepath = tempfile.mkstemp() 361 | try: 362 | 363 | with Sultan.load(src=filepath) as s: 364 | self.assertEqual( 365 | str(s.yum('install', 'apache')), 366 | 'source %s && yum install apache;' % filepath) 367 | finally: 368 | if os.path.exists(filepath): 369 | os.unlink(filepath) 370 | 371 | def test_src_with_compound(self): 372 | 373 | handle, filepath = tempfile.mkstemp() 374 | try: 375 | 376 | with Sultan.load(cwd='/tmp', src=filepath) as s: 377 | self.assertEqual( 378 | str(s.yum('install', 'apache')), 379 | 'source %s && cd /tmp && yum install apache;' % filepath) 380 | finally: 381 | if os.path.exists(filepath): 382 | os.unlink(filepath) 383 | 384 | 385 | class SultanCommandTestCase(unittest.TestCase): 386 | 387 | def test_normal(self): 388 | 389 | sultan = Sultan() 390 | command = Command(sultan, "yum") 391 | self.assertEqual(str(command), "yum") 392 | 393 | def test_where_attribute(self): 394 | 395 | sultan = Sultan() 396 | command = Command(sultan, "df") 397 | self.assertEqual(str(command(where="/bin")), "/bin/df;") 398 | 399 | 400 | class PipeTestCase(unittest.TestCase): 401 | 402 | def test_pipe(self): 403 | 404 | s = Sultan() 405 | r = Pipe(s, '|') 406 | self.assertEqual(r.command, "|") 407 | self.assertEqual(str(r.command), "|") 408 | 409 | 410 | class AndTestCase(unittest.TestCase): 411 | 412 | def test_and(self): 413 | 414 | s = Sultan() 415 | r = And(s, '&') 416 | self.assertEqual(r.command, "&") 417 | self.assertEqual(str(r.command), "&") 418 | 419 | 420 | class OrTestCommand(unittest.TestCase): 421 | 422 | def test_or(self): 423 | 424 | s = Sultan() 425 | r = Or(s, '|') 426 | self.assertEqual(r.command, '|') 427 | self.assertEqual(str(r.command), '|') 428 | 429 | 430 | class TestRedirect(unittest.TestCase): 431 | 432 | def test_redirect_stdout_only(self): 433 | 434 | s = Sultan() 435 | r = Redirect(s, '') 436 | r("/tmp/foo", stdout=True) 437 | self.assertEqual(r.command, "1> /tmp/foo") 438 | 439 | def test_redirect_stderr_only(self): 440 | 441 | s = Sultan() 442 | r = Redirect(s, '') 443 | r("/tmp/foo", stderr=True) 444 | self.assertEqual(r.command, "2> /tmp/foo") 445 | 446 | def test_redirect_stdout_only_with_append(self): 447 | 448 | s = Sultan() 449 | r = Redirect(s, '') 450 | r("/tmp/foo", stdout=True, append=True) 451 | self.assertEqual(r.command, "1>> /tmp/foo") 452 | 453 | def test_redirect_stderr_only_with_append(self): 454 | 455 | s = Sultan() 456 | r = Redirect(s, '') 457 | r("/tmp/foo", stderr=True, append=True) 458 | self.assertEqual(r.command, "2>> /tmp/foo") 459 | 460 | def test_redirect_stdout_and_stderr(self): 461 | 462 | s = Sultan() 463 | r = Redirect(s, '') 464 | r("/tmp/foo", stdout=True, stderr=True) 465 | self.assertEqual(r.command, "&> /tmp/foo") 466 | 467 | def test_redirect_stdout_and_stderr_with_append(self): 468 | 469 | s = Sultan() 470 | r = Redirect(s, '') 471 | r("/tmp/foo", stdout=True, stderr=True, append=True) 472 | self.assertEqual(r.command, "&>> /tmp/foo") 473 | 474 | def test_clear_buffer_after_run(self): 475 | 476 | s = Sultan() 477 | first = s.echo("first").run().stdout 478 | second = s.echo("second").run().stdout 479 | self.assertEqual(first, ['first']) 480 | self.assertEqual(second, ['second']) 481 | -------------------------------------------------------------------------------- /test/unit/sultan/test_config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from sultan.config import Settings 3 | 4 | 5 | class TestSettings(unittest.TestCase): 6 | 7 | def setUp(self): 8 | 9 | self.settings = Settings() 10 | 11 | def test_default_settings_loaded(self): 12 | 13 | self.assertEqual(self.settings.HALT_ON_ERROR, True) 14 | 15 | def test_log_format(self): 16 | 17 | self.assertEqual(self.settings.LOG_FORMAT, '%(log_color)s[%(name)s]: %(message)s') 18 | 19 | def test_log_colors(self): 20 | 21 | self.assertEqual(self.settings.LOG_COLORS, { 22 | 'DEBUG': 'cyan', 23 | 'INFO': 'green', 24 | 'WARNING': 'yellow', 25 | 'ERROR': 'red', 26 | 'CRITICAL': 'bold_red', 27 | }) -------------------------------------------------------------------------------- /test/unit/sultan/test_result.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import unittest 3 | 4 | from sultan.result import Result 5 | 6 | class SultanResultTestCase(unittest.TestCase): 7 | 8 | def setUp(self): 9 | 10 | self.stdout = ''' 11 | drwxr-xr-x 3 davydany wheel 102 Sep 10 08:45 27D4E633-1477-40F6-AE3B-01AA85A6CBD9 12 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:52 30B00D4E-939A-404E-B2EB-23CB94489B64_IN 13 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:52 30B00D4E-939A-404E-B2EB-23CB94489B64_OUT 14 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:52 456237D8-E9A4-4F77-9E8E-D9303863B2F3_IN 15 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:52 456237D8-E9A4-4F77-9E8E-D9303863B2F3_OUT 16 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:52 47798DB1-EC09-4E72-A1B1-808629D92383_IN 17 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:52 47798DB1-EC09-4E72-A1B1-808629D92383_OUT 18 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:50 671A5D52-29E0-4747-B8CA-F035ED2B1EFB_IN 19 | prw-rw-rw- 1 davydany wheel 0 Sep 7 06:49 671A5D52-29E0-4747-B8CA-F035ED2B1EFB_OUT 20 | drwxr-xr-x 3 davydany wheel 102 Sep 10 08:45 743B6A79-82A4-471F-A9E8-B5BAF5B8E90B 21 | drwxr-xr-x 2 davydany wheel 68 Sep 7 06:52 814486EC-476D-47F1-AEBC-8E813E890B83 22 | ''' 23 | 24 | self.stderr = ''' 25 | ls: /foobar: No such file or directory 26 | ls: /root: No such file or directory 27 | ''' 28 | 29 | @mock.patch("sultan.result.subprocess") 30 | def test_stdout(self, m_subprocess): 31 | m_subprocess.Popen = mock.Mock() 32 | m_subprocess.Popen().communicate.return_value = (self.stdout, self.stderr) 33 | result = Result(m_subprocess.Popen(), [], {}) 34 | self.assertEqual(result.stdout, self.stdout.strip().splitlines()) 35 | 36 | @mock.patch("sultan.result.subprocess") 37 | def test_stderr(self, m_subprocess): 38 | m_subprocess.Popen = mock.Mock() 39 | m_subprocess.Popen().communicate.return_value = (self.stdout, self.stderr) 40 | result = Result(m_subprocess.Popen(), [], {}) 41 | self.assertEqual(result.stderr, self.stderr.strip().splitlines()) 42 | --------------------------------------------------------------------------------