From e7e3435e1915575508d9cac3518c15278e83bb58 Mon Sep 17 00:00:00 2001 From: timeforplanb123 Date: Sat, 24 Apr 2021 22:56:27 +0300 Subject: [PATCH] Add Nornir plugins and update netmiko to 3.4.0: - add nornir-f5 and nornir-pyez Nornir plugins - update dependencies (netmiko to 3.4.0 and click to 7.1.2 from ^7.1.2) - fix default_value in click command options - fix regex in _doc_generator - fix the code responsible for collecting statistic - fix documentation --- README.md | 8 +-- docs/examples.md | 4 +- docs/index.md | 6 +- docs/useful.md | 2 + docs/workflow.md | 75 +++++++++++++++++++++++- nornir_cli/__init__.py | 2 +- nornir_cli/common_commands/common.py | 2 +- nornir_cli/nornir_cli.py | 57 +++++++++++++----- nornir_cli/plugin_commands/cmd_common.py | 31 ---------- poetry.lock | 56 +++++++++++++++++- pyproject.toml | 8 ++- tests/test_nornir_cli.py | 2 +- 12 files changed, 187 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index a74757f..e43b3bc 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ nornir_cli * **Manage your custom nornir runbooks** - * Create and manage your own runbooks collections - * Add your custom nornir runbooks to runbooks collections and run it for any hosts directly from the CLI + * Create and manage your own runbook collections + * Add your custom nornir runbooks to runbook collections and run it for any hosts directly from the CLI * Or use `nornir_cli` for inventory management only, and take the result in your nornir runbooks. By excluding getting and filtering the inventory in your runbooks, you will make them more versatile. * **Manage Inventory** @@ -39,7 +39,7 @@ nornir_cli * **Json input. Json output** - Json strings are everywhere! Ok, only in commands options + Json strings are everywhere! Ok, only in command options * **Custom Multi Commands with click** @@ -80,7 +80,7 @@ docker build -t timeforplanb123/nornir_cli . docker run --rm -it timeforplanb123/nornir_cli sh # nornir_cli --version -nornir_cli, version 0.2.0 +nornir_cli, version 0.3.0 ``` diff --git a/docs/examples.md b/docs/examples.md index 6f89f93..a4b2adf 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -199,7 +199,7 @@ And here is an example of this runbook: ``` === "jinja2 template:" ```jinja - # nornir_cli/custom_commands/templates/dhcp_snooping.j2 + # nornir_cli/custom_commands/dhcp/templates/dhcp_snooping.j2 dhcp enable dhcp snooping enable @@ -225,7 +225,7 @@ And here is an example of this runbook: ``` === "textfsm template:" ```text - # nornir_cli/custom_commands/templates/disp_int.template + # nornir_cli/custom_commands/dhcp/templates/disp_int.template Value NAME (\S+) Value DESCRIPTION (.*) diff --git a/docs/index.md b/docs/index.md index 8bf0380..569f2ce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,8 +4,8 @@ * **Manage your custom nornir runbooks** - * Create and manage your own runbooks collections - * Add your custom nornir runbooks to runbooks collections and run it for any hosts directly from the CLI + * Create and manage your own runbook collections + * Add your custom nornir runbooks to runbook collections and run it for any hosts directly from the CLI * Or use `nornir_cli` for inventory management only, and take the result in your nornir runbooks. By excluding getting and filtering the inventory in your runbooks, you will make them more versatile. * **Manage Inventory** @@ -65,7 +65,7 @@ docker build -t timeforplanb123/nornir_cli . docker run --rm -it timeforplanb123/nornir_cli sh # nornir_cli --version -nornir_cli, version 0.2.0 +nornir_cli, version 0.3.0 ``` #### Simple Example diff --git a/docs/useful.md b/docs/useful.md index 2590761..cae4cf0 100644 --- a/docs/useful.md +++ b/docs/useful.md @@ -93,9 +93,11 @@ All custom Nornir runbooks stored in `custom_commands` directory (see [Click Mul Commands: dhcp mpls + nornir-f5 nornir_f5 plugin nornir-jinja2 nornir_jinja2 plugin nornir-napalm nornir_napalm plugin nornir-netmiko nornir_netmiko plugin + nornir-pyez nornir_pyez plugin nornir-scrapli nornir_scrapli plugin ``` diff --git a/docs/workflow.md b/docs/workflow.md index 14de4c4..aaa8d89 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -24,7 +24,7 @@ $ nornir_cli nornir-netmiko init -c ~/config.yaml ``` Why is `nornir-netmiko` here? `nornir_cli` runs Tasks based on Nornir plugins or your custom Nornir runbooks, so the first step is to select an available plugin or custom. -For version `0.2.0`, only Connection plugins and `nornir_jinja2` are available: +For version `0.3.0`, the following Nornir plugins are available: ```text $ nornir_cli --help Usage: nornir_cli [OPTIONS] COMMAND [ARGS]... @@ -38,9 +38,11 @@ Options: --help Show this message and exit. Commands: - nornir_jinja2 nornir_jinja2 plugin + nornir-f5 nornir_f5 plugin + nornir-jinja2 nornir_jinja2 plugin nornir-napalm nornir_napalm plugin nornir-netmiko nornir_netmiko plugin + nornir-pyez nornir_pyez plugin nornir-scrapli nornir_scrapli plugin ``` #### Without a configuration file @@ -332,6 +334,75 @@ At first, let's check all available Tasks/commands for current list of nornir pl template_string Renders a string with jinja2. All the host data is available in the template ``` +=== "nornir-pyez:" + ```text + $ nornir_cli nornir-pyez + Usage: nornir_cli nornir-pyez [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 + [ARGS]...]... + + nornir_pyez plugin + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + pyez_facts + pyez_config + pyez_get_config + pyez_diff + pyez_commit + pyez_int_terse + pyez_route_info + pyez_rpc + pyez_sec_ike + pyez_sec_ipsec + pyez_sec_nat_dest + pyez_sec_nat_src + pyez_sec_policy + pyez_sec_zones + ``` +=== "nornir-f5:" + ```text + $ nornir_cli nornir-f5 + Usage: nornir_cli nornir-f5 [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 + [ARGS]...]... + + nornir_f5 plugin + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + atc Task to deploy declaratives on F5 devices + atc_info Task to verify if ATC service is available + and collect service info + + bigip_cm_config_sync Task to synchronize the configuration + between devices + + bigip_cm_failover_status Task to get the failover status of the + device + + bigip_cm_sync_status Task to get the synchronization status of + the device + + bigip_shared_file_transfer_uploads + Upload a file to a BIG-IP system using the + iControl REST API + + bigip_shared_iapp_lx_package Task to manage Javascript LX packages on a + BIG-IP + + bigip_sys_version Gets the system version of the BIG-IP + bigip_util_unix_ls Task to list information about the FILEs + bigip_util_unix_rm Task to delete a file from a BIG-IP system + ``` And start `netmiko_send_command`, for example: diff --git a/nornir_cli/__init__.py b/nornir_cli/__init__.py index d3ec452..493f741 100644 --- a/nornir_cli/__init__.py +++ b/nornir_cli/__init__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.3.0" diff --git a/nornir_cli/common_commands/common.py b/nornir_cli/common_commands/common.py index 9d81a51..39271da 100644 --- a/nornir_cli/common_commands/common.py +++ b/nornir_cli/common_commands/common.py @@ -104,7 +104,7 @@ def _get_lists(s): def _doc_generator(s): regex = ( r".*kwargs: (?P.*)" - r"|.*task: (?P.*)" + r"|.*task(?P.*)" r"|(?P.*Returns.*)" r"|(?P^\S+:.*)" r"|(?P.* – .*)" diff --git a/nornir_cli/nornir_cli.py b/nornir_cli/nornir_cli.py index 80c2090..37d901f 100644 --- a/nornir_cli/nornir_cli.py +++ b/nornir_cli/nornir_cli.py @@ -6,7 +6,6 @@ from nornir_cli import __version__ -# CMD_FOLDERS = ["common_commands", "custom_commands"] CMD_FOLDERS = ["common_commands"] PACKAGE_NAME = "nornir_cli" @@ -109,7 +108,6 @@ def get_command(self, ctx, cmd_name): return def get_custom_command(self, ctx, cmd_name): - # CMD_FOLDERS = CMD_FOLDERS + cmd_path try: for abs_path, rel_path in zip( _get_cmd_folder(CMD_FOLDERS + cmd_path), @@ -190,32 +188,40 @@ def finder(): else: init_nornir_cli.group(cls=scls, chain=True)(f) - grp_exceptions = os.environ.get("NORNIR_CLI_GRP_EXCEPTIONS") - custom_exceptions = ["__pycache__", "templates"] + + grp_exceptions = os.environ.get("NORNIR_CLI_GRP_EXCEPTIONS") if grp_exceptions: custom_exceptions += grp_exceptions.split(",") + scls = class_factory("LazyClass", param, ["custom_commands"]) + return wrapper -# +# command decorator def decorator(plugin, ctx): def wrapper(f): # methods with a large and complex __doc__ :( method_exceptions = ("send_interactive",) + if obj_or.__doc__: - short_help = obj_or.__doc__.split("\n")[1].strip(", ., :") + doc = [title for title in obj_or.__doc__.split("\n")[:2] if title] - f.__doc__ = "\n".join(list(_doc_generator(obj_or.__doc__))) + short_help = doc[0].strip(", ., :") - if obj_or.__name__ in method_exceptions: - f.__doc__ = f"{short_help}\n" + "\n".join( - list( - _doc_generator(obj_or.__doc__[obj_or.__doc__.find(" Args:") : :]) - ) - ) + f.__doc__ = "\n".join(list(_doc_generator(obj_or.__doc__))) + if obj_or.__name__ in method_exceptions: + f.__doc__ = f"{short_help}\n" + "\n".join( + list( + _doc_generator( + obj_or.__doc__[obj_or.__doc__.find(" Args:") : :] + ) + ) + ) + else: + short_help = "" cmd = click.command(name=obj_or.__name__, short_help=short_help)(f) click.option( @@ -242,6 +248,7 @@ def wrapper(f): for key, value in p.items() if key not in ["self", "task", "args", "kwargs"] } + # dynamically generate options for k, v in all_dict.items(): default_value = str(v.default) if not isinstance(v.default, type) else None @@ -249,17 +256,21 @@ def wrapper(f): "--" + k, default=default_value, show_default=True, - required=False if default_value else True, + required=False if default_value or default_value == "" else True, type=PARAMETER_TYPES.setdefault(type(v.default), click.STRING), )(cmd) + # last original functions with arguments ctx.obj["queue_parameters"][obj_or].update({k: v.default}) + # list of dictionaries with original function (key) and set of arguments (value) ctx.obj["queue_functions"].append(ctx.obj["queue_parameters"]) + # ctx.obj["queue_functions"] in the form of a generator expression ctx.obj["queue_functions_generator"] = ( func_param for func_param in ctx.obj["queue_functions"] ) + return cmd # get original function from Nornir plugin @@ -321,8 +332,22 @@ def nornir_jinja2(): pass -@dec() -def custom(): +@dec("nornir_pyez.plugins") +def nornir_pyez(): + """ + nornir_pyez plugin """ + pass + + +@dec("nornir_f5.plugins") +def nornir_f5(): """ + nornir_f5 plugin + """ + pass + + +@dec() +def custom(): pass diff --git a/nornir_cli/plugin_commands/cmd_common.py b/nornir_cli/plugin_commands/cmd_common.py index 723ba94..dec90d4 100644 --- a/nornir_cli/plugin_commands/cmd_common.py +++ b/nornir_cli/plugin_commands/cmd_common.py @@ -6,16 +6,6 @@ from tqdm import tqdm -# def _get_color(f, ch): -# if f: -# color = "red" -# elif ch: -# color = "yellow" -# else: -# color = "green" -# return color - - def multiple_progress_bar(task, method, pg_bar, **kwargs): task.run(task=method, **kwargs) if pg_bar: @@ -75,24 +65,3 @@ def cli(ctx, pg_bar, show_result, *args, **kwargs): # show statistic _info(nr, task) - # ch_sum = 0 - # for host in nr.inventory.hosts: - # f, ch = (task[host].failed, task[host].changed) - # ch_sum += int(ch) - # click.secho( - # f"{host:<50}: ok={not f:<15} changed={ch:<15} failed={f:<15}", - # fg=_get_color(f, ch), - # bold=True, - # ) - # print() - # f_sum = len(nr.data.failed_hosts) - # ok_sum = len(nr.inventory.hosts) - f_sum - # for state, summary, color in zip( - # ("OK", "CHANGED", "FAILED"), (ok_sum, ch_sum, f_sum), ("green", "yellow", "red") - # ): - # click.secho( - # f"{state:<8}: {summary}", - # fg=color, - # bold=True, - # ) - # print() diff --git a/poetry.lock b/poetry.lock index f0df819..d62924a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -469,6 +469,21 @@ typing_extensions = ">=3.7,<4.0" [package.extras] docs = ["jupyter (>=1,<2)", "nbsphinx (>=0.5,<0.6)", "pygments (>=2,<3)", "sphinx (>=1,<2)", "sphinx-issues (>=1.2,<2.0)", "sphinx_rtd_theme (>=0.4,<0.5)", "sphinxcontrib-napoleon (>=0.7,<0.8)"] +[[package]] +name = "nornir-f5" +version = "0.5.0" +description = "F5 plugins for Nornir" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +nornir = ">=3.0.0,<4.0.0" +packaging = ">=20.9,<21.0" +requests = ">=2.25.1,<3.0.0" +requests-toolbelt = ">=0.9.1,<0.10.0" +urllib3 = ">=1.26.3,<2.0.0" + [[package]] name = "nornir-jinja2" version = "0.1.2" @@ -516,6 +531,20 @@ python-versions = ">=3.6,<4.0" [package.dependencies] netmiko = ">=3.1.0,<4.0.0" +[[package]] +name = "nornir-pyez" +version = "0.0.10" +description = "PyEZs library and plugins for Nornir" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +junos-eznc = ">=2.5.4" +nornir = ">=3.0.0,<4.0.0" +nornir-utils = ">=0.1.0" +xmltodict = ">=0.12.0" + [[package]] name = "nornir-scrapli" version = "2021.1.30" @@ -566,7 +595,7 @@ dev = ["pytest", "pyyaml", "black", "yamllint", "ruamel.yaml"] name = "packaging" version = "20.9" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -809,6 +838,17 @@ urllib3 = ">=1.21.1,<1.27" security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +[[package]] +name = "requests-toolbelt" +version = "0.9.1" +description = "A utility belt for advanced users of python-requests" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "ruamel.yaml" version = "0.16.13" @@ -1060,7 +1100,7 @@ pyyaml = "*" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "34dd68affcc65a33658358681dabc848e581fad06530a85ed178637a29d08be3" +content-hash = "78fcaa63cd9f98f19cd4577a871e3262e969adc4af2debfea2aaa0f1046fd4aa" [metadata.files] appdirs = [ @@ -1342,6 +1382,10 @@ nornir = [ {file = "nornir-3.1.0-py3-none-any.whl", hash = "sha256:0eed0ca73122ce8f4b90a560ada79d81174845a8a5320972292417eb19efa587"}, {file = "nornir-3.1.0.tar.gz", hash = "sha256:f96b0e2b8983045eef5345de5a6e61198ed2658a0e42049e4bd9a127b2bae034"}, ] +nornir-f5 = [ + {file = "nornir_f5-0.5.0-py3-none-any.whl", hash = "sha256:dcb1db192801d0f7df0e7cfffb573942686eaf5e698fc8e18e7c4ef64d639c07"}, + {file = "nornir_f5-0.5.0.tar.gz", hash = "sha256:0bbae2fd6877c571e0eddea0f8d1165dcf41e93b9792bc7f9739dde4105c636f"}, +] nornir-jinja2 = [ {file = "nornir_jinja2-0.1.2-py3-none-any.whl", hash = "sha256:9a1c9e8ef2b3d72966f68e415a77c10b0ef5f4d7dbc730acd0bc78d57d4ba716"}, {file = "nornir_jinja2-0.1.2.tar.gz", hash = "sha256:83520fa59076bfebe17cbacf54de6040eb7daefe564d2dd7563c13785140fa93"}, @@ -1358,6 +1402,10 @@ nornir-netmiko = [ {file = "nornir_netmiko-0.1.1-py3-none-any.whl", hash = "sha256:c6eadb81f6f3b2f0c27bae151cc62673303f9d085ec3c773ecdc98f20ef30f91"}, {file = "nornir_netmiko-0.1.1.tar.gz", hash = "sha256:fc41ded40923d23c6155b92c6749170629b4cc2649c340d24bff9f49315836c6"}, ] +nornir-pyez = [ + {file = "nornir_pyez-0.0.10-py3-none-any.whl", hash = "sha256:5b1b93aea1982f266e4a0d832a29d6c4fa769efcfa98d97434f73a7d895088ca"}, + {file = "nornir_pyez-0.0.10.tar.gz", hash = "sha256:7fd7e7a4210d5d9a9b1922b53b60e0f589561669aec946c89b623da2e0aa18f4"}, +] nornir-scrapli = [ {file = "nornir_scrapli-2021.1.30-py3-none-any.whl", hash = "sha256:2a9079781ec6e000f62a0ddae1432b35d76da583c543158e8057be75b8ed6e45"}, {file = "nornir_scrapli-2021.1.30.tar.gz", hash = "sha256:8130e153c8e51ff6bafa50ac8d9f2fce232bf281448ea99f669bdd0b3f13b6ec"}, @@ -1588,6 +1636,10 @@ requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] "ruamel.yaml" = [ {file = "ruamel.yaml-0.16.13-py2.py3-none-any.whl", hash = "sha256:64b06e7873eb8e1125525ecef7345447d786368cadca92a7cd9b59eae62e95a3"}, {file = "ruamel.yaml-0.16.13.tar.gz", hash = "sha256:bb48c514222702878759a05af96f4b7ecdba9b33cd4efcf25c86b882cef3a942"}, diff --git a/pyproject.toml b/pyproject.toml index ea8cd7a..2649172 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nornir_cli" -version = "0.2.0" +version = "0.3.0" description = "Nornir CLI" license = "MIT" readme = "README.md" @@ -14,12 +14,12 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.8" -click = "^7.1.2" +click = "7.1.2" nornir = "3.1.0" nornir-utils = "0.1.2" nornir-jinja2 = "0.1.2" nornir-netmiko = "0.1.1" -netmiko = "3.3.3" +netmiko = "3.4.0" nornir-scrapli = "2021.01.30" scrapli = "2021.01.30" scrapli-netconf = "2021.01.30" @@ -29,6 +29,8 @@ ttp = "^0.6.0" # initially, nornir_cli was created to work with NetBox inventory ^_- nornir-netbox = "^0.2.0" genie = "20.9" +nornir-pyez = "0.0.10" +nornir-f5 = "0.5.0" [tool.poetry.dev-dependencies] pytest = "^5.2" diff --git a/tests/test_nornir_cli.py b/tests/test_nornir_cli.py index 44e9e29..1542a26 100644 --- a/tests/test_nornir_cli.py +++ b/tests/test_nornir_cli.py @@ -2,4 +2,4 @@ def test_version(): - assert __version__ == "0.1.0" + assert __version__ == "0.3.0"