From cc8f20b58122f99261dd9b48831171c84ff16e04 Mon Sep 17 00:00:00 2001 From: carl Date: Mon, 16 Sep 2019 20:45:40 -0700 Subject: [PATCH] initial commit! --- .github/workflows/commit.yaml | 24 + .github/workflows/publish.yaml | 47 ++ .gitignore | 75 +++ .pylintrc | 416 ++++++++++++++++ CODE_OF_CONDUCT.md | 3 + LICENSE | 21 + Makefile | 14 + README.md | 10 + _config.yml | 1 + docs/nornsible/index.html | 387 ++++++++++++++ docs/nornsible/nornsible.html | 610 +++++++++++++++++++++++ nornir.log | 198 ++++++++ nornsible/__init__.py | 20 + nornsible/nornsible.py | 206 ++++++++ pyproject.toml | 16 + requirements-dev.txt | 12 + requirements.txt | 1 + setup.cfg | 24 + setup.py | 31 ++ tests/_test_nornir_inventory/groups.yaml | 4 + tests/_test_nornir_inventory/hosts.yaml | 15 + tests/test_nornsible.py | 314 ++++++++++++ tox.ini | 25 + 23 files changed, 2474 insertions(+) create mode 100644 .github/workflows/commit.yaml create mode 100644 .github/workflows/publish.yaml create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 _config.yml create mode 100644 docs/nornsible/index.html create mode 100644 docs/nornsible/nornsible.html create mode 100644 nornir.log create mode 100644 nornsible/__init__.py create mode 100644 nornsible/nornsible.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/_test_nornir_inventory/groups.yaml create mode 100644 tests/_test_nornir_inventory/hosts.yaml create mode 100644 tests/test_nornsible.py create mode 100644 tox.ini diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml new file mode 100644 index 0000000..96997ea --- /dev/null +++ b/.github/workflows/commit.yaml @@ -0,0 +1,24 @@ +name: build + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 2 + matrix: + python-version: [3.6, 3.7] + steps: + - uses: actions/checkout@v1 + - name: set up python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: setup test env + run: | + python -m pip install --upgrade pip + python -m pip install setuptools + python -m pip install tox + - name: run tox + run: python -m tox --skip-missing-interpreters=true diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..96e3eb9 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,47 @@ +name: re-test and publish to pypi + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 2 + matrix: + python-version: [3.6, 3.7] + steps: + - uses: actions/checkout@v1 + - name: set up python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: setup test env + run: | + python -m pip install --upgrade pip + python -m pip install setuptools + python -m pip install tox + - name: run tox + run: python -m tox --skip-missing-interpreters=true + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: set up python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: setup publish env + run: | + python -m pip install --upgrade pip + python -m pip install setuptools + python -m pip install wheel + python -m pip install twine + - name: build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USER }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASS }} + run: | + python setup.py sdist bdist_wheel + python -m twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5259f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# pycharm +.idea + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# swap files +*.swp + +# macos stuff +.DS_Store +*/.DS_Store diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..35d6115 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,416 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=yes + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist=numpy + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=C0103,C0111,C0330,E1101,W0104,W0105,W1203,W0201,R0902,R0913 +# C0103 = constant-name +# C0111 = missing-docstring (class docstring) +# C0330 = bad-continuation (hanging indent that black doesnt like) +# E1101 = no-member (issue w/ multiple inheritance and where does an attribtue come from) +# W0104 = pointless-statement +# W0105 = pointless-string-statement +# W1203 = logging-fstring-interpolation (py3.6, using f-strings so dont care) +# W0201 = attribute defined outside init -- removing until i figure out how i want to deal w/ inheritance vs construction +# R0902 = too-many-instance-attributes +# R0913 = too-many-arguments + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^test_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=y + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=10 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,matplotlib.cm,tensorflow.python,tensorflow,tensorflow.train.Example,RunOptions + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members=set_shape,np.float32 + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=yes + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=(_+[a-zA-Z0-9_]*?$)|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=10 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=30 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=100 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=10 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e90fb30 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +Be excellent to each other! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..91f9667 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Carl Montanari + +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/Makefile b/Makefile new file mode 100644 index 0000000..6391d9c --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +test_unit: + python -m pytest \ + --cov=nornsible \ + --cov-report html \ + --cov-report term \ + tests/. + +.PHONY: docs +docs: + python -m pdoc \ + --html \ + --output-dir docs \ + nornsible \ + --force diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfe18d3 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +![](https://github.com/carlmontanari/nornsible/workflows/build/badge.svg) +[![PyPI version](https://badge.fury.io/py/nornsible.svg)](https://badge.fury.io/py/nornsible) +[![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/) +[![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/) +[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) + +nornsible +======= + +Wrap Nornir with Ansible-like host/group limits and tagging! diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..bad8266 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-midnight diff --git a/docs/nornsible/index.html b/docs/nornsible/index.html new file mode 100644 index 0000000..06121e3 --- /dev/null +++ b/docs/nornsible/index.html @@ -0,0 +1,387 @@ + + + + + + +nornsible API documentation + + + + + + + + + +
+
+
+

Module nornsible

+
+
+

nornir tag and host/group limit wrapper

+
+Source code +
"""nornir tag and host/group limit wrapper"""
+import logging
+from logging import NullHandler
+
+from nornsible.nornsible import (
+    Init_Nornsible,
+    parse_cli_args,
+    process_tags,
+    patch_config,
+    patch_inventory,
+)
+
+
+__version__ = "2019.09.16"
+__all__ = ("Init_Nornsible", "parse_cli_args", "process_tags", "patch_config", "patch_inventory")
+
+
+# Setup logger
+session_log = logging.getLogger(__name__)
+logging.getLogger(__name__).addHandler(NullHandler())
+
+
+
+

Sub-modules

+
+
nornsible.nornsible
+
+
+
+
+
+
+
+
+

Functions

+
+
+def Init_Nornsible(nr) +
+
+

Patch nornir object based on cli arguments

+

Arguments

+
+
nr
+
Nornir object
+
+

Returns

