diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..eb8bd95 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.bat] +indent_style = tab +end_of_line = crlf + +[LICENSE] +insert_final_newline = false + +[Makefile] +indent_style = tab diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..774076d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +* ansible-taskrunner version: +* Python version: +* Operating System: + +### Description + +Describe what you were trying to get done. +Tell us what happened, what went wrong, and what you expected to happen. + +### What I Did + +``` +Paste the command(s) you ran and the output. +If there was a crash, please include the traceback here. +``` diff --git a/.gitignore b/.gitignore index 4fff260..b7006ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,104 @@ -# Ignore python compiled code -*.pyc -# Ignore ansible task retry files -*.retry -# Ignore everything under lib -lib/*/* -# Except for our core modules -!lib/common/click_extras -!lib/common/errorhandler -!lib/common/formatting -!lib/common/help -!lib/common/providers -!lib/commonsuperduperconfig -!lib/common/yamlc -!lib/common/yamlr -# Ignore build files -dist -build -release \ No newline at end of file +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +ansible_taskrunner/lib/py2 +ansible_taskrunner/lib/py3 +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +release + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0a5b375 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +# Config file for automatic testing at travis-ci.org + +language: python +python: + - 3.6 + - 3.5 + - 3.4 + - 2.7 + +# Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors +install: pip install -U tox-travis + +# Command to run tests, e.g. python setup.py test +script: tox + + diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..eca78ea --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,13 @@ +======= +Credits +======= + +Development Lead +---------------- + +* Engelbert Tejeda + +Contributors +------------ + +None yet. Why not be the first? diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..93892ed --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,128 @@ +.. highlight:: shell + +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/berttejeda/ansible_taskrunner/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help +wanted" is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "enhancement" +and "help wanted" is open to whoever wants to implement it. + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +ansible-taskrunner could always use more documentation, whether as part of the +official ansible-taskrunner docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at https://github.com/berttejeda/ansible_taskrunner/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up `ansible_taskrunner` for local development. + +1. Fork the `ansible_taskrunner` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/ansible_taskrunner.git + +3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: + + $ mkvirtualenv ansible_taskrunner + $ cd ansible_taskrunner/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass flake8 and the + tests, including testing other Python versions with tox:: + + $ flake8 ansible_taskrunner tests + $ python setup.py test or py.test + $ tox + + To get flake8 and tox, just pip install them into your virtualenv. + +6. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +7. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check + https://travis-ci.org/berttejeda/ansible_taskrunner/pull_requests + and make sure that the tests pass for all supported Python versions. + +Tips +---- + +To run a subset of tests:: + + + $ python -m unittest tests.test_ansible_taskrunner + +Deploying +--------- + +A reminder for the maintainers on how to deploy. +Make sure all your changes are committed (including an entry in HISTORY.rst). +Then run:: + +$ bumpversion patch # possible: major / minor / patch +$ git push +$ git push --tags + +Travis will then deploy to PyPI if tests pass. diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..504314a --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,8 @@ +======= +History +======= + +0.0.13 (2019-07-17) +------------------ + +* First release on PyPI. diff --git a/LICENSE b/LICENSE index bd4f91a..52a03e1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,22 @@ -MIT License - -Copyright (c) 2018 Bert Tejeda - -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: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -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. +MIT License + +Copyright (c) 2019, Engelbert Tejeda + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9fb3cff --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,12 @@ +include AUTHORS.rst +include CONTRIBUTING.rst +include HISTORY.rst +include LICENSE +include README.rst +include ansible_taskrunner/lib/locale/en.yaml + +recursive-include tests * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] + +recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b3d16f4 --- /dev/null +++ b/Makefile @@ -0,0 +1,88 @@ +.PHONY: clean clean-test clean-pyc clean-build docs help +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +try: + from urllib import pathname2url +except: + from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + +lint: ## check style with flake8 + flake8 ansible_taskrunner tests + +test: ## run tests quickly with the default Python + python setup.py test + +test-all: ## run tests on every Python version with tox + tox + +coverage: ## check code coverage quickly with the default Python + coverage run --source ansible_taskrunner setup.py test + coverage report -m + coverage html + $(BROWSER) htmlcov/index.html + +docs: ## generate Sphinx HTML documentation, including API docs + rm -f docs/ansible_taskrunner.rst + rm -f docs/modules.rst + sphinx-apidoc -o docs/ ansible_taskrunner + $(MAKE) -C docs clean + $(MAKE) -C docs html + $(BROWSER) docs/_build/html/index.html + +servedocs: docs ## compile the docs watching for changes + watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + +release: dist ## package and upload a release + twine upload dist/* + +dist: clean ## builds source and wheel package + python setup.py sdist + python setup.py bdist_wheel + ls -l dist + +install: clean ## install the package to the active Python's site-packages + python setup.py install diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..dca184e --- /dev/null +++ b/README.rst @@ -0,0 +1,37 @@ +================== +ansible-taskrunner +================== + + +.. image:: https://img.shields.io/pypi/v/ansible_taskrunner.svg + :target: https://pypi.python.org/pypi/ansible_taskrunner + +.. image:: https://img.shields.io/travis/berttejeda/ansible_taskrunner.svg + :target: https://travis-ci.org/berttejeda/ansible_taskrunner + +.. image:: https://readthedocs.org/projects/ansible-taskrunner/badge/?version=latest + :target: https://ansible-taskrunner.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + + + + +ansible-playbook wrapper with YAML-abstracted python click cli options + + +* Free software: MIT license +* Documentation: https://ansible-taskrunner.readthedocs.io. + + +Features +-------- + +* TODO Employ language/regional options + +Credits +------- + +This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. + +.. _Cookiecutter: https://github.com/audreyr/cookiecutter +.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage diff --git a/Taskfile.yaml b/Taskfile.yaml index 04139e0..54024a9 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -14,6 +14,7 @@ - mylistvalue3 - mylistvalue4 required_parameters: + optional_parameters: aws: -d|--db-hosts: dbhosts_aws -a|--some-special-aws-flag: aws_flag @@ -26,7 +27,6 @@ -d|--db-hosts: dbhosts -w|--web-hosts: webhosts -t|--some-parameter: some_value - optional_parameters: -l|--another-parameter: another_value -A: hello -PR: preflight_and_run @@ -59,7 +59,7 @@ shell: bash source: |- echo 'Running Preflight Tasks!' - tasks run -d dbhost1 -w webhost1 -t value1 + tasks run -d dbhost1 -w webhost1 -t value1 tasks: - debug: msg: | diff --git a/ansible_taskrunner/__init__.py b/ansible_taskrunner/__init__.py new file mode 100644 index 0000000..1607f1e --- /dev/null +++ b/ansible_taskrunner/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +"""Top-level package for ansible-taskrunner.""" + +__author__ = """Engelbert Tejeda""" +__email__ = 'berttejeda@gmail.com' +__version__ = '0.0.14' + +__author__ = 'etejed001c' diff --git a/ansible_taskrunner/ansible_taskrunner.py b/ansible_taskrunner/ansible_taskrunner.py new file mode 100644 index 0000000..7fbbae4 --- /dev/null +++ b/ansible_taskrunner/ansible_taskrunner.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +"""Main module.""" diff --git a/tasks.py b/ansible_taskrunner/cli.py similarity index 53% rename from tasks.py rename to ansible_taskrunner/cli.py index 1a7243b..0c5c32b 100644 --- a/tasks.py +++ b/ansible_taskrunner/cli.py @@ -1,35 +1,50 @@ -#!/usr/bin/env python -# coding=utf-8 -""" Ansible Task Runner -""" - +# -*- coding: utf-8 -*- # Import builtins from __future__ import print_function -from datetime import datetime import logging import logging.handlers import os import sys +# For zip-app +self_file_name = os.path.basename(__file__) +if self_file_name == '__main__.py': + script_name = os.path.dirname(__file__) +else: + script_name = self_file_name + +# Needed for zip-app +# Make the zipapp work for python2/python3 +py_path = 'py3' if sys.version_info[0] >= 3 else 'py2' +project_root = os.path.dirname(os.path.abspath(__file__)) +if sys.platform in ['win32', 'cygwin']: + sys.path.insert(0, project_root + '\\lib') + sys.path.insert(0, project_root + '\\lib\\%s' % py_path) +elif sys.platform in ['darwin']: + sys.path.insert(0, project_root + '/lib') + sys.path.insert(0, project_root + '/lib/%s' % py_path) +else: + sys.path.insert(0, project_root + '/lib') + sys.path.insert(0, project_root + '/lib/%s' % py_path) + # Import third-party and custom modules try: - if sys.version_info[0] >= 3: - from lib.py3 import click - from lib.py3 import yaml - else: - from lib.py2 import click - from lib.py2 import yaml - from lib.common.errorhandler import catchException - from lib.common.errorhandler import ERR_ARGS_TASKF_OVERRIDE - from lib.common.formatting import reindent - from lib.common.help import SAMPLE_CONFIG - from lib.common.help import SAMPLE_TASKS_MANIFEST - from lib.common.superduperconfig import SuperDuperConfig - from lib.common.click_extras import ExtendedEpilog - from lib.common.click_extras import ExtendedHelp - from lib.common.click_extras import ExtendCLI - from lib.common.yamlc import YamlCLIInvocation - from lib.common.yamlr import YamlReader + import click + from errorhandler import catchException + from errorhandler import ERR_ARGS_TASKF_OVERRIDE + from formatting import logging_format + from formatting import reindent + from help import SAMPLE_CONFIG + from help import SAMPLE_TASKS_MANIFEST + from superduperconfig import SuperDuperConfig + from click_extras import ExtendedEpilog + from click_extras import ExtendedHelp + from click_extras import ExtendCLI + from yamlc import YamlCLIInvocation + from yamlr import YamlReader + # TODO + # Employ language/regional options + # from lib.language import get_strings except ImportError as e: print('Failed to import at least one required module') print('Error was %s' % e) @@ -37,9 +52,24 @@ print('pip install -U -r requirements.txt') sys.exit(1) +# Cover My A$$ +_cma = """ + CMA: + This software is made available by the author in the hope + that it will be useful, but without any warranty. + The author is not liable for any consequence related to the + use of the provided application. +""" + +_doc = """ +Ansible Taskrunner - ansible-playbook wrapper +- YAML-abstracted python click cli options +- Utilizes a specially-formatted ansible-playbook +""" + # Private variables __author__ = 'etejeda' -__version__ = '0.0.13-alpha' +__version__ = '0.0.14-alpha' __program_name__ = 'tasks' __debug = False verbose = 0 @@ -51,7 +81,9 @@ logger = logging.getLogger('logger') logger.setLevel(logging.INFO) streamhandler = logging.StreamHandler() -streamhandler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s - %(message)s")) +streamhandler.setFormatter( + logging.Formatter("[%(asctime)s] %(levelname)s - %(message)s") +) logger.addHandler(streamhandler) # Load Config(s) @@ -59,13 +91,8 @@ superconf = SuperDuperConfig(__program_name__) config = superconf.load_config(config_file) -# For zip-app -script_name = os.path.basename(__file__) -if script_name == '__main__.py': - script_name = os.path.dirname(__file__) -def main(args, tasks_file='Taskfile.yaml', param_set=None, path_string='vars', cli_provider='default', debug=False): - +def main(args, tasks_file='Taskfile.yaml', param_set=None, path_string='vars'): # We'll pass this down to the run invocation global _param_set _param_set = param_set @@ -77,22 +104,24 @@ def main(args, tasks_file='Taskfile.yaml', param_set=None, path_string='vars', c # Detect command provider cli_provider = yamlr.deep_get(config, 'cli.providers.default', {}) if cli_provider == 'bash': - from lib.common.providers import bash as bash_cli + from providers import bash as bash_cli provider_cli = bash_cli.ProviderCLI() elif cli_provider == 'vagrant': - from lib.common.providers import vagrant as vagrant_cli + from providers import vagrant as vagrant_cli provider_cli = vagrant_cli.ProviderCLI() else: - from lib.common.providers import ansible as ansible_cli + from providers import ansible as ansible_cli provider_cli = ansible_cli.ProviderCLI() - + # Load Tasks Manifest yaml_input_file = tasks_file yaml_path_string = path_string if os.path.exists(yaml_input_file): yaml_data = superconf.load_config(yaml_input_file, data_key=0) else: - logger.warning("Couln't find %s or any other Tasks Manifest" % yaml_input_file) + logger.warning( + "Couln't find %s or any other Tasks Manifest" % yaml_input_file + ) yaml_data = {} # Extend CLI Options as per Tasks Manifest @@ -110,29 +139,34 @@ def main(args, tasks_file='Taskfile.yaml', param_set=None, path_string='vars', c for provider in plugins.providers: provider_cli = provider.ProviderCLI() - HELP = """\b - Task runner that serves as a higher-level automation layer to ansible - The script expects a specially formatted ansible-playbook in order to function properly + click_help = """\b + Ansible Taskrunner - ansible-playbook wrapper + - YAML-abstracted python click cli options + - Utilizes a specially-formatted ansible-playbook """ - EPILOG = "" + click_help_epilog = "" - @click.group(cls=ExtendedEpilog, help=HELP, epilog=EPILOG) + @click.group(cls=ExtendedEpilog, help=click_help, epilog=click_help_epilog) @click.version_option(version=__version__) - @click.option('--config', '-C', type=str, nargs=1, help='Specify a config file (default is config.ini)') + @click.option('--config', '-C', type=str, nargs=1, + help='Specify a config file (default is config.ini)') @click.option('--debug', '-d', is_flag=True, help='Enable debug output') - @click.option('--verbose', '-v', count=True, help='Increase verbosity of output') - @click.option('--log', '-l', type=str, help='Specify (an) optional log file(s)') - def cli(**kwargs): + @click.option('--verbose', '-v', count=True, + help='Increase verbosity of output') + @click.option('--log', '-l', type=str, + help='Specify (an) optional log file(s)') + def cli(**kwargs): global config, config_file, __debug, verbose, loglevel, logger # Are we specifying an alternate config file? if kwargs['config']: config = superconf.load_config(config_file) if config is None: - logger.warning('No valid config file found %s, using program defaults' % config_file) - # Verbose mode enabled? - verbose = kwargs['verbose'] + logger.warning('No valid config file found %s' % config_file) + logger.warning('Using program defaults') + # Verbose mode enabled? + verbose = kwargs.get('verbose', None) # Debug mode enabled? - __debug = kwargs['debug'] + __debug = kwargs.get('debug', None) # Set up logging with our desired output level if __debug: loglevel = logging.DEBUG # 10 @@ -142,21 +176,22 @@ def cli(**kwargs): loglevel = logging.INFO # 20 logger.setLevel(loglevel) # Add the log file handler to the logger, if applicable - logfilename = kwargs['log'] + logfilename = kwargs.get('log') if logfilename: - filehandler = logging.handlers.RotatingFileHandler(logfilename, maxBytes=10000000, backupCount=5) - formatter = logging.Formatter( - "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s") + filehandler = logging.handlers.RotatingFileHandler( + logfilename, maxBytes=10000000, backupCount=5) + formatter = logging.Formatter(logging_format) filehandler.setFormatter(formatter) logger.addHandler(filehandler) logger.debug('Debug Mode Enabled, keeping any generated temp files') return 0 # Examples command - @cli.command(help="Initialize local directory with sample files to get your started") + @cli.command(help='Initialize local directory with sample files') @click.version_option(version=__version__) - @click.option('--show-samples', '-m', is_flag=True, help='Only show a sample task manifest, don\'t write it') - def init(args=None, **kwargs): + @click.option('--show-samples', '-m', is_flag=True, + help='Only show a sample task manifest, don\'t write it') + def init(**kwargs): logger.info('Initializing ...') if kwargs['show_samples']: logger.info('Displaying sample config') @@ -165,17 +200,21 @@ def init(args=None, **kwargs): print(SAMPLE_TASKS_MANIFEST) else: if not os.path.exists(config_file): - logger.info('File does not exist ... writing sample config %s' % config_file) + logger.info( + 'Existing config not found, writing %s' % config_file) with open(config_file, 'w') as f: f.write(SAMPLE_CONFIG) else: - logger.info('File exists ... not writing sample config %s' % config_file) + logger.info( + 'File exists, not writing sample config %s' % config_file) if not os.path.exists(tasks_file): - logger.info('File does not exist ... writing sample manifest %s' % tasks_file) + logger.info( + 'Existing manifest not found, writing %s' % tasks_file) with open(tasks_file, 'w') as f: f.write(SAMPLE_TASKS_MANIFEST) else: - logger.info('File exists ... not writing sample manifest %s' % tasks_file) + logger.info( + 'File exists, not writing manifest %s' % tasks_file) # Parse help documentation help_string = yamlr.deep_get(yaml_vars, 'help.message', '') @@ -185,8 +224,8 @@ def init(args=None, **kwargs): if isinstance(examples, list): for example in examples: if isinstance(example, dict): - for key,value in example.items(): - examples_string += '- {k}:\n{v}'.format(k=key, v=value) + for key, value in example.items(): + examples_string += '- {k}:\n{v}'.format(k=key, v=value) if isinstance(example, str): examples_string += '%s\n' % example epilog = ''' @@ -195,12 +234,14 @@ def init(args=None, **kwargs): {ex} '''.format(ep=epilog_string, ex=examples_string) epilog = reindent(epilog, 0) - + # Run command @cli.command(cls=ExtendedHelp, help="{h}".format(h=help_string), epilog=epilog) @click.version_option(version=__version__) - @click.option('---raw', '---r', is_flag=False, help='Specify raw options to pass down to the underlying subprocess', required=False) + @click.option('---raw', '---r', is_flag=False, + help='Specify raw options for underlying subprocess', + required=False) @click.option('--echo', is_flag=True, help='Don\'t run, simply echo underlying commands') @@ -208,52 +249,66 @@ def init(args=None, **kwargs): @provider_cli.options def run(args=None, **kwargs): # Process Raw Args - raw_args = kwargs['_raw'] if kwargs['_raw'] else '' + raw_args = kwargs['_raw'] if kwargs.get('_raw') else '' # Instantiate the cli invocation class yamlcli = YamlCLIInvocation() args = ' '.join(args) if args else '' # Initialize values for subshell - prefix = 'echo' if kwargs['echo'] else '' + prefix = 'echo' if kwargs.get('echo') else '' # Default variables - default_vars = dict([(key, value) for key, value in yaml_vars.items() if not isinstance(value, dict)]) + default_vars = dict( + [(key, value) for key, value in yaml_vars.items() + if not isinstance(value, dict)]) # Parameter set var (if it has been specified) - paramset_var = "parameter_set=%s" % (_param_set if _param_set else 'False') + paramset_var = "parameter_set=%s" % ( + _param_set if _param_set else 'False') # List-type variables list_vars = [] for var in default_vars: if isinstance(default_vars[var], list): - list_vars.append('{k}=$(cat < 1: paramset = ''.join(paramset[1]) else: paramset = None # Call main function as per parameter set if paramset: - sys.exit(main(cli_args, param_set=paramset, tasks_file=tf_override)) + sys.exit(main(cli_args, param_set=paramset, + tasks_file=tf_override)) else: sys.exit(main(cli_args, tasks_file=tf_override)) else: quit(ERR_ARGS_TASKF_OVERRIDE.format(script=script_name)) else: # Determine paramter set - paramset = ''.join([a for a in sys.argv[1:arg_run_index] if a not in ['run'] and not a.startswith('-')]) + paramset = ''.join( + [a for a in sys.argv[1:arg_run_index] if a not in [ + 'run'] and not a.startswith('-')]) if paramset: sys.exit(main(cli_args, param_set=paramset)) else: @@ -331,12 +393,19 @@ def run(args=None, **kwargs): if tf_override: demark = sys.argv.index(tf_override) run_args = sys.argv[demark + 1:] - run_flgs = [a for a in sys.argv[:demark] if a.startswith('-') and a != sys.argv[arg_tf_index]] - cli_args = run_flgs + run_args - if any([ext in tf_override for ext in ["yaml","yml"]]): + run_flgs = [a for a in sys.argv[:demark] if a.startswith( + '-') and a != sys.argv[arg_tf_index]] + cli_args = run_flgs + run_args + if any([ext in tf_override for ext in ["yaml", "yml"]]): # Call main function as per parameter set - sys.exit(main(cli_args, tasks_file=tf_override)) + sys.exit(main(cli_args, tasks_file=tf_override)) else: - quit(ERR_ARGS_TASKF_OVERRIDE.format(script=script_name)) + quit(ERR_ARGS_TASKF_OVERRIDE.format(script=script_name)) else: - sys.exit(main(sys.argv[1:])) + sys.exit(main(sys.argv[1:])) + + # CLI entry point + + +if __name__ == '__main__': + entrypoint() diff --git a/lib/__init__.py b/ansible_taskrunner/lib/__init__.py similarity index 100% rename from lib/__init__.py rename to ansible_taskrunner/lib/__init__.py diff --git a/lib/common/providers/vagrant/__init__.py b/ansible_taskrunner/lib/bash/__init__.py similarity index 59% rename from lib/common/providers/vagrant/__init__.py rename to ansible_taskrunner/lib/bash/__init__.py index 04881c4..2fe0bba 100644 --- a/lib/common/providers/vagrant/__init__.py +++ b/ansible_taskrunner/lib/bash/__init__.py @@ -1,10 +1,8 @@ # Imports -import json import logging -import re import sys -provider_name = 'vagrant' +provider_name = 'bash' # Logging logger = logging.getLogger('logger') @@ -12,12 +10,9 @@ # Import third-party and custom modules try: - if sys.version_info[0] >= 3: - from lib.py3 import click - else: - from lib.py2 import click - from lib.common.formatting import ansi_colors, reindent - from lib.common.yamlc import YamlCLIInvocation + import click + from .. import reindent + from .. import YamlCLIInvocation except ImportError as e: print('Failed to import at least one required module') print('Error was %s' % e) @@ -25,33 +20,35 @@ print('pip install -U -r requirements.txt') sys.exit(1) -class ProviderCLI(): + +class ProviderCLI: def __init__(self, parameter_set=None, vars_input={}): self.vars = vars_input self.parameter_set = parameter_set self.logger = logger pass - def options(self, func): + @staticmethod + def options(func): """Add provider-specific click options""" return func - def invocation(self, - yaml_input_file=None, - string_vars=[], - default_vars={}, - paramset_var=None, - bash_functions=[], - cli_vars='', - yaml_vars={}, - list_vars=[], - debug=False, - args=None, - prefix='', - raw_args='', - kwargs={}): + @staticmethod + def invocation(yaml_input_file=None, + string_vars=[], + default_vars={}, + paramset_var=None, + bash_functions=[], + cli_vars='', + yaml_vars={}, + list_vars=[], + debug=False, + args=None, + prefix='', + raw_args='', + kwargs={}): """Invoke commands according to provider""" - logger.info('Vagrant Command Provider') + logger.info('Bash Command Provider') command = ''' {dsv} {psv} @@ -66,10 +63,10 @@ def invocation(self, bfn='\n'.join(bash_functions), deb=debug ) - command = reindent(command,0) + command = reindent(command, 0) # Command invocation if prefix == 'echo': logger.info("ECHO MODE ON") print(command) else: - YamlCLIInvocation().call(command) \ No newline at end of file + YamlCLIInvocation().call(command) diff --git a/lib/common/click_extras/__init__.py b/ansible_taskrunner/lib/click_extras/__init__.py similarity index 82% rename from lib/common/click_extras/__init__.py rename to ansible_taskrunner/lib/click_extras/__init__.py index 62ecde8..829d307 100644 --- a/lib/common/click_extras/__init__.py +++ b/ansible_taskrunner/lib/click_extras/__init__.py @@ -8,18 +8,14 @@ # Import third-party and custom modules try: - if sys.version_info[0] >= 3: - from lib.py3 import click - from lib.py3 import yaml - else: - from lib.py2 import click - from lib.py2 import yaml + import click except ImportError as e: print('Failed to import at least one required module') print('Error was %s' % e) print('pip install -U -r requirements.txt') sys.exit(1) + class ExtendedEpilog(click.Group): def format_epilog(self, ctx, formatter): """Format click epilog to honor newline characters""" @@ -28,6 +24,7 @@ def format_epilog(self, ctx, formatter): for line in self.epilog.split('\n'): formatter.write_text(line) + class ExtendedHelp(click.Command): def format_help(self, ctx, formatter): """Format click help to honor newline characters""" @@ -48,6 +45,7 @@ def format_help(self, ctx, formatter): for line in self.epilog.split('\n'): formatter.write_text(line) + class ExtendCLI(): def __init__(self, parameter_set=None, vars_input={}): self.vars = vars_input @@ -63,7 +61,8 @@ def process_options(self, parameters, func, is_required=False): numargs_unlimited_is_set = False numargs_unlimited_count = 0 numargs_unlimited_count_max = 1 - vanilla_parameters = dict([(k,v) for k,v in parameters.items() if not isinstance(parameters[k], dict)]) + vanilla_parameters = dict( + [(k, v) for k, v in parameters.items() if not isinstance(parameters[k], dict)]) if self.parameter_set: _parameters = parameters.get(self.parameter_set, {}) if _parameters: @@ -88,13 +87,16 @@ def process_options(self, parameters, func, is_required=False): first_option = option_string[0].strip() second_option = option_string[1].strip() numargs = 1 if numargs < 1 else numargs - option = click.option(first_option, second_option, value, type=str, nargs=numargs, help=option_help, required=is_required) + option = click.option(first_option, second_option, value, type=str, + nargs=numargs, help=option_help, required=is_required) else: if numargs_unlimited_is_set and not numargs_unlimited_count > numargs_unlimited_count_max: - option = click.argument(cli_option, nargs=numargs, required=is_required) + option = click.argument( + cli_option, nargs=numargs, required=is_required) else: numargs = 1 if numargs < 1 else numargs - option = click.option(cli_option, value, is_flag=True, default=False, help=option_help, required=is_required) + option = click.option( + cli_option, value, is_flag=True, default=False, help=option_help, required=is_required) func = option(func) numargs_unlimited_is_set = False numargs = 1 @@ -103,9 +105,11 @@ def process_options(self, parameters, func, is_required=False): def options(self, func): """Read dictionary of parameters, append these as additional options to incoming click function""" required_parameters = self.vars.get('required_parameters', {}) - extended_cli_func_required = self.process_options(required_parameters, func, is_required=True) + extended_cli_func_required = self.process_options( + required_parameters, func, is_required=True) optional_parameters = self.vars.get('optional_parameters', {}) - extended_cli_func = self.process_options(optional_parameters, extended_cli_func_required) + extended_cli_func = self.process_options( + optional_parameters, extended_cli_func_required) # if required_parameters: # self.logger.warning("The value for 'required_parameters' is invalid") return extended_cli_func diff --git a/lib/common/errorhandler/__init__.py b/ansible_taskrunner/lib/errorhandler/__init__.py similarity index 55% rename from lib/common/errorhandler/__init__.py rename to ansible_taskrunner/lib/errorhandler/__init__.py index aa490d1..e186490 100644 --- a/lib/common/errorhandler/__init__.py +++ b/ansible_taskrunner/lib/errorhandler/__init__.py @@ -6,10 +6,13 @@ ''' # see https://stackoverflow.com/questions/6234405/logging-uncaught-exceptions-in-python + + def catchException(logger, script_name, typ, value, traceback): - """Log uncaught exception to python logger""" - logger.critical("Program Crash") - logger.critical("Type: %s" % typ) - logger.critical("Value: %s" % value) - logger.critical("Traceback: %s" % traceback) - logger.info("Run same command but with %s --debug to see full stack trace!" % script_name) + """Log uncaught exception to python logger""" + logger.critical("Program Crash") + logger.critical("Type: %s" % typ) + logger.critical("Value: %s" % value) + logger.critical("Traceback: %s" % traceback) + logger.info( + "Run same command but with %s --debug to see full stack trace!" % script_name) diff --git a/lib/common/formatting/__init__.py b/ansible_taskrunner/lib/formatting/__init__.py similarity index 83% rename from lib/common/formatting/__init__.py rename to ansible_taskrunner/lib/formatting/__init__.py index 047012e..1838758 100644 --- a/lib/common/formatting/__init__.py +++ b/ansible_taskrunner/lib/formatting/__init__.py @@ -1,5 +1,5 @@ # Defaults -ansi_colors=""" +ansi_colors = """ RESTORE=$(echo -en '\033[0m') RED=$(echo -en '\033[00;31m') GREEN=$(echo -en '\033[00;32m') @@ -11,6 +11,9 @@ LIGHTGRAY=$(echo -en '\033[00;37m') """ +logging_format = "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s" + + def reindent(s, numSpaces): """Remove leading spaces from string see: Python Cookbook by David Ascher, Alex Martelli""" s = s.split('\n') diff --git a/lib/common/help/__init__.py b/ansible_taskrunner/lib/help/__init__.py similarity index 95% rename from lib/common/help/__init__.py rename to ansible_taskrunner/lib/help/__init__.py index f1d797b..e2b42a4 100644 --- a/lib/common/help/__init__.py +++ b/ansible_taskrunner/lib/help/__init__.py @@ -65,11 +65,11 @@ shell: bash source: |- echo 'Running Preflight Tasks!' - tasks run -d dbhost1 -w webhost1 -t value1 + tasks run -d dbhost1 -w webhost1 -t value1 tasks: - - debug: + - debug: msg: | Hello from Ansible! You specified: {{ some_value }} You specified: {{ another_value }} -''' \ No newline at end of file +''' diff --git a/ansible_taskrunner/lib/language/__init__.py b/ansible_taskrunner/lib/language/__init__.py new file mode 100644 index 0000000..cf034d3 --- /dev/null +++ b/ansible_taskrunner/lib/language/__init__.py @@ -0,0 +1,21 @@ +# Import builtins +import os +import sys + +# Import third-party and custom modules +try: + from superduperconfig import SuperDuperConfig +except ImportError as e: + print('Failed to import at least one required module') + print('Error was %s' % e) + print('Please install/update the required modules:') + print('pip install -U -r requirements.txt') + sys.exit(1) + + +def get_strings(): + self_path = os.path.dirname(os.path.abspath(__file__)) + __program_name__ = 'tasks' + superconf = SuperDuperConfig(__program_name__) + language_file = '%s/en.yaml' % self_path + return superconf.load_config(language_file) diff --git a/ansible_taskrunner/lib/language/en.yaml b/ansible_taskrunner/lib/language/en.yaml new file mode 100644 index 0000000..da2ae6c --- /dev/null +++ b/ansible_taskrunner/lib/language/en.yaml @@ -0,0 +1,2 @@ +strings: + info: diff --git a/lib/common/providers/__init__.py b/ansible_taskrunner/lib/providers/__init__.py similarity index 100% rename from lib/common/providers/__init__.py rename to ansible_taskrunner/lib/providers/__init__.py diff --git a/lib/common/providers/ansible/__init__.py b/ansible_taskrunner/lib/providers/ansible.py similarity index 61% rename from lib/common/providers/ansible/__init__.py rename to ansible_taskrunner/lib/providers/ansible.py index 10eaf61..115c13d 100644 --- a/lib/common/providers/ansible/__init__.py +++ b/ansible_taskrunner/lib/providers/ansible.py @@ -1,9 +1,7 @@ # Imports -import json import logging import os from os import fdopen, remove -import re import sys from tempfile import mkstemp @@ -15,12 +13,9 @@ # Import third-party and custom modules try: - if sys.version_info[0] >= 3: - from lib.py3 import click - else: - from lib.py2 import click - from lib.common.formatting import ansi_colors, reindent - from lib.common.yamlc import YamlCLIInvocation + import click + from formatting import ansi_colors, reindent + from yamlc import YamlCLIInvocation except ImportError as e: print('Failed to import at least one required module') print('Error was %s' % e) @@ -28,56 +23,66 @@ print('pip install -U -r requirements.txt') sys.exit(1) -class ProviderCLI(): - def __init__(self, parameter_set=None, vars_input={}): + +class ProviderCLI: + def __init__(self, parameter_set=None, vars_input=None): + if vars_input is None: + vars_input = {} self.vars = vars_input self.parameter_set = parameter_set self.logger = logger pass - def options(self, func): + @staticmethod + def options(func): """Add provider-specific click options""" - option = click.option('---debug', '---d', type=str, help='Start task run with ansible in debug mode', default=False, required=False) - func = option(func) - option = click.option('-v', count=True, help='Start task run with ansible in verbose mode', default=False, required=False) + option = click.option('---debug', '---d', type=str, help='Start task run with ansible in debug mode', + default=False, required=False) func = option(func) - option = click.option('---inventory', '---i', is_flag=False, help='Override embedded inventory specification', required=False) + option = click.option('---inventory', '---i', is_flag=False, help='Override embedded inventory specification', + required=False) func = option(func) return func - def invocation(self, - yaml_input_file=None, - string_vars=[], - default_vars={}, - paramset_var=None, - bash_functions=[], - cli_vars='', - yaml_vars={}, - list_vars=[], - debug=False, - args=None, - prefix='', - raw_args='', - kwargs={}): + @staticmethod + def invocation(yaml_input_file=None, + string_vars=[], + default_vars={}, + paramset_var=None, + bash_functions=[], + cli_vars='', + yaml_vars={}, + list_vars=[], + debug=False, + args=None, + prefix='', + raw_args='', + kwargs={}): """Invoke commands according to provider""" logger.info('Ansible Command Provider') - ansible_playbook_command = default_vars.get('ansible_playbook_command','ansible-playbook') + ansible_playbook_command = default_vars.get( + 'ansible_playbook_command', 'ansible-playbook') # Embedded inventory logic embedded_inventory = False inventory_input = kwargs.get('_inventory') embedded_inventory_string = yaml_vars.get('inventory') if not inventory_input and not embedded_inventory_string: - logger.error("Playbook does not contain an inventory declaration and no inventory was specified. Seek --help") + logger.error( + "Playbook does not contain an inventory declaration and no inventory was specified. Seek --help") sys.exit(1) elif inventory_input: ansible_inventory_file_path = inventory_input ansible_inventory_file_path_descriptor = None else: - ansible_inventory_file_path_descriptor, ansible_inventory_file_path = mkstemp(prefix='ansible-inventory', suffix='.tmp.ini') - logger.info("No inventory specified, so I'm using the embedded inventory from the playbook and writing a temporary inventory file %s (normally deleted after run)" % ansible_inventory_file_path) + ansible_inventory_file_path_descriptor, ansible_inventory_file_path = mkstemp(prefix='ansible-inventory', + suffix='.tmp.ini') + logger.info("No inventory specified") + logger.info("Writing a temporary inventory file %s (normally deleted after run)" + % ansible_inventory_file_path) inventory_input = embedded_inventory_string embedded_inventory = True - ansible_extra_options = ['-e {k}="{v}"'.format(k=key,v=value) for key,value in kwargs.items() if value] + ansible_extra_options = [ + '-e {k}="{v}"'.format(k=key, v=value) for key, value in kwargs.items() if value] ansible_extra_options.append('-e %s' % paramset_var) # Build command string pre_commands = ''' @@ -103,16 +108,16 @@ def invocation(self, deb=debug ) ansible_command = ''' - {apc} ${{__ansible_extra_options}} -i {inf} {opt} {arg} {raw} {ply} + {apc} ${{__ansible_extra_options}} -i {inf} {opt} {arg} {raw} {ply} '''.format( - apc = ansible_playbook_command, + apc=ansible_playbook_command, inf=ansible_inventory_file_path, opt=' '.join(ansible_extra_options), ply=yaml_input_file, arg=args, raw=raw_args ) - command = reindent(pre_commands + ansible_command,0) + command = reindent(pre_commands + ansible_command, 0) # Command invocation if prefix == 'echo': if debug: @@ -122,13 +127,15 @@ def invocation(self, YamlCLIInvocation().call(command) # Debugging if debug: - ansible_command_file_descriptor, ansible_command_file_path = mkstemp(prefix='ansible-command', suffix='.tmp.sh') - logger.debug('Ansible command file can be found here: %s' % ansible_command_file_path) - logger.debug('Ansible inventory file can be found here: %s' % ansible_inventory_file_path) + ansible_command_file_descriptor, ansible_command_file_path = mkstemp(prefix='ansible-command', + suffix='.tmp.sh') + logger.debug('Ansible command file can be found here: %s' % + ansible_command_file_path) + logger.debug('Ansible inventory file can be found here: %s' % + ansible_inventory_file_path) with fdopen(ansible_command_file_descriptor, "w") as f: f.write(command) else: if ansible_inventory_file_path_descriptor: os.close(ansible_inventory_file_path_descriptor) remove(ansible_inventory_file_path) - \ No newline at end of file diff --git a/lib/common/providers/bash/__init__.py b/ansible_taskrunner/lib/providers/bash.py similarity index 59% rename from lib/common/providers/bash/__init__.py rename to ansible_taskrunner/lib/providers/bash.py index dc6edad..2bce5e1 100644 --- a/lib/common/providers/bash/__init__.py +++ b/ansible_taskrunner/lib/providers/bash.py @@ -1,7 +1,5 @@ # Imports -import json import logging -import re import sys provider_name = 'bash' @@ -12,12 +10,9 @@ # Import third-party and custom modules try: - if sys.version_info[0] >= 3: - from lib.py3 import click - else: - from lib.py2 import click - from lib.common.formatting import ansi_colors, reindent - from lib.common.yamlc import YamlCLIInvocation + import click + from formatting import reindent + from yamlc import YamlCLIInvocation except ImportError as e: print('Failed to import at least one required module') print('Error was %s' % e) @@ -25,33 +20,33 @@ print('pip install -U -r requirements.txt') sys.exit(1) -class ProviderCLI(): + +class ProviderCLI: def __init__(self, parameter_set=None, vars_input={}): self.vars = vars_input self.parameter_set = parameter_set self.logger = logger pass - def options(self, func): + @staticmethod + def options(func): """Add provider-specific click options""" - option = click.option('--this-is-a-bash-switch', is_flag = True, default=False, required=False) - func = option(func) return func - def invocation(self, - yaml_input_file=None, - string_vars=[], - default_vars={}, - paramset_var=None, - bash_functions=[], - cli_vars='', - yaml_vars={}, - list_vars=[], - debug=False, - args=None, - prefix='', - raw_args='', - kwargs={}): + @staticmethod + def invocation(yaml_input_file=None, + string_vars=[], + default_vars={}, + paramset_var=None, + bash_functions=[], + cli_vars='', + yaml_vars={}, + list_vars=[], + debug=False, + args=None, + prefix='', + raw_args='', + kwargs={}): """Invoke commands according to provider""" logger.info('Bash Command Provider') command = ''' @@ -68,10 +63,10 @@ def invocation(self, bfn='\n'.join(bash_functions), deb=debug ) - command = reindent(command,0) + command = reindent(command, 0) # Command invocation if prefix == 'echo': logger.info("ECHO MODE ON") print(command) else: - YamlCLIInvocation().call(command) \ No newline at end of file + YamlCLIInvocation().call(command) diff --git a/ansible_taskrunner/lib/providers/vagrant.py b/ansible_taskrunner/lib/providers/vagrant.py new file mode 100644 index 0000000..abc5b7b --- /dev/null +++ b/ansible_taskrunner/lib/providers/vagrant.py @@ -0,0 +1,75 @@ +# Imports +import logging +import sys + +provider_name = 'vagrant' + +# Logging +logger = logging.getLogger('logger') +logger.setLevel(logging.INFO) + +# Import third-party and custom modules +try: + import click + from formatting import reindent + from yamlc import YamlCLIInvocation +except ImportError as e: + print('Failed to import at least one required module') + print('Error was %s' % e) + print('Please install/update the required modules:') + print('pip install -U -r requirements.txt') + sys.exit(1) + + +class ProviderCLI: + def __init__(self, parameter_set=None, vars_input={}): + self.vars = vars_input + self.parameter_set = parameter_set + self.logger = logger + pass + + @staticmethod + def options(func): + """Add provider-specific click options""" + option = click.option('--is-vagrant', + is_flag=True, default=False, required=False) + func = option(func) + return func + + @staticmethod + def invocation(yaml_input_file=None, + string_vars=[], + default_vars={}, + paramset_var=None, + bash_functions=[], + cli_vars='', + yaml_vars={}, + list_vars=[], + debug=False, + args=None, + prefix='', + raw_args='', + kwargs={}): + """Invoke commands according to provider""" + logger.info('Vagrant Command Provider') + command = ''' + {dsv} + {psv} + {dlv} + {clv} + {bfn} + '''.format( + dlv='\n'.join(list_vars), + dsv='\n'.join(string_vars), + psv=paramset_var, + clv=cli_vars, + bfn='\n'.join(bash_functions), + deb=debug + ) + command = reindent(command, 0) + # Command invocation + if prefix == 'echo': + logger.info("ECHO MODE ON") + print(command) + else: + YamlCLIInvocation().call(command) diff --git a/ansible_taskrunner/lib/superduperconfig/__init__.py b/ansible_taskrunner/lib/superduperconfig/__init__.py new file mode 100644 index 0000000..368cba2 --- /dev/null +++ b/ansible_taskrunner/lib/superduperconfig/__init__.py @@ -0,0 +1,72 @@ +import logging +import os +import sys +import yaml + +# Logging +logger = logging.getLogger('logger') +logger.setLevel(logging.INFO) + + +class SuperDuperConfig(): + + def __init__(self, prog_name): + self.program_name = prog_name + self.logger = logger + pass + + def load_config(self, config_file, req_keys=[], failfast=False, data_key=None, debug=False): + """ Load config file + """ + config_path_strings = [ + os.path.realpath(os.path.expanduser( + os.path.join('~', '.%s' % self.program_name))), + '.', '/etc/%s' % self.program_name + ] + config_paths = [os.path.join(p, config_file) + for p in config_path_strings] + config_found = False + config_is_valid = False + for config_path in config_paths: + config_exists = os.path.exists(config_path) + if config_exists: + config_found = True + try: + with open(config_path, 'r') as ymlfile: + cfg = yaml.load(ymlfile, yaml.Loader) + config_dict = cfg[data_key] if data_key is not None else cfg + config_is_valid = all([m[m.keys()[0]].get(k) + for k in req_keys for m in config_dict]) + self.logger.info( + "Found input file - {cf}".format(cf=config_path)) + if not config_is_valid: + logger.warning( + """At least one required key was not defined in your input file: {cf}.""".format( + cf=config_path) + ) + self.logger.warning( + "Review the available documentation or consult --help") + config_file = config_path + break + except Exception as e: + self.logger.warning( + "I encountered a problem reading your input file: {cp}, error was {err}".format( + cp=config_path, err=str(e)) + ) + if not config_found: + if failfast: + self.logger.error("Could not find %s. Aborting." % config_file) + sys.exit(1) + else: + self.logger.warning( + "Could not find %s, not loading values" % config_file) + + if config_found and config_is_valid: + return config_dict + else: + if failfast: + self.logger.error( + "Config %s is invalid. Aborting." % config_file) + sys.exit(1) + else: + return {} diff --git a/examples/custom-cli-provider/plugins/providers/example/__init__.py b/ansible_taskrunner/lib/vagrant/__init__.py similarity index 50% rename from examples/custom-cli-provider/plugins/providers/example/__init__.py rename to ansible_taskrunner/lib/vagrant/__init__.py index 87afab8..b838ad5 100644 --- a/examples/custom-cli-provider/plugins/providers/example/__init__.py +++ b/ansible_taskrunner/lib/vagrant/__init__.py @@ -1,10 +1,8 @@ # Imports -import json import logging -import re import sys -provider_name = 'example' +provider_name = 'vagrant' # Logging logger = logging.getLogger('logger') @@ -12,12 +10,9 @@ # Import third-party and custom modules try: - if sys.version_info[0] >= 3: - from lib.py3 import click - else: - from lib.py2 import click - from lib.common.formatting import ansi_colors, reindent - from lib.common.yamlc import YamlCLIInvocation + import click + from .. import reindent + from .. import YamlCLIInvocation except ImportError as e: print('Failed to import at least one required module') print('Error was %s' % e) @@ -25,49 +20,56 @@ print('pip install -U -r requirements.txt') sys.exit(1) -class ProviderCLI(): + +class ProviderCLI: def __init__(self, parameter_set=None, vars_input={}): self.vars = vars_input self.parameter_set = parameter_set self.logger = logger pass - def options(self, func): - option = click.option('--this-is-an-example-switch', is_flag = True, default=False, required=False) + @staticmethod + def options(func): + """Add provider-specific click options""" + option = click.option('--is-vagrant', + is_flag=True, default=False, required=False) func = option(func) return func - def invocation(self, - yaml_input_file=None, - string_vars=[], - default_vars={}, - paramset_var=None, - bash_functions=[], - cli_vars='', - yaml_vars={}, - list_vars=[], - debug=False, - args=None, - prefix='', - raw_args='', - kwargs={}): - logger.info('Example Command Provider') + @staticmethod + def invocation(yaml_input_file=None, + string_vars=[], + default_vars={}, + paramset_var=None, + bash_functions=[], + cli_vars='', + yaml_vars={}, + list_vars=[], + debug=False, + args=None, + prefix='', + raw_args='', + kwargs={}): + """Invoke commands according to provider""" + logger.info('Vagrant Command Provider') command = ''' {dsv} + {psv} {dlv} {clv} {bfn} '''.format( dlv='\n'.join(list_vars), dsv='\n'.join(string_vars), + psv=paramset_var, clv=cli_vars, bfn='\n'.join(bash_functions), deb=debug ) - command = reindent(command,0) + command = reindent(command, 0) # Command invocation if prefix == 'echo': logger.info("ECHO MODE ON") print(command) else: - YamlCLIInvocation().call(command) \ No newline at end of file + YamlCLIInvocation().call(command) diff --git a/ansible_taskrunner/lib/yamlc/__init__.py b/ansible_taskrunner/lib/yamlc/__init__.py new file mode 100644 index 0000000..35c2200 --- /dev/null +++ b/ansible_taskrunner/lib/yamlc/__init__.py @@ -0,0 +1,73 @@ +import os +import sys +from subprocess import PIPE, Popen, STDOUT + + +class YamlCLIInvocation: + + def __init__(self): + self.invocation = type('obj', (object,), + { + 'stdout': None, + 'failed': False, + 'returncode': 0 + } + ) + + @staticmethod + def which(program): + """ + Returns the fully-qualified path to the specified binary + """ + def is_exe(filepath): + if sys.platform == 'win32': + filepath = filepath.replace('\\', '/') + for exe in [filepath, filepath + '.exe']: + if all([os.path.isfile(exe), os.access(exe, os.X_OK)]): + return True + else: + return os.path.isfile(filepath) and os.access(filepath, os.X_OK) + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + return None + + def call(self, cmd): + """Call specified command using subprocess library""" + bash_binary = self.which('bash') + # Execute the command, catching failures + try: + if sys.version_info[0] >= 3: + # Invoke process and poll for new output until finished + with Popen([bash_binary, '-c', cmd], stdout=PIPE, stderr=STDOUT, bufsize=1, universal_newlines=True) as p: + for line in p.stdout: + sys.stdout.write(line) # process line here + if p.returncode != 0: + self.invocation.failed = True + self.invocation.returncode = p.returncode + self.invocation.stdout = 'Encountered error code {errcode} in the specified command {args}'.format( + errcode=p.returncode, args=p.args) + return self.invocation + else: + # Invoke process + process = Popen( + [bash_binary, '-c', cmd], + stdout=PIPE, + stderr=STDOUT) + # Poll for new output until finished + while True: + nextline = process.stdout.readline() + if nextline == '' and process.poll() is not None: + break + sys.stdout.write(nextline) + sys.stdout.flush() + except Exception as e: + print( + 'Encountered error in the specified command, error was {err}'.format(err=e)) diff --git a/lib/common/yamlr/__init__.py b/ansible_taskrunner/lib/yamlr/__init__.py similarity index 90% rename from lib/common/yamlr/__init__.py rename to ansible_taskrunner/lib/yamlr/__init__.py index 009d2ae..0da6297 100644 --- a/lib/common/yamlr/__init__.py +++ b/ansible_taskrunner/lib/yamlr/__init__.py @@ -1,6 +1,5 @@ from functools import reduce -import os -import yaml + class YamlReader: @@ -11,7 +10,8 @@ def deep_get(self, data_input, keys, default=None): """Recursively retrieve values from a dictionary object""" result = '' if isinstance(data_input, dict): - result = reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split('.'), data_input) + result = reduce(lambda d, key: d.get(key, default) if isinstance( + d, dict) else default, keys.split('.'), data_input) return(result) def get(self, yaml_input, dict_path, default_data=''): @@ -33,4 +33,4 @@ def get(self, yaml_input, dict_path, default_data=''): except Exception as e: raise(e) else: - return default_data \ No newline at end of file + return default_data diff --git a/plugins/__init__.py b/ansible_taskrunner/plugins/__init__.py similarity index 80% rename from plugins/__init__.py rename to ansible_taskrunner/plugins/__init__.py index 1e7f3d1..b4350ad 100644 --- a/plugins/__init__.py +++ b/ansible_taskrunner/plugins/__init__.py @@ -8,10 +8,11 @@ except NameError: FileNotFoundError = IOError + class Plugin: """ Plugin System """ - def __init__(self, PluginFolder="./plugins", MainModule="__init__.py", provider='default'): + def __init__(self, PluginFolder="plugins", MainModule="__init__.py", provider='ansible'): self.PluginFolder = PluginFolder self.Provider = provider self.MainModule = MainModule @@ -29,14 +30,18 @@ def getPlugins(self, PluginFolder): except (AttributeError, FileNotFoundError, ImportError, NotImplementedError): continue plugin_type = (os.path.split(root)[-1]) - plugins.append({'type': plugin_type, 'path': os.path.join(root, plugin, self.MainModule)}) + plugins.append({'type': plugin_type, 'path': os.path.join( + root, plugin, self.MainModule)}) return plugins # + def path_import2(self, absolute_path): - spec = importlib.util.spec_from_file_location(absolute_path, absolute_path) - module = spec.loader.load_module(spec.name) - return module + spec = importlib.util.spec_from_file_location( + absolute_path, absolute_path) + module = spec.loader.load_module(spec.name) + return module # + def activatePlugins(self, PluginFolder=None): PluginFolder = PluginFolder or self.PluginFolder plugin_objects = { @@ -45,4 +50,4 @@ def activatePlugins(self, PluginFolder=None): for plugin_object in self.getPlugins(PluginFolder): plugin_instance = self.path_import2(plugin_object['path']) plugin_objects[plugin_object['type']].append(plugin_instance) - return type('obj', (object,), plugin_objects) \ No newline at end of file + return type('obj', (object,), plugin_objects) diff --git a/build.yaml b/build.yaml index 5573393..f969c5c 100644 --- a/build.yaml +++ b/build.yaml @@ -1,8 +1,12 @@ --- - hosts: local vars: + app_dir: ansible_taskrunner + lib_dir: ${app_dir}/lib + plugins_dir: ${app_dir}/plugins + workdir: ${PWD} help: - readme: | + message: | Task Runner for the Task Runner! examples: inventory: | @@ -12,41 +16,50 @@ -b: build optional_parameters: -bp|--build-and-push: deployment_host_and_path + -x|--replace-exe: REPLACE_EXE functions: build: shell: bash source: |- set -o errexit + cd ${app_dir} for pyver in py2 py3;do if ! test -d lib/${pyver};then mkdir lib/${pyver} source activate $pyver - pip install -t lib/${pyver} -r requirments + pip install -t lib/${pyver} -r ${workdir}/requirments.txt fi done - __version=$(egrep '.*__version__ =' tasks.py | cut -d\ -f3 | tr -d "'") + __version=$(egrep '.*__version__ =' __init__.py | cut -d\ -f3 | tr -d "'") echo "Version is ${__version}" - __release_dir=release/${__version} - lint_result=$(./tasks.py --help) + __release_dir=../release/${__version} + lint_result=$(python cli.py --help) echo "Initial lint OK, proceeding with build" if [[ "$OSTYPE" =~ .*msys.* ]];then echo "OSType is Windows, nesting libdir ..." - mkdir windows + if test -d windows;then + rm -rf windows + else + mkdir windows + fi cp -r lib plugins windows echo "Creating zip-app" - make-zipapp -f tasks.py -X __pycache__ -x .pyc -d windows + make-zipapp -f cli.py -X __pycache__ -x .pyc -d windows if test -d windows;then rm -rf windows;fi else echo "OSType is most likely POSIX native" echo "Creating zip-app" - ../make-zipapp/make-zipapp.py -f tasks.py -d lib + make-zipapp -f cli.py -X __pycache__ -x .pyc fi - lint_result=$(./tasks.py --help) + mv cli tasks + lint_result=$(tasks --help) echo "Initial lint OK, proceeding with release" if ! test -d ${__release_dir};then mkdir -p ${__release_dir};fi mv -f tasks ${__release_dir} - echo "Replacing current executable: $(which tasks)" - yes | cp ${__release_dir}/tasks $(which tasks) + if [[ -n $REPLACE_EXE ]];then + echo "Replacing current executable: $(which tasks)" + yes | cp ${__release_dir}/tasks $(which tasks) + fi if [[ -n $deployment_host_and_path ]];then echo "Pushing up" scp_result=$(scp ${__release_dir}/tasks ${deployment_host_and_path}) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..07d852d --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = ansible_taskrunner +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..426bac5 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..9e8f401 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# ansible_taskrunner documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 9 13:47:02 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another +# directory, add these directories to sys.path here. If the directory is +# relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +import ansible_taskrunner + +# -- General configuration --------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'ansible-taskrunner' +copyright = u"2019, Engelbert Tejeda" +author = u"Engelbert Tejeda" + +# The version info for the project you're documenting, acts as replacement +# for |version| and |release|, also used in various other places throughout +# the built documents. +# +# The short X.Y version. +version = ansible_taskrunner.__version__ +# The full version, including alpha/beta/rc tags. +release = ansible_taskrunner.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a +# theme further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output --------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'ansible_taskrunnerdoc' + + +# -- Options for LaTeX output ------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'ansible_taskrunner.tex', + u'ansible-taskrunner Documentation', + u'Engelbert Tejeda', 'manual'), +] + + +# -- Options for manual page output ------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'ansible_taskrunner', + u'ansible-taskrunner Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'ansible_taskrunner', + u'ansible-taskrunner Documentation', + author, + 'ansible_taskrunner', + 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..819f45e --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 0000000..e4e52cc --- /dev/null +++ b/docs/history.rst @@ -0,0 +1 @@ +.. include:: ../HISTORY.rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..b3557f4 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +Welcome to ansible-taskrunner's documentation! +====================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + readme + installation + usage + modules + contributing + authors + history + +Indices and tables +================== +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..a4536b0 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,51 @@ +.. highlight:: shell + +============ +Installation +============ + + +Stable release +-------------- + +To install ansible-taskrunner, run this command in your terminal: + +.. code-block:: console + + $ pip install ansible_taskrunner + +This is the preferred method to install ansible-taskrunner, as it will always install the most recent stable release. + +If you don't have `pip`_ installed, this `Python installation guide`_ can guide +you through the process. + +.. _pip: https://pip.pypa.io +.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ + + +From sources +------------ + +The sources for ansible-taskrunner can be downloaded from the `Github repo`_. + +You can either clone the public repository: + +.. code-block:: console + + $ git clone git://github.com/berttejeda/ansible_taskrunner + +Or download the `tarball`_: + +.. code-block:: console + + $ curl -OL https://github.com/berttejeda/ansible_taskrunner/tarball/master + +Once you have a copy of the source, you can install it with: + +.. code-block:: console + + $ python setup.py install + + +.. _Github repo: https://github.com/berttejeda/ansible_taskrunner +.. _tarball: https://github.com/berttejeda/ansible_taskrunner/tarball/master diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..b833cc7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=ansible_taskrunner + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 0000000..de17838 --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..15fb3ee --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,7 @@ +===== +Usage +===== + +To use ansible-taskrunner in a project:: + + import ansible_taskrunner diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/examples/ansible/README.md b/examples/ansible/README.md deleted file mode 100644 index c2ed860..0000000 --- a/examples/ansible/README.md +++ /dev/null @@ -1,87 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Ansible Example](#ansible-example) -- [Exercises](#exercises) - - [Obtain Project Files](#obtain-project-files) - - [Mock Test](#mock-test) - - [Copy file(s) to target host](#copy-files-to-target-host) - - [Equivalent ansible-playbook command](#equivalent-ansible-playbook-command) -- [Learning Points](#learning-points) -- [Caveats](#caveats) - - - - -# Ansible Example - -The Tasksfile here is an ansible playbook that copies a file/directory to a destination path on the target machine(s). - - -# Exercises - - -## Obtain Project Files - -* Download the latest [release](https://github.com/berttejeda/ansible-taskrunner/releases)
-* Clone the git repository and navigate to the example
- -``` -git clone https://github.com/berttejeda/ansible-taskrunner.git -cd ansible-taskrunner/exercises/ansible -``` - - -## Mock Test - -* Let's run the `mock_test` embedded function
-``` -tasks run -h localhost -p test -t /tmp --mock -``` - - -## Copy file(s) to target host - -* Try a typical use case
-``` -mkdir -p ${HOME}/some/directory -tasks run -h some-host.somedomain -p ${HOME}/some/directory -t /tmp -``` - - -## Equivalent ansible-playbook command - -* Let's echo the underlying ansible command
-``` -tasks run -h localhost -p ${HOME}/some/directory -t /tmp --echo -``` - -* The output should be similar to:
- -``` -ansible-playbook ${__ansible_extra_options} -i /tmp/ansible-inventoryrUXgPX.tmp.ini -e target_hosts="localhost" -e echo="True" -e target_path="/tmp" -e local_path="/home/vagrant/some/directory" -e parameter_set=False Taskfile.yaml -``` - -The above command makes use of an ephemeral inventory file that is dynamically created at runtime. -Let's adjust the above by utilizing an inline inventory specification instead, as follows: - -``` -mkdir -p ${HOME}/some/directory -ansible-playbook ${__ansible_extra_options} -i localhost, -e target_hosts="localhost" -e echo="True" -e target_path="/tmp" -e local_path="/home/vagrant/some/directory" -e parameter_set=False Taskfile.yaml -``` - - -# Learning Points - -- You are able to launch the `ansible-playbook` command using a meta step that makes it trivial to alter playbook execution behavior -- Easily adjust commandline options (just shuffle things around in yaml) -- Much easier than running the `ansible-playbook` with a long list of `--extra-vars` (`-e`) - - -# Caveats - -- The example here is strictly for elucidating what's really happening under the hood, so to speak. -- I shy away from playbooks with an 'all' _hosts_ designation. -- As with any piece of automation, you can do some serious damage given the right set of tasks. -- Please, for the love of FSM, make sure you know what you're doing 😎 \ No newline at end of file diff --git a/examples/ansible/Taskfile.yaml b/examples/ansible/Taskfile.yaml deleted file mode 100644 index fe9ac59..0000000 --- a/examples/ansible/Taskfile.yaml +++ /dev/null @@ -1,54 +0,0 @@ ---- -- hosts: all - gather_facts: true - become: true - vars: - DATETIME: "{{ [ansible_date_time.year, ansible_date_time.month, ansible_date_time.day, ansible_date_time.hour, ansible_date_time.minute] | join('') }}" - help: - message: | - Copy local file(s)/folder(s) from local to target host(s) - epilog: | - After playbook run, the specified file system objects should - be mirrored onto the target host(s) - examples: - - You want to synchronize files to your target hosts: | - tasks run -p myfolder -h host1 -t /data - - You want to synchronize mock data: | - tasks run -p myfolder -h host1 -t /data --mock - inventory: | - [target_hosts] - $(echo -e "${target_hosts}" | tr ',' '\n') - [all_hosts:children] - target_hosts - required_parameters: - -h|--target-hosts: target_hosts - -p|--local-path: local_path - -t|--target-path: target_path - optional_parameters: - --mock: mock_test - functions: - mock_test: - shell: bash - source: |- - echo 'Mock mode!' - if ! test -f ${local_path}/mock_data;then - mkdir -p ${local_path}/mock_data - echo myfile1 > ${local_path}/mock_data/myfile1.txt - echo myfile2 > ${local_path}/mock_data/myfile2.txt - echo myfile3 > ${local_path}/mock_data/myfile3.txt - fi - tasks run -p ${local_path} -t ${target_path} -h ${target_hosts} - tasks: - - - name: Ensure target directory exists on host - file: - path: '{{ target_path }}' - state: directory - - - name: Copy files - synchronize: - src: '{{ local_path }}' - dest: '{{ target_path }}' - rsync_opts: - - "--no-motd" -... diff --git a/examples/ansible/config.yaml b/examples/ansible/config.yaml deleted file mode 100644 index bce7960..0000000 --- a/examples/ansible/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -cli: - providers: - default: ansible -... diff --git a/examples/bash/README.md b/examples/bash/README.md deleted file mode 100644 index e6cb915..0000000 --- a/examples/bash/README.md +++ /dev/null @@ -1,53 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Bash Example](#bash-example) -- [Exercises](#exercises) - - [Obtain Project Files](#obtain-project-files) - - [Ping Google Function](#ping-google-function) -- [Learning Points](#learning-points) -- [Caveats](#caveats) - - - - -# Bash Example - -The Tasksfile here consists of an ansible playbook that has been repurposed as a bastardized bash script. - -Essentially, it's a stripped down ansible playbook that holds bash functions represented in YAML syntax. - - -# Exercises - - -## Obtain Project Files - -* Download the latest [release](https://github.com/berttejeda/ansible-taskrunner/releases)
-* Clone the git repository and navigate to the example
- -``` -git clone https://github.com/berttejeda/ansible-taskrunner.git -cd ansible-taskrunner/exercises/bash -``` - - -## Ping Google Function - -* Let's run the `ping_google` embedded function
-``` -tasks run --ping-google -``` - - -# Learning Points - -- You are able to easily adjust cli options for your YAML-organized Bash script :) -- You can get pretty creative with how you proceed with this approach - - -# Caveats - -- As with any piece of automation, you can do some serious damage given the right set of commands and functions. -- Please, for the love of FSM, make sure you know what you're doing 😎 \ No newline at end of file diff --git a/examples/bash/Taskfile.yaml b/examples/bash/Taskfile.yaml deleted file mode 100644 index 5bdbcdb..0000000 --- a/examples/bash/Taskfile.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -- hosts: all - gather_facts: true - become: true - vars: - help: - message: | - This is essentially a stripped down ansible playbook that holds bash functions represented in YAML syntax - The neat part about this is that you get to easily adjust cli options for your YAML-organized Bash script :) - epilog: - examples: - - You want to ping google's dns servers: | - tasks run -h 8.8.8.8 - - You want to run the ping_google embedded function: | - tasks run --ping-google - required_parameters: - optional_parameters: - -h|--target-hosts: target_hosts - --ping-google: ping_google - functions: - ping_google: - shell: bash - source: |- - echo Attempting to ping Google DNS ... - if ping -c 1 -w 1 8.8.8.8 > /dev/null;then - echo ok - else - echo ping failed - fi -... diff --git a/examples/bash/config.yaml b/examples/bash/config.yaml deleted file mode 100644 index f1df81c..0000000 --- a/examples/bash/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -cli: - providers: - default: bash -... diff --git a/examples/custom-cli-provider/README.md b/examples/custom-cli-provider/README.md deleted file mode 100644 index bce6a85..0000000 --- a/examples/custom-cli-provider/README.md +++ /dev/null @@ -1,70 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Custom CLI Provider Example](#custom-cli-provider-example) -- [Exercises](#exercises) - - [Obtain Project Files](#obtain-project-files) - - [Show help message](#show-help-message) -- [Learning Points](#learning-points) -- [Caveats](#caveats) - - - - -# Custom CLI Provider Example - -**Note**: This exercise only works with python 3 (for now) -**TODO**: Refactor plugin logic to work with python version 2.x - -Here we employ a custom cli-provider named _example_ - -The cli-provider is located in the [plugins](plugins) directory adjacent to this document. - -The Tasksfile here consists of an ansible playbook that has been repurposed as a bastardized bash script. - -Essentially, it's a stripped down ansible playbook that holds bash functions represented in YAML syntax. - -The exercise here showcases how one can extend the functionality of `tasks`, the ansible task runner command -through the use of custom cli-provider plugins. - - -# Exercises - - -## Obtain Project Files - -* Download the latest [release](https://github.com/berttejeda/ansible-taskrunner/releases)
-* Clone the git repository and navigate to the example
- -``` -git clone https://github.com/berttejeda/ansible-taskrunner.git -cd ansible-taskrunner/exercises/custom-cli-provider -``` - -## Show help message - -* Let's review the output of the the `run` subcommand help
-``` -tasks run --help -``` - -You should see `--this-is-an-example-switch` in the option listing. - -You can get creative by adjusting the `invocation` function in the [example](plugins/providers/example) plugin. - -It's all python, so code away to your heart's content! - - -# Learning Points - -- You are able to easily adjust cli options for your YAML-organized Bash script :) -- You are able to override the default handler for command and cli-options through a plugin system -- You can get pretty creative with how you proceed with this approach - - -# Caveats - -- Again, the plugin system is only working for python 3, so don't expect this to work if your python version is 2.x. -- As with any piece of automation, you can do some serious damage given the right set of commands and functions. -- Please, for the love of FSM, make sure you know what you're doing 😎 \ No newline at end of file diff --git a/examples/custom-cli-provider/Taskfile.yaml b/examples/custom-cli-provider/Taskfile.yaml deleted file mode 100644 index 5bdbcdb..0000000 --- a/examples/custom-cli-provider/Taskfile.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -- hosts: all - gather_facts: true - become: true - vars: - help: - message: | - This is essentially a stripped down ansible playbook that holds bash functions represented in YAML syntax - The neat part about this is that you get to easily adjust cli options for your YAML-organized Bash script :) - epilog: - examples: - - You want to ping google's dns servers: | - tasks run -h 8.8.8.8 - - You want to run the ping_google embedded function: | - tasks run --ping-google - required_parameters: - optional_parameters: - -h|--target-hosts: target_hosts - --ping-google: ping_google - functions: - ping_google: - shell: bash - source: |- - echo Attempting to ping Google DNS ... - if ping -c 1 -w 1 8.8.8.8 > /dev/null;then - echo ok - else - echo ping failed - fi -... diff --git a/examples/custom-cli-provider/config.yaml b/examples/custom-cli-provider/config.yaml deleted file mode 100644 index ec89d58..0000000 --- a/examples/custom-cli-provider/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -cli: - providers: - default: example -... diff --git a/examples/custom-cli-provider/dynamic-import.py b/examples/custom-cli-provider/dynamic-import.py deleted file mode 100644 index 9cbfbaf..0000000 --- a/examples/custom-cli-provider/dynamic-import.py +++ /dev/null @@ -1,8 +0,0 @@ -import imp -from imp_get_suffixes import module_types - -print 'Package:' -f, filename, description = imp.find_module('example') -print module_types[description[2]], filename -print - diff --git a/examples/vagrant/.vagrant/rgloader/loader.rb b/examples/vagrant/.vagrant/rgloader/loader.rb deleted file mode 100644 index c3c05b0..0000000 --- a/examples/vagrant/.vagrant/rgloader/loader.rb +++ /dev/null @@ -1,9 +0,0 @@ -# This file loads the proper rgloader/loader.rb file that comes packaged -# with Vagrant so that encoded files can properly run with Vagrant. - -if ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"] - require File.expand_path( - "rgloader/loader", ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"]) -else - raise "Encoded files can't be read outside of the Vagrant installer." -end diff --git a/examples/vagrant/README.md b/examples/vagrant/README.md deleted file mode 100644 index e5b7957..0000000 --- a/examples/vagrant/README.md +++ /dev/null @@ -1,70 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Vagrant Command Example](#vagrant-command-example) -- [Exercises](#exercises) - - [Obtain Project Files](#obtain-project-files) - - [Bring up Ubuntu Xenial 64 Box](#bring-up-ubuntu-xenial-64-box) - - [Bring up an Oracle Enterprise Linux 64 Box](#bring-up-an-oracle-enterprise-linux-64-box) -- [Learning Points](#learning-points) -- [Caveats](#caveats) - - - - -# Vagrant Command Example - -The Tasksfile here consists of an ansible playbook that has been repurposed as a bastardized bash script. - -We've also made it into a meta-automation step to the `vagrant` command. - -Essentially, this is a: -- Stripped down ansible playbook that holds bash functions represented in YAML syntax -- Embedded functions are meant to facilitate interaction with the vagrant command, e.g. providing some custom cli options and the like - - -# Exercises - - -## Obtain Project Files - -* Download the latest [release](https://github.com/berttejeda/ansible-taskrunner/releases)
-* Clone the git repository and navigate to the example
- -``` -git clone https://github.com/berttejeda/ansible-taskrunner.git -cd ansible-taskrunner/exercises/bash -``` - - -## Bring up an Ubuntu Xenial 64 Box - -* Let's fire up an Ubuntu Xenial 64 Virtual Machine
-``` -tasks run --xenial -``` - - -## Bring up an Oracle Enterprise Linux 64 Box - -* Let's fire up an Oracle Enterprise Linux 64 Virtual Machine
-``` -tasks run --oel76 -``` - - -# Learning Points - -- You are able to easily adjust cli options for your YAML-organized Bash script :) -- You are able to seamlessly interact with the vagrant command -- You can get pretty creative with how you proceed with this approach -- This is how I'm managing my vagrant environments at the moment - - See: - - -# Caveats - -- Consider this **experimental**, as I have not had extensive experience vetting the possible *gotchas* with this configuration (i.e. calling the `vagrant` command via python **subprocess**) -- As with any piece of automation, you can do some serious damage given the right set of commands and functions. -- Please, for the love of FSM, make sure you know what you're doing 😎 \ No newline at end of file diff --git a/examples/vagrant/Taskfile.yaml b/examples/vagrant/Taskfile.yaml deleted file mode 100644 index 32c0ab0..0000000 --- a/examples/vagrant/Taskfile.yaml +++ /dev/null @@ -1,40 +0,0 @@ ---- -- hosts: all - gather_facts: true - become: true - vars: - oel76_box_url: https://yum.oracle.com/boxes/oraclelinux/ol76/ol76.box - oel76_box_name: ubuntu/xenial64 - ubuntu_xenial_box_name: ubuntu/xenial64 - help: - message: | - Essentially, this is a: - - Stripped down ansible playbook that holds bash functions represented in YAML syntax - - Embedded functions are meant to facilitate interaction with the vagrant command, e.g. providing some custom cli options and the like - epilog: - examples: - - You want to fire up an Ubuntu Xenial x64 machine: | - tasks run --xenial - - You want to fire up an Oracle Enterprise Linux 7.6 x64 machine: | - tasks run --oel76 - required_parameters: - optional_parameters: - -h|--target-hosts: target_hosts - --xenial: vagrant_up_xenial - --oel76: vagrant_up_oel76 - functions: - vagrant_up_oel76: - shell: bash - source: |- - vagrant box add --name ${oel76_box_name} ${oel76_box_url} - vagrant init ol76 - echo Bringing oracle vm up ... - vagrant up - vagrant_up_xenial: - shell: bash - source: |- - vagrant box add ${ubuntu_xenial_box_name} - vagrant init ${ubuntu_xenial_box_name} - echo Bringing Ubuntu vm up ... - vagrant up -... diff --git a/examples/vagrant/Vagrantfile b/examples/vagrant/Vagrantfile deleted file mode 100644 index 019cefe..0000000 --- a/examples/vagrant/Vagrantfile +++ /dev/null @@ -1,70 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# All Vagrant configuration is done below. The "2" in Vagrant.configure -# configures the configuration version (we support older styles for -# backwards compatibility). Please don't change it unless you know what -# you're doing. -Vagrant.configure("2") do |config| - # The most common configuration options are documented and commented below. - # For a complete reference, please see the online documentation at - # https://docs.vagrantup.com. - - # Every Vagrant development environment requires a box. You can search for - # boxes at https://vagrantcloud.com/search. - config.vm.box = "ubuntu/xenial64" - - # Disable automatic box update checking. If you disable this, then - # boxes will only be checked for updates when the user runs - # `vagrant box outdated`. This is not recommended. - # config.vm.box_check_update = false - - # Create a forwarded port mapping which allows access to a specific port - # within the machine from a port on the host machine. In the example below, - # accessing "localhost:8080" will access port 80 on the guest machine. - # NOTE: This will enable public access to the opened port - # config.vm.network "forwarded_port", guest: 80, host: 8080 - - # Create a forwarded port mapping which allows access to a specific port - # within the machine from a port on the host machine and only allow access - # via 127.0.0.1 to disable public access - # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" - - # Create a private network, which allows host-only access to the machine - # using a specific IP. - # config.vm.network "private_network", ip: "192.168.33.10" - - # Create a public network, which generally matched to bridged network. - # Bridged networks make the machine appear as another physical device on - # your network. - # config.vm.network "public_network" - - # Share an additional folder to the guest VM. The first argument is - # the path on the host to the actual folder. The second argument is - # the path on the guest to mount the folder. And the optional third - # argument is a set of non-required options. - # config.vm.synced_folder "../data", "/vagrant_data" - - # Provider-specific configuration so you can fine-tune various - # backing providers for Vagrant. These expose provider-specific options. - # Example for VirtualBox: - # - # config.vm.provider "virtualbox" do |vb| - # # Display the VirtualBox GUI when booting the machine - # vb.gui = true - # - # # Customize the amount of memory on the VM: - # vb.memory = "1024" - # end - # - # View the documentation for the provider you are using for more - # information on available options. - - # Enable provisioning with a shell script. Additional provisioners such as - # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the - # documentation for more information about their specific syntax and use. - # config.vm.provision "shell", inline: <<-SHELL - # apt-get update - # apt-get install -y apache2 - # SHELL -end diff --git a/examples/vagrant/config.yaml b/examples/vagrant/config.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/lib/common/yamlc/__init__.py b/lib/common/yamlc/__init__.py deleted file mode 100644 index fe7450a..0000000 --- a/lib/common/yamlc/__init__.py +++ /dev/null @@ -1,93 +0,0 @@ -import platform -import os -import re -import sys -from subprocess import PIPE, Popen, STDOUT - -class YamlCLIInvocation: - - def __init__(self, **kwargs): - self.invocation = type('obj', (object,), - { - 'stdout' : None, - 'failed' : False, - 'returncode' : 0 - } - ) - - def get_distribution(self): - """Return the OS distribution name""" - if platform.system() == 'Linux': - try: - supported_dists = platform._supported_dists + ('arch', 'alpine', 'devuan') - distribution = platform.linux_distribution(supported_dists=supported_dists)[0].capitalize() - if not distribution and os.path.isfile('/etc/system-release'): - distribution = platform.linux_distribution(supported_dists=['system'])[0].capitalize() - if 'Amazon' in distribution: - distribution = 'Amazon' - else: - distribution = 'OtherLinux' - except: - distribution = sys.platform.dist()[0].capitalize() - else: - distribution = None - return distribution - - def which(self, program): - ''' - Returns the fully-qualified path to the specified binary - ''' - def is_exe(fpath): - if sys.platform == 'win32': - fpath = fpath.replace('\\','/') - for exe in [fpath, fpath + '.exe']: - if all([os.path.isfile(exe), os.access(exe, os.X_OK)]): - return True - else: - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file - return None - - def call(self, cmd): - """Call specified command using subprocess library""" - bash_binary = self.which('bash') - # Execute the command, catching failures - if self.get_distribution() == 'Ubuntu': - bash_cmd = [bash_binary, '-l'] - else: - bash_cmd = [bash_binary] - try: - if sys.version_info[0] >= 3: - # Invoke process and poll for new output until finished - with Popen( [bash_binary, '-c', cmd], stdout=PIPE, stderr=STDOUT, bufsize=1, universal_newlines=True) as p: - for line in p.stdout: - sys.stdout.write(line) # process line here - if p.returncode != 0: - self.invocation.failed = True - self.invocation.returncode = p.returncode - self.invocation.stdout = 'Encountered error code {errcode} in the specified command {args}'.format(errcode=p.returncode, args=p.args) - return self.invocation - else: - # Invoke process - process = Popen( - [bash_binary, '-c', cmd], - stdout=PIPE, - stderr=STDOUT) - # Poll for new output until finished - while True: - nextline = process.stdout.readline() - if nextline == '' and process.poll() is not None: - break - sys.stdout.write(nextline) - sys.stdout.flush() - except Exception as e: - print('Encountered error in the specified command, error was {err}'.format(err=e)) diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..9b84b37 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,9 @@ +pip==18.1 +bumpversion==0.5.3 +wheel==0.32.1 +watchdog==0.9.0 +flake8==3.5.0 +tox==3.5.2 +coverage==4.5.1 +Sphinx==1.8.1 +twine==1.12.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0b60895 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,22 @@ +[bumpversion] +current_version = 0.0.13 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:ansible_taskrunner/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + +[bdist_wheel] +universal = 1 + +[flake8] +exclude = docs + +[aliases] +# Define setup.py command aliases here + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..756c2e4 --- /dev/null +++ b/setup.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The setup script.""" + +from setuptools import setup, find_packages +try: # for pip >= 10 + from pip._internal.req import parse_requirements +except ImportError: # for pip <= 9.0.3 + from pip.req import parse_requirements +import os +import re +import shutil +import sys + +with open("README.rst", "rb") as readme_file: + readme = readme_file.read().decode("utf-8") + +with open('HISTORY.rst') as history_file: + history = history_file.read() + +embedded_libs = [ +'ansible_taskrunner/lib/py2', +'ansible_taskrunner/lib/py3' +] + +for embedded_lib in embedded_libs: + if os.path.isdir(embedded_lib): + print('Removing embedded lib %s' % embedded_lib) + shutil.rmtree(embedded_lib) + +# parse_requirements() returns generator of pip.req.InstallRequirement objects +install_reqs = parse_requirements("requirements.txt", session=False) +# reqs is a list of requirement +# e.g. ['django==1.5.1', 'mezzanine==1.4.6'] +requirements = [str(ir.req) for ir in install_reqs] + +# Derive version info from main module +try: + # https://stackoverflow.com/questions/52007436/pypi-is-adding-dashes-to-the-beginning-and-end-of-version-name + version = re.search( + '^__version__[\s]+=[\s]+(.*).*', + open('ansible_taskrunner/__init__.py').read(), + re.M + ).group(1).strip('"').strip("'") +except AttributeError as e: + print(''' + I had trouble determining the verison information from your app. + Make sure the version string matches this format: + __version__ = '1.0' + ''') + +setup_requirements = [ ] + +test_requirements = [ ] + +setup( + author="Engelbert Tejeda", + author_email='berttejeda@gmail.com', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + "Programming Language :: Python :: 2", + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + description="ansible-playbook wrapper with YAML-abstracted python click cli options", + entry_points={ + 'console_scripts': [ + 'tasks=ansible_taskrunner.cli:entrypoint', + ], + }, + install_requires=requirements, + license="MIT license", + long_description=readme + '\n\n' + history, + include_package_data=True, + keywords='ansible playbook wrapper bash python click task-runner subprocess yaml cli options', + name='ansible_taskrunner', + packages=find_packages(exclude=['py2','py3']), + setup_requires=setup_requirements, + test_suite='tests', + tests_require=test_requirements, + url='https://github.com/berttejeda/ansible_taskrunner', + version=version, + zip_safe=False, +) diff --git a/setup.yaml b/setup.yaml new file mode 100644 index 0000000..a707599 --- /dev/null +++ b/setup.yaml @@ -0,0 +1,68 @@ +--- +- hosts: local + vars: + help: + readme: | + Task Runner for the Task Runner! + examples: + inventory: | + [local] + localhost + required_parameters: + optional_parameters: + -b: build + --s: setup_and_test + --u: uninstall + -bp|--build-and-push: deployment_host_and_path + functions: + setup_and_test: + shell: bash + source: |- + python setup.py -q install + tasks run --help + uninstall: + shell: bash + source: |- + pip uninstall ansible_taskrunner -y + build: + shell: bash + source: |- + set -o errexit + for pyver in py2 py3;do + if ! test -d lib/${pyver};then + mkdir lib/${pyver} + source activate $pyver + pip install -t lib/${pyver} -r requirments + fi + done + __version=$(egrep '.*__version__ =' tasks.py | cut -d\ -f3 | tr -d "'") + echo "Version is ${__version}" + __release_dir=release/${__version} + lint_result=$(./tasks.py --help) + echo "Initial lint OK, proceeding with build" + if [[ "$OSTYPE" =~ .*msys.* ]];then + echo "OSType is Windows, nesting libdir ..." + mkdir windows + cp -r lib plugins windows + echo "Creating zip-app" + make-zipapp -f tasks.py -X __pycache__ -x .pyc -d windows + if test -d windows;then rm -rf windows;fi + else + echo "OSType is most likely POSIX native" + echo "Creating zip-app" + ../make-zipapp/make-zipapp.py -f tasks.py -d lib + fi + lint_result=$(./tasks.py --help) + echo "Initial lint OK, proceeding with release" + if ! test -d ${__release_dir};then mkdir -p ${__release_dir};fi + mv -f tasks ${__release_dir} + echo "Replacing current executable: $(which tasks)" + yes | cp ${__release_dir}/tasks $(which tasks) + if [[ -n $deployment_host_and_path ]];then + echo "Pushing up" + scp_result=$(scp ${__release_dir}/tasks ${deployment_host_and_path}) + fi + tasks: + - debug: + msg: Done! +... \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..dc342c3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +"""Unit test package for ansible_taskrunner.""" diff --git a/tests/test_ansible_taskrunner.py b/tests/test_ansible_taskrunner.py new file mode 100644 index 0000000..05ab5ea --- /dev/null +++ b/tests/test_ansible_taskrunner.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Tests for `ansible_taskrunner` package.""" + + +import unittest +from click.testing import CliRunner + +from ansible_taskrunner import cli + + +class TestAnsible_taskrunner(unittest.TestCase): + """Tests for `ansible_taskrunner` package.""" + + def setUp(self): + """Set up test fixtures, if any.""" + + def tearDown(self): + """Tear down test fixtures, if any.""" + + def test_000_something(self): + """Test something.""" + + def test_command_line_interface(self): + """Test the CLI.""" + runner = CliRunner() + result = runner.invoke(cli.main) + assert result.exit_code == 0 + assert 'ansible_taskrunner.cli.main' in result.output + help_result = runner.invoke(cli.main, ['--help']) + assert help_result.exit_code == 0 + assert '--help Show this message and exit.' in help_result.output diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a23a0a7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,21 @@ +[tox] +envlist = py27, py34, py35, py36, flake8 + +[travis] +python = + 3.6: py36 + 3.5: py35 + 3.4: py34 + 2.7: py27 + +[testenv:flake8] +basepython = python +deps = flake8 +commands = flake8 ansible_taskrunner + +[testenv] +setenv = + PYTHONPATH = {toxinidir} + +commands = python setup.py test +