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
+
+
+
+
+
+
+
+
+
+
+
+
+
+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())
+
+
+
+
+
+
+
+
+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
+
+
+
+
+
+
+
+
+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/.