+
+
nr
+
Nornir object; modified if cli args dictate the need to do so; otherwise passed as is
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def Init_Nornsible(nr):
+    """
+    Patch nornir object based on cli arguments
+
+    Arguments:
+        nr: Nornir object
+
+    Returns:
+        nr: Nornir object; modified if cli args dictate the need to do so; otherwise passed as is
+
+    Raises:
+        N/A  # noqa
+
+    """
+    cli_args = parse_cli_args(sys.argv[1:])
+
+    nr.run_tags = cli_args.pop("run_tags")
+    nr.skip_tags = cli_args.pop("skip_tags")
+
+    if any(a for a in cli_args.values()):
+        nr.config = patch_config(cli_args, nr.config)
+        nr.inventory = patch_inventory(cli_args, nr.inventory)
+        return nr
+
+    return nr
+
+
+
+def parse_cli_args(args) +
+
+

Parse CLI provided arguments; ignore unrecognized.

+

Arguments

+
+
args
+
List of CLI provided arguments
+
+

Returns

+
+
cli_args
+
Processed CLI arguments
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def parse_cli_args(args: list) -> dict:
+    """
+    Parse CLI provided arguments; ignore unrecognized.
+
+    Arguments:
+        args: List of CLI provided arguments
+
+    Returns:
+        cli_args: Processed CLI arguments
+
+    Raises:
+        N/A  # noqa
+
+    """
+    parser = argparse.ArgumentParser(description="Nornir Script Wrapper")
+    parser.add_argument(
+        "-w",
+        "--workers",
+        help="number of workers to set for global configuration",
+        type=int,
+        default=0,
+    )
+    parser.add_argument(
+        "-l",
+        "--limit",
+        help="limit to host or comma separated list of hosts",
+        type=str.lower,
+        default="",
+    )
+    parser.add_argument(
+        "-g",
+        "--groups",
+        help="limit to group or comma separated list of groups",
+        type=str.lower,
+        default="",
+    )
+    parser.add_argument(
+        "-t", "--tags", help="names of tasks to explicitly run", type=str.lower, default=""
+    )
+    parser.add_argument("-s", "--skip", help="names of tasks to skip", type=str.lower, default="")
+    args, _ = parser.parse_known_args(args)
+    cli_args = {}
+    cli_args["workers"] = args.workers if args.workers else False
+    cli_args["limit"] = set(args.limit.split(",")) if args.limit else False
+    cli_args["groups"] = set(args.groups.split(",")) if args.groups else False
+    cli_args["run_tags"] = set(args.tags.split(",")) if args.tags else []
+    cli_args["skip_tags"] = set(args.skip.split(",")) if args.skip else []
+    return cli_args
+
+
+
+def patch_config(cli_args, conf) +
+
+

Patch nornir core configurations per cli arguments.

+

Arguments

+
+
cli_args
+
Updates from CLI to update in Nornir objects
+
conf
+
nornir.core.configuration.Config object; Initialized Nornir Config object
+
+

Returns

+
+
conf
+
nornir.core.configuration.Config object; Updated Nornir Config object
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def patch_config(cli_args: dict, conf):
+    """
+    Patch nornir core configurations per cli arguments.
+
+    Arguments:
+        cli_args: Updates from CLI to update in Nornir objects
+        conf: nornir.core.configuration.Config object; Initialized Nornir Config object
+
+    Returns:
+        conf: nornir.core.configuration.Config object; Updated Nornir Config object
+
+    Raises:
+        N/A  # noqa
+
+    """
+    if cli_args["workers"]:
+        conf.core.num_workers = cli_args["workers"]
+
+    return conf
+
+
+
+def patch_inventory(cli_args, inv) +
+
+

Patch nornir inventory configurations per cli arguments.

+

Arguments

+
+
cli_args
+
Updates from CLI to update in Nornir objects
+
inv
+
nornir.core.inventory.Inventory object; Initialized Nornir Inventory object
+
+

Returns

+
+
inv
+
nornir.core.inventory.Inventory object; Updated Nornir Inventory object
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def patch_inventory(cli_args: dict, inv):
+    """
+    Patch nornir inventory configurations per cli arguments.
+
+    Arguments:
+        cli_args: Updates from CLI to update in Nornir objects
+        inv: nornir.core.inventory.Inventory object; Initialized Nornir Inventory object
+
+    Returns:
+        inv: nornir.core.inventory.Inventory object; Updated Nornir Inventory object
+
+    Raises:
+        N/A  # noqa
+
+    """
+    if cli_args["limit"]:
+        valid_hosts = []
+        invalid_hosts = []
+        for host in cli_args["limit"]:
+            if host in inv.hosts.keys():
+                valid_hosts.append(host)
+            else:
+                invalid_hosts.append(host)
+        if invalid_hosts:
+            print(
+                "Host limit contained invalid host(s), ignoring: "
+                f"{[host for host in invalid_hosts]}"
+            )
+        inv = inv.filter(filter_func=lambda h: h.name in valid_hosts)
+
+    elif cli_args["groups"]:
+        valid_groups = [g for g in cli_args["groups"] if g in inv.groups.keys()]
+        invalid_groups = [g for g in cli_args["groups"] if g not in inv.groups.keys()]
+        if invalid_groups:
+            print(
+                "Group limit contained invalid group(s), ignoring: "
+                f"{[host for host in invalid_groups]}"
+            )
+        inv = inv.filter(filter_func=lambda h: any(True for g in valid_groups if g in h.groups))
+    return inv
+
+
+
+def process_tags(wrapped_func) +
+
+

Decorate an "operation" – execute or skip the operation based on tags

+

Args

+
+
wrapped_func
+
function to wrap in tag processor
+
+

Returns

+
+
tag_wrapper
+
wrapped function
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def process_tags(wrapped_func):
+    """
+    Decorate an "operation" -- execute or skip the operation based on tags
+
+    Args:
+        wrapped_func: function to wrap in tag processor
+
+    Returns:
+        tag_wrapper: wrapped function
+
+    Raises:
+        N/A  # noqa
+
+    """
+
+    def tag_wrapper(task, *args, **kwargs):
+        if set([wrapped_func.__name__]).intersection(task.nornir.skip_tags):
+            msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
+            process_tags_messages(msg)
+            return Result(host=task.host, result="Task skipped!", failed=False, changed=False)
+        if not task.nornir.run_tags:
+            return wrapped_func(task, *args, **kwargs)
+        if set([wrapped_func.__name__]).intersection(task.nornir.run_tags):
+            return wrapped_func(task, *args, **kwargs)
+        msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
+        process_tags_messages(msg)
+        return Result(host=task.host, result="Task skipped!", failed=False, changed=False)
+
+    tag_wrapper.__name__ = wrapped_func.__name__
+    return tag_wrapper
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/docs/nornsible/nornsible.html b/docs/nornsible/nornsible.html new file mode 100644 index 0000000..63ea105 --- /dev/null +++ b/docs/nornsible/nornsible.html @@ -0,0 +1,610 @@ + + + + + + +nornsible.nornsible API documentation + + + + + + + + + +
+
+
+

Module nornsible.nornsible

+
+
+
+Source code +
import argparse
+import logging
+import sys
+import threading
+
+from colorama import Fore, Style, init
+from nornir.core.task import Result
+
+
+session_log = logging.getLogger(__name__)
+
+init(autoreset=True, strip=False)
+LOCK = threading.Lock()
+
+
+def parse_cli_args(args: list) -> dict:
+    """
+    Parse CLI provided arguments; ignore unrecognized.
+
+    Arguments:
+        args: List of CLI provided arguments
+
+    Returns:
+        cli_args: Processed CLI arguments
+
+    Raises:
+        N/A  # noqa
+
+    """
+    parser = argparse.ArgumentParser(description="Nornir Script Wrapper")
+    parser.add_argument(
+        "-w",
+        "--workers",
+        help="number of workers to set for global configuration",
+        type=int,
+        default=0,
+    )
+    parser.add_argument(
+        "-l",
+        "--limit",
+        help="limit to host or comma separated list of hosts",
+        type=str.lower,
+        default="",
+    )
+    parser.add_argument(
+        "-g",
+        "--groups",
+        help="limit to group or comma separated list of groups",
+        type=str.lower,
+        default="",
+    )
+    parser.add_argument(
+        "-t", "--tags", help="names of tasks to explicitly run", type=str.lower, default=""
+    )
+    parser.add_argument("-s", "--skip", help="names of tasks to skip", type=str.lower, default="")
+    args, _ = parser.parse_known_args(args)
+    cli_args = {}
+    cli_args["workers"] = args.workers if args.workers else False
+    cli_args["limit"] = set(args.limit.split(",")) if args.limit else False
+    cli_args["groups"] = set(args.groups.split(",")) if args.groups else False
+    cli_args["run_tags"] = set(args.tags.split(",")) if args.tags else []
+    cli_args["skip_tags"] = set(args.skip.split(",")) if args.skip else []
+    return cli_args
+
+
+def patch_inventory(cli_args: dict, inv):
+    """
+    Patch nornir inventory configurations per cli arguments.
+
+    Arguments:
+        cli_args: Updates from CLI to update in Nornir objects
+        inv: nornir.core.inventory.Inventory object; Initialized Nornir Inventory object
+
+    Returns:
+        inv: nornir.core.inventory.Inventory object; Updated Nornir Inventory object
+
+    Raises:
+        N/A  # noqa
+
+    """
+    if cli_args["limit"]:
+        valid_hosts = []
+        invalid_hosts = []
+        for host in cli_args["limit"]:
+            if host in inv.hosts.keys():
+                valid_hosts.append(host)
+            else:
+                invalid_hosts.append(host)
+        if invalid_hosts:
+            print(
+                "Host limit contained invalid host(s), ignoring: "
+                f"{[host for host in invalid_hosts]}"
+            )
+        inv = inv.filter(filter_func=lambda h: h.name in valid_hosts)
+
+    elif cli_args["groups"]:
+        valid_groups = [g for g in cli_args["groups"] if g in inv.groups.keys()]
+        invalid_groups = [g for g in cli_args["groups"] if g not in inv.groups.keys()]
+        if invalid_groups:
+            print(
+                "Group limit contained invalid group(s), ignoring: "
+                f"{[host for host in invalid_groups]}"
+            )
+        inv = inv.filter(filter_func=lambda h: any(True for g in valid_groups if g in h.groups))
+    return inv
+
+
+def patch_config(cli_args: dict, conf):
+    """
+    Patch nornir core configurations per cli arguments.
+
+    Arguments:
+        cli_args: Updates from CLI to update in Nornir objects
+        conf: nornir.core.configuration.Config object; Initialized Nornir Config object
+
+    Returns:
+        conf: nornir.core.configuration.Config object; Updated Nornir Config object
+
+    Raises:
+        N/A  # noqa
+
+    """
+    if cli_args["workers"]:
+        conf.core.num_workers = cli_args["workers"]
+
+    return conf
+
+
+def process_tags_messages(msg):
+    """
+    Handle printing pretty messages for process_tags decorator
+
+    Args:
+        msg: message to beautifully print to stdout
+
+    Returns:
+         N/A
+
+    Raises:
+        N/A  # noqa
+
+    """
+    LOCK.acquire()
+    try:
+        print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "-" * (80 - len(msg))))
+    finally:
+        LOCK.release()
+
+
+def process_tags(wrapped_func):
+    """
+    Decorate an "operation" -- execute or skip the operation based on tags
+
+    Args:
+        wrapped_func: function to wrap in tag processor
+
+    Returns:
+        tag_wrapper: wrapped function
+
+    Raises:
+        N/A  # noqa
+
+    """
+
+    def tag_wrapper(task, *args, **kwargs):
+        if set([wrapped_func.__name__]).intersection(task.nornir.skip_tags):
+            msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
+            process_tags_messages(msg)
+            return Result(host=task.host, result="Task skipped!", failed=False, changed=False)
+        if not task.nornir.run_tags:
+            return wrapped_func(task, *args, **kwargs)
+        if set([wrapped_func.__name__]).intersection(task.nornir.run_tags):
+            return wrapped_func(task, *args, **kwargs)
+        msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
+        process_tags_messages(msg)
+        return Result(host=task.host, result="Task skipped!", failed=False, changed=False)
+
+    tag_wrapper.__name__ = wrapped_func.__name__
+    return tag_wrapper
+
+
+def Init_Nornsible(nr):
+    """
+    Patch nornir object based on cli arguments
+
+    Arguments:
+        nr: Nornir object
+
+    Returns:
+        nr: Nornir object; modified if cli args dictate the need to do so; otherwise passed as is
+
+    Raises:
+        N/A  # noqa
+
+    """
+    cli_args = parse_cli_args(sys.argv[1:])
+
+    nr.run_tags = cli_args.pop("run_tags")
+    nr.skip_tags = cli_args.pop("skip_tags")
+
+    if any(a for a in cli_args.values()):
+        nr.config = patch_config(cli_args, nr.config)
+        nr.inventory = patch_inventory(cli_args, nr.inventory)
+        return nr
+
+    return nr
+
+
+
+
+
+
+
+

Functions

+
+
+def Init_Nornsible(nr) +
+
+

Patch nornir object based on cli arguments

+

Arguments

+
+
nr
+
Nornir object
+
+

Returns

+
+
nr
+
Nornir object; modified if cli args dictate the need to do so; otherwise passed as is
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def Init_Nornsible(nr):
+    """
+    Patch nornir object based on cli arguments
+
+    Arguments:
+        nr: Nornir object
+
+    Returns:
+        nr: Nornir object; modified if cli args dictate the need to do so; otherwise passed as is
+
+    Raises:
+        N/A  # noqa
+
+    """
+    cli_args = parse_cli_args(sys.argv[1:])
+
+    nr.run_tags = cli_args.pop("run_tags")
+    nr.skip_tags = cli_args.pop("skip_tags")
+
+    if any(a for a in cli_args.values()):
+        nr.config = patch_config(cli_args, nr.config)
+        nr.inventory = patch_inventory(cli_args, nr.inventory)
+        return nr
+
+    return nr
+
+
+
+def parse_cli_args(args) +
+
+

Parse CLI provided arguments; ignore unrecognized.

+

Arguments

+
+
args
+
List of CLI provided arguments
+
+

Returns

+
+
cli_args
+
Processed CLI arguments
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def parse_cli_args(args: list) -> dict:
+    """
+    Parse CLI provided arguments; ignore unrecognized.
+
+    Arguments:
+        args: List of CLI provided arguments
+
+    Returns:
+        cli_args: Processed CLI arguments
+
+    Raises:
+        N/A  # noqa
+
+    """
+    parser = argparse.ArgumentParser(description="Nornir Script Wrapper")
+    parser.add_argument(
+        "-w",
+        "--workers",
+        help="number of workers to set for global configuration",
+        type=int,
+        default=0,
+    )
+    parser.add_argument(
+        "-l",
+        "--limit",
+        help="limit to host or comma separated list of hosts",
+        type=str.lower,
+        default="",
+    )
+    parser.add_argument(
+        "-g",
+        "--groups",
+        help="limit to group or comma separated list of groups",
+        type=str.lower,
+        default="",
+    )
+    parser.add_argument(
+        "-t", "--tags", help="names of tasks to explicitly run", type=str.lower, default=""
+    )
+    parser.add_argument("-s", "--skip", help="names of tasks to skip", type=str.lower, default="")
+    args, _ = parser.parse_known_args(args)
+    cli_args = {}
+    cli_args["workers"] = args.workers if args.workers else False
+    cli_args["limit"] = set(args.limit.split(",")) if args.limit else False
+    cli_args["groups"] = set(args.groups.split(",")) if args.groups else False
+    cli_args["run_tags"] = set(args.tags.split(",")) if args.tags else []
+    cli_args["skip_tags"] = set(args.skip.split(",")) if args.skip else []
+    return cli_args
+
+
+
+def patch_config(cli_args, conf) +
+
+

Patch nornir core configurations per cli arguments.

+

Arguments

+
+
cli_args
+
Updates from CLI to update in Nornir objects
+
conf
+
nornir.core.configuration.Config object; Initialized Nornir Config object
+
+

Returns

+
+
conf
+
nornir.core.configuration.Config object; Updated Nornir Config object
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def patch_config(cli_args: dict, conf):
+    """
+    Patch nornir core configurations per cli arguments.
+
+    Arguments:
+        cli_args: Updates from CLI to update in Nornir objects
+        conf: nornir.core.configuration.Config object; Initialized Nornir Config object
+
+    Returns:
+        conf: nornir.core.configuration.Config object; Updated Nornir Config object
+
+    Raises:
+        N/A  # noqa
+
+    """
+    if cli_args["workers"]:
+        conf.core.num_workers = cli_args["workers"]
+
+    return conf
+
+
+
+def patch_inventory(cli_args, inv) +
+
+

Patch nornir inventory configurations per cli arguments.

+

Arguments

+
+
cli_args
+
Updates from CLI to update in Nornir objects
+
inv
+
nornir.core.inventory.Inventory object; Initialized Nornir Inventory object
+
+

Returns

+
+
inv
+
nornir.core.inventory.Inventory object; Updated Nornir Inventory object
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def patch_inventory(cli_args: dict, inv):
+    """
+    Patch nornir inventory configurations per cli arguments.
+
+    Arguments:
+        cli_args: Updates from CLI to update in Nornir objects
+        inv: nornir.core.inventory.Inventory object; Initialized Nornir Inventory object
+
+    Returns:
+        inv: nornir.core.inventory.Inventory object; Updated Nornir Inventory object
+
+    Raises:
+        N/A  # noqa
+
+    """
+    if cli_args["limit"]:
+        valid_hosts = []
+        invalid_hosts = []
+        for host in cli_args["limit"]:
+            if host in inv.hosts.keys():
+                valid_hosts.append(host)
+            else:
+                invalid_hosts.append(host)
+        if invalid_hosts:
+            print(
+                "Host limit contained invalid host(s), ignoring: "
+                f"{[host for host in invalid_hosts]}"
+            )
+        inv = inv.filter(filter_func=lambda h: h.name in valid_hosts)
+
+    elif cli_args["groups"]:
+        valid_groups = [g for g in cli_args["groups"] if g in inv.groups.keys()]
+        invalid_groups = [g for g in cli_args["groups"] if g not in inv.groups.keys()]
+        if invalid_groups:
+            print(
+                "Group limit contained invalid group(s), ignoring: "
+                f"{[host for host in invalid_groups]}"
+            )
+        inv = inv.filter(filter_func=lambda h: any(True for g in valid_groups if g in h.groups))
+    return inv
+
+
+
+def process_tags(wrapped_func) +
+
+

Decorate an "operation" – execute or skip the operation based on tags

+

Args

+
+
wrapped_func
+
function to wrap in tag processor
+
+

Returns

+
+
tag_wrapper
+
wrapped function
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def process_tags(wrapped_func):
+    """
+    Decorate an "operation" -- execute or skip the operation based on tags
+
+    Args:
+        wrapped_func: function to wrap in tag processor
+
+    Returns:
+        tag_wrapper: wrapped function
+
+    Raises:
+        N/A  # noqa
+
+    """
+
+    def tag_wrapper(task, *args, **kwargs):
+        if set([wrapped_func.__name__]).intersection(task.nornir.skip_tags):
+            msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
+            process_tags_messages(msg)
+            return Result(host=task.host, result="Task skipped!", failed=False, changed=False)
+        if not task.nornir.run_tags:
+            return wrapped_func(task, *args, **kwargs)
+        if set([wrapped_func.__name__]).intersection(task.nornir.run_tags):
+            return wrapped_func(task, *args, **kwargs)
+        msg = f"---- {task.host} skipping task {wrapped_func.__name__} "
+        process_tags_messages(msg)
+        return Result(host=task.host, result="Task skipped!", failed=False, changed=False)
+
+    tag_wrapper.__name__ = wrapped_func.__name__
+    return tag_wrapper
+
+
+
+def process_tags_messages(msg) +
+
+

Handle printing pretty messages for process_tags decorator

+

Args

+
+
msg
+
message to beautifully print to stdout
+
+

Returns

+
+
N/A
+
 
+
+

Raises

+
+
N/A +# noqa
+
 
+
+
+Source code +
def process_tags_messages(msg):
+    """
+    Handle printing pretty messages for process_tags decorator
+
+    Args:
+        msg: message to beautifully print to stdout
+
+    Returns:
+         N/A
+
+    Raises:
+        N/A  # noqa
+
+    """
+    LOCK.acquire()
+    try:
+        print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "-" * (80 - len(msg))))
+    finally:
+        LOCK.release()
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/nornir.log b/nornir.log new file mode 100644 index 0000000..ed37275 --- /dev/null +++ b/nornir.log @@ -0,0 +1,198 @@ +2019-09-16 19:37:02,198 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:37:08,378 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:37:14,004 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:37:58,361 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:39:03,808 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 3 hosts +2019-09-16 19:39:08,578 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 3 hosts +2019-09-16 19:40:13,277 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:40:41,964 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:40:47,755 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:41:10,006 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:41:26,100 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:41:44,780 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:42:04,016 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:43:02,044 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:43:05,076 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:43:45,462 - nornir.core - INFO - run() - Running task 'test_task' with args {'command': "echo 'Hello World!'", 'name': 'test_task'} on 1 hosts +2019-09-16 19:43:58,826 - nornir.core - INFO - run() - Running task 'test_task' with args {'command': "echo 'Hello World!'", 'name': 'test_task'} on 1 hosts +2019-09-16 19:44:01,400 - nornir.core - INFO - run() - Running task 'test_task' with args {'command': "echo 'Hello World!'", 'name': 'test_task'} on 1 hosts +2019-09-16 19:44:29,006 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:45:01,330 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:45:22,916 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:45:46,696 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:46:10,498 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:46:26,283 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:46:43,474 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:47:10,997 - nornir.core - INFO - run() - Running task 'command' with args {'command': "echo 'Hello World!'"} on 1 hosts +2019-09-16 19:47:22,262 - nornir.core - INFO - run() - Running task 'something' with args {'command': "echo 'Hello World!'", 'name': 'something'} on 1 hosts +2019-09-16 19:47:30,688 - nornir.core - INFO - run() - Running task 'test_task' with args {'command': "echo 'Hello World!'", 'name': 'test_task'} on 1 hosts +2019-09-16 19:47:40,781 - nornir.core - INFO - run() - Running task 'test_task' with args {'command': "echo 'Hello World!'", 'name': 'test_task'} on 1 hosts +2019-09-16 19:47:53,470 - nornir.core - INFO - run() - Running task 'test_task' with args {'command': "echo 'Hello World!'", 'name': 'test_task'} on 1 hosts +2019-09-16 19:51:00,087 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:51:25,424 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:51:47,053 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:53:20,744 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:53:20,854 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 19:53:37,383 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:53:37,491 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 19:54:34,558 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:54:34,668 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 19:58:04,375 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:58:04,480 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 19:58:27,559 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 19:58:27,665 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 19:59:48,096 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 3 hosts +2019-09-16 19:59:48,200 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 3 hosts +2019-09-16 20:00:30,289 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:00:30,398 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:00:48,542 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:00:48,646 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:01:23,371 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:01:23,475 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:02:16,208 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:02:28,262 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:03:49,555 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:03:49,664 - nornir.core - INFO - run() - Running task 'another_custom_task_example' with args {} on 1 hosts +2019-09-16 20:09:01,861 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:09:01,967 - nornir.core - INFO - run() - Running task 'another_custom_task_example' with args {} on 1 hosts +2019-09-16 20:09:17,552 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:09:17,790 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:09:17,896 - nornir.core - INFO - run() - Running task 'another_custom_task_example' with args {} on 1 hosts +2019-09-16 20:10:10,489 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:10:10,733 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:10:10,840 - nornir.core - INFO - run() - Running task 'another_custom_task_example' with args {} on 1 hosts +2019-09-16 20:13:15,262 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:13:54,479 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:14:26,714 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:15:22,175 - nornir.core.task - ERROR - start() - Host 'localhost': task 'custom_task_example' failed with traceback: +Traceback (most recent call last): + File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/nornir/core/task.py", line 67, in start + r = self.task(self, **self.params) + File "/Users/carl/Dev/GitHub/nornsible/nornsible/nornsible.py", line 149, in tag_wrapper + if set([wrapped_func.__name__]).intersection(task.nornir.skip_tags): + File "/Users/carl/Dev/GitHub/nornsible/nornsible/nornsible.py", line 149, in tag_wrapper + if set([wrapped_func.__name__]).intersection(task.nornir.skip_tags): + File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/bdb.py", line 88, in trace_dispatch + return self.dispatch_line(frame) + File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/bdb.py", line 113, in dispatch_line + if self.quitting: raise BdbQuit +bdb.BdbQuit + +2019-09-16 20:15:32,325 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:15:39,661 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:15:39,774 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:15:39,882 - nornir.core - INFO - run() - Running task 'another_custom_task_example' with args {} on 1 hosts +2019-09-16 20:16:33,257 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:16:41,494 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:16:41,605 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:16:41,713 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:17:17,249 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:17:17,360 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:17:17,468 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:17:59,969 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:18:00,082 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:18:00,191 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:18:20,879 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:18:20,984 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:18:54,682 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:18:54,788 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:19:18,047 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:19:18,154 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:19:53,243 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:19:53,348 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:20:15,934 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:20:16,041 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:20:24,729 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:20:24,843 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:20:24,947 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:20:41,693 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:20:41,805 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:20:41,914 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:20:44,591 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:20:44,707 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:20:44,812 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:22:23,439 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:23,552 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:23,660 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:22:23,775 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:23,878 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:22:28,717 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:28,829 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:28,937 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:22:29,051 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:29,156 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:22:31,168 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:31,280 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:31,390 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:22:31,504 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:22:31,608 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:23:31,157 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:23:31,275 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:23:31,386 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:23:31,499 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:23:31,605 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:23:33,578 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:23:33,698 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:23:33,804 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:23:33,923 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:23:34,028 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:24:43,184 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:24:43,299 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:24:43,409 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:24:43,526 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:24:43,631 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:24:45,649 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:24:45,763 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:24:45,869 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:24:45,980 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:24:46,086 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:28:28,234 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:28:28,347 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:28:28,455 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:28:28,568 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:28:28,675 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:28:30,698 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:28:30,812 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:28:30,918 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:28:31,036 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:28:31,143 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:37:06,064 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:37:06,176 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:37:06,286 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:37:06,402 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:37:06,508 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:37:08,614 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:37:08,730 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:37:08,835 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:37:08,953 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:37:09,063 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:08,719 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:08,833 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:08,940 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:09,053 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:09,158 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:11,247 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:11,364 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:11,474 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:11,586 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:11,696 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:42,694 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:42,812 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:42,922 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:43,040 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:43,150 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:45,435 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:45,547 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:45,651 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:45,767 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:45,877 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:56,192 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:56,311 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:56,418 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:56,534 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:56,640 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:58,677 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:58,796 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:58,905 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts +2019-09-16 20:38:59,018 - nornir.core - INFO - run() - Running task 'custom_task_example' with args {} on 1 hosts +2019-09-16 20:38:59,127 - nornir.core - INFO - run() - Running task 'custom_task_example_2' with args {} on 1 hosts diff --git a/nornsible/__init__.py b/nornsible/__init__.py new file mode 100644 index 0000000..c71e905 --- /dev/null +++ b/nornsible/__init__.py @@ -0,0 +1,20 @@ +"""nornir tag and host/group limit wrapper""" +import logging +from logging import NullHandler + +from nornsible.nornsible import ( + Init_Nornsible, + parse_cli_args, + process_tags, + patch_config, + patch_inventory, +) + + +__version__ = "2019.09.16" +__all__ = ("Init_Nornsible", "parse_cli_args", "process_tags", "patch_config", "patch_inventory") + + +# Setup logger +session_log = logging.getLogger(__name__) +logging.getLogger(__name__).addHandler(NullHandler()) diff --git a/nornsible/nornsible.py b/nornsible/nornsible.py new file mode 100644 index 0000000..ab5c2cf --- /dev/null +++ b/nornsible/nornsible.py @@ -0,0 +1,206 @@ +import argparse +import logging +import sys +import threading + +from colorama import Fore, Style, init +from nornir.core.task import Result + + +session_log = logging.getLogger(__name__) + +init(autoreset=True, strip=False) +LOCK = threading.Lock() + + +def parse_cli_args(args: list) -> dict: + """ + Parse CLI provided arguments; ignore unrecognized. + + Arguments: + args: List of CLI provided arguments + + Returns: + cli_args: Processed CLI arguments + + Raises: + N/A # noqa + + """ + parser = argparse.ArgumentParser(description="Nornir Script Wrapper") + parser.add_argument( + "-w", + "--workers", + help="number of workers to set for global configuration", + type=int, + default=0, + ) + parser.add_argument( + "-l", + "--limit", + help="limit to host or comma separated list of hosts", + type=str.lower, + default="", + ) + parser.add_argument( + "-g", + "--groups", + help="limit to group or comma separated list of groups", + type=str.lower, + default="", + ) + parser.add_argument( + "-t", "--tags", help="names of tasks to explicitly run", type=str.lower, default="" + ) + parser.add_argument("-s", "--skip", help="names of tasks to skip", type=str.lower, default="") + args, _ = parser.parse_known_args(args) + cli_args = {} + cli_args["workers"] = args.workers if args.workers else False + cli_args["limit"] = set(args.limit.split(",")) if args.limit else False + cli_args["groups"] = set(args.groups.split(",")) if args.groups else False + cli_args["run_tags"] = set(args.tags.split(",")) if args.tags else [] + cli_args["skip_tags"] = set(args.skip.split(",")) if args.skip else [] + return cli_args + + +def patch_inventory(cli_args: dict, inv): + """ + Patch nornir inventory configurations per cli arguments. + + Arguments: + cli_args: Updates from CLI to update in Nornir objects + inv: nornir.core.inventory.Inventory object; Initialized Nornir Inventory object + + Returns: + inv: nornir.core.inventory.Inventory object; Updated Nornir Inventory object + + Raises: + N/A # noqa + + """ + if cli_args["limit"]: + valid_hosts = [] + invalid_hosts = [] + for host in cli_args["limit"]: + if host in inv.hosts.keys(): + valid_hosts.append(host) + else: + invalid_hosts.append(host) + if invalid_hosts: + print( + "Host limit contained invalid host(s), ignoring: " + f"{[host for host in invalid_hosts]}" + ) + inv = inv.filter(filter_func=lambda h: h.name in valid_hosts) + + elif cli_args["groups"]: + valid_groups = [g for g in cli_args["groups"] if g in inv.groups.keys()] + invalid_groups = [g for g in cli_args["groups"] if g not in inv.groups.keys()] + if invalid_groups: + print( + "Group limit contained invalid group(s), ignoring: " + f"{[host for host in invalid_groups]}" + ) + inv = inv.filter(filter_func=lambda h: any(True for g in valid_groups if g in h.groups)) + return inv + + +def patch_config(cli_args: dict, conf): + """ + Patch nornir core configurations per cli arguments. + + Arguments: + cli_args: Updates from CLI to update in Nornir objects + conf: nornir.core.configuration.Config object; Initialized Nornir Config object + + Returns: + conf: nornir.core.configuration.Config object; Updated Nornir Config object + + Raises: + N/A # noqa + + """ + if cli_args["workers"]: + conf.core.num_workers = cli_args["workers"] + + return conf + + +def process_tags_messages(msg): + """ + Handle printing pretty messages for process_tags decorator + + Args: + msg: message to beautifully print to stdout + + Returns: + N/A + + Raises: + N/A # noqa + + """ + LOCK.acquire() + try: + print("{}{}{}{}".format(Style.BRIGHT, Fore.CYAN, msg, "-" * (80 - len(msg)))) + finally: + LOCK.release() + + +def process_tags(wrapped_func): + """ + Decorate an "operation" -- execute or skip the operation based on tags + + Args: + wrapped_func: function to wrap in tag processor + + Returns: + tag_wrapper: wrapped function + + Raises: + N/A # noqa + + """ + + def tag_wrapper(task, *args, **kwargs): + if set([wrapped_func.__name__]).intersection(task.nornir.skip_tags): + msg = f"---- {task.host} skipping task {wrapped_func.__name__} " + process_tags_messages(msg) + return Result(host=task.host, result="Task skipped!", failed=False, changed=False) + if not task.nornir.run_tags: + return wrapped_func(task, *args, **kwargs) + if set([wrapped_func.__name__]).intersection(task.nornir.run_tags): + return wrapped_func(task, *args, **kwargs) + msg = f"---- {task.host} skipping task {wrapped_func.__name__} " + process_tags_messages(msg) + return Result(host=task.host, result="Task skipped!", failed=False, changed=False) + + tag_wrapper.__name__ = wrapped_func.__name__ + return tag_wrapper + + +def Init_Nornsible(nr): + """ + Patch nornir object based on cli arguments + + Arguments: + nr: Nornir object + + Returns: + nr: Nornir object; modified if cli args dictate the need to do so; otherwise passed as is + + Raises: + N/A # noqa + + """ + cli_args = parse_cli_args(sys.argv[1:]) + + nr.run_tags = cli_args.pop("run_tags") + nr.skip_tags = cli_args.pop("skip_tags") + + if any(a for a in cli_args.values()): + nr.config = patch_config(cli_args, nr.config) + nr.inventory = patch_inventory(cli_args, nr.inventory) + return nr + + return nr diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..40a8fc5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.black] +line-length = 100 +target-version = ['py37'] +include = '\.pyi?$' +exclude = ''' +( + /( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | venv + )/ +) +''' diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..4e6b1a1 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,12 @@ +tox>=3.14.0 +black>=19.3b0 +pytest>=5.0.1 +pytest-cov>=2.7.1 +pylama>=7.6.6 +pycodestyle>=2.5.0 +pydocstyle>=4.0.1 +pylint>=2.3.1 +darglint>=0.6.1 +pdoc3>=0.6.2 +nornir>=2.2.0 +-r requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e24644a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +colorama>=0.3.9 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..dd0da5c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,24 @@ +[pylama] +linters = mccabe,pycodestyle,pylint +skip = tests/*,.tox/*,venv/*,build/*,private/*,comparison_tests/* + +[pylama:pycodestyle] +max_line_length = 100 + +[pylama:pylint] +rcfile = .pylintrc + +[pydocstyle] +ignore = D101,D202,D212,D400,D406,D407,D408,D409,D415 +match-dir = ^ssh2net/* + +[darglint] +docstring_style = google + +[mypy] +python_version = 3.7 +strict_optional = False +disallow_any_generics = False +ignore_missing_imports = True +warn_redundant_casts = True +warn_unused_configs = True diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fb50d69 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +"""nornir tag and host/group limit wrapper""" +import setuptools + + +__author__ = "Carl Montanari" + +with open("README.md", "r") as f: + README = f.read() + +setuptools.setup( + name="nornsible", + version="2019.09.16", + author=__author__, + author_email="carl.r.montanari@gmail.com", + description="Wrapper for tags and host/group limiting for nornir scripts.", + long_description=README, + long_description_content_type="text/markdown", + packages=setuptools.find_packages(), + install_requires=[], + classifiers=[ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + ], + python_requires=">=3.6", +) diff --git a/tests/_test_nornir_inventory/groups.yaml b/tests/_test_nornir_inventory/groups.yaml new file mode 100644 index 0000000..7783210 --- /dev/null +++ b/tests/_test_nornir_inventory/groups.yaml @@ -0,0 +1,4 @@ +--- +sea: {} +nxos: {} +eos: {} diff --git a/tests/_test_nornir_inventory/hosts.yaml b/tests/_test_nornir_inventory/hosts.yaml new file mode 100644 index 0000000..33c8788 --- /dev/null +++ b/tests/_test_nornir_inventory/hosts.yaml @@ -0,0 +1,15 @@ +--- +sea-eos-1: + hostname: 1.2.3.4 + groups: + - sea + - eos + +sea-nxos-1: + hostname: 4.3.2.1 + groups: + - sea + - nxos + +localhost: + hostname: 127.0.0.1 diff --git a/tests/test_nornsible.py b/tests/test_nornsible.py new file mode 100644 index 0000000..ce51227 --- /dev/null +++ b/tests/test_nornsible.py @@ -0,0 +1,314 @@ +from pathlib import Path +import sys +from unittest.mock import patch + +from nornir import InitNornir + +import nornsible +from nornsible import Init_Nornsible, parse_cli_args, process_tags, patch_config, patch_inventory + + +NORNSIBLE_DIR = nornsible.__file__ +TEST_DIR = f"{Path(NORNSIBLE_DIR).parents[1]}/tests/" + + +@process_tags +def custom_task_example(task): + return "Hello, world!" + + +@process_tags +def custom_task_example_2(task): + return "Hello, world!" + + +def test_parse_cli_args_basic_short(): + args = [ + "-l", + "sea-eos-1", + "-g", + "sea", + "-s", + "deploy_configs", + "-t", + "render_configs", + "-w", + "10", + ] + args = parse_cli_args(args) + assert args["limit"] == {"sea-eos-1"} + assert args["groups"] == {"sea"} + assert args["skip_tags"] == {"deploy_configs"} + assert args["run_tags"] == {"render_configs"} + assert args["workers"] == 10 + + +def test_parse_cli_args_basic_long(): + args = [ + "--limit", + "sea-eos-1", + "--groups", + "sea", + "--skip", + "deploy_configs", + "--tags", + "render_configs", + "--workers", + "10", + ] + args = parse_cli_args(args) + assert args["limit"] == {"sea-eos-1"} + assert args["groups"] == {"sea"} + assert args["skip_tags"] == {"deploy_configs"} + assert args["run_tags"] == {"render_configs"} + assert args["workers"] == 10 + + +def test_parse_cli_args_basic_none(): + args = ["somethingelse", "notrelevant"] + args = parse_cli_args(args) + assert args["limit"] is False + assert args["groups"] is False + assert args["skip_tags"] == [] + assert args["run_tags"] == [] + assert args["workers"] is False + + +def test_patch_inventory_basic_limit_host(): + args = ["-l", "sea-eos-1"] + args = parse_cli_args(args) + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr.inventory = patch_inventory(args, nr.inventory) + assert set(nr.inventory.hosts.keys()) == {"sea-eos-1"} + + +def test_patch_inventory_basic_limit_group(): + args = ["-g", "eos"] + args = parse_cli_args(args) + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr.inventory = patch_inventory(args, nr.inventory) + assert set(nr.inventory.hosts.keys()) == {"sea-eos-1"} + + +def test_patch_config_basic_limit_workers(): + args = ["-w", "10"] + args = parse_cli_args(args) + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr.config = patch_config(args, nr.config) + assert nr.config.core.num_workers == 10 + + +def test_patch_inventory_basic_limit_host_invalid(): + args = ["-l", "sea-1234"] + args = parse_cli_args(args) + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr.inventory = patch_inventory(args, nr.inventory) + assert set(nr.inventory.hosts.keys()) == set() + + +def test_patch_inventory_basic_limit_group_invalid(): + args = ["-g", "eos1234"] + args = parse_cli_args(args) + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr.inventory = patch_inventory(args, nr.inventory) + assert set(nr.inventory.hosts.keys()) == set() + + +def test_set_nornsible_limit_host(): + testargs = ["somescript", "-l", "sea-eos-1"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + assert set(nr.inventory.hosts.keys()) == {"sea-eos-1"} + + +def test_set_nornsible_limit_group(): + testargs = ["somescript", "-g", "eos"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + assert set(nr.inventory.hosts.keys()) == {"sea-eos-1"} + + +def test_set_nornsible_workers(): + testargs = ["somescript", "-w", "10"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + assert nr.config.core.num_workers == 10 + + +def test_set_nornsible_limithost_invalid(): + testargs = ["somescript", "-l", "sea-eos-1234"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + assert set(nr.inventory.hosts.keys()) == set() + + +def test_set_nornsible_limit_group_invalid(): + testargs = ["somescript", "-g", "eos1234"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + assert set(nr.inventory.hosts.keys()) == set() + + +def test_set_nornsible_do_nothing(): + testargs = ["somescript"] + with patch.object(sys, "argv", testargs): + initial_nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nornsible_nr = Init_Nornsible(initial_nr) + assert nornsible_nr == initial_nr + + +def test_tags_wrapper_skip_task(): + testargs = ["somescript", "-l", "localhost", "-s", "custom_task_example"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + task_result = nr.run(task=custom_task_example) + assert set(task_result.keys()) == {"localhost"} + assert task_result["localhost"].result == "Task skipped!" + + +def test_tags_wrapper_explicit_task(): + testargs = ["somescript", "-l", "localhost", "-t", "custom_task_example_2"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + print(nr.inventory.hosts) + tasks = [custom_task_example, custom_task_example_2] + task_results = [] + for task in tasks: + task_results.append(nr.run(task=task)) + + assert task_results[0]["localhost"].result == "Task skipped!" + assert task_results[1]["localhost"].result == "Hello, world!" + + +def test_tags_wrapper_no_tags(): + testargs = ["somescript", "-l", "localhost"] + with patch.object(sys, "argv", testargs): + nr = InitNornir( + inventory={ + "plugin": "nornir.plugins.inventory.simple.SimpleInventory", + "options": { + "host_file": f"{TEST_DIR}_test_nornir_inventory/hosts.yaml", + "group_file": f"{TEST_DIR}_test_nornir_inventory/groups.yaml", + }, + } + ) + nr = Init_Nornsible(nr) + print(nr.inventory.hosts) + tasks = [custom_task_example, custom_task_example_2] + task_results = [] + for task in tasks: + task_results.append(nr.run(task=task)) + + assert task_results[0]["localhost"].result == "Hello, world!" + assert task_results[1]["localhost"].result == "Hello, world!" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8cf94ca --- /dev/null +++ b/tox.ini @@ -0,0 +1,25 @@ +[tox] +envlist = + py36,py37 + + +[testenv] +deps = + -rrequirements-dev.txt +commands = + python -m pytest tests/. + + +[testenv:py37] +deps = + -rrequirements-dev.txt +commands = + python -m pytest \ + --cov=nornsible \ + --cov-report html \ + --cov-report term \ + tests/. + python -m black . + python -m pylama . + python -m pydocstyle . + darglint nornsible/.