From 3c1a31ebc177edd3e4c2d1d98a05695c60d7805b Mon Sep 17 00:00:00 2001 From: timeforplanb123 Date: Sun, 18 Apr 2021 23:01:31 +0300 Subject: [PATCH] v0.2.0 - Runbook collections and nornir_jinja2 plugin - Add Runbook collections functionality. About - https://timeforplanb123.github.io/workflow/#runbook-collections - Add statistic function (see _info) - Add nornir_jinja2 plugin - Change the documentation and fix errors --- .github/workflows/release.yml | 16 ++ README.md | 17 +- docs/examples.md | 36 +--- docs/index.md | 14 +- docs/useful.md | 177 +++++++++++++++++- docs/workflow.md | 93 +++++++-- examples/custom_runbooks/README.md | 6 +- .../dhcp_snooping/cmd_dhcp_snooping.py | 27 +-- mkdocs.yml | 30 ++- nornir_cli/__init__.py | 2 +- nornir_cli/common_commands/__init__.py | 2 + nornir_cli/common_commands/common.py | 36 ++++ nornir_cli/nornir_cli.py | 97 ++++++++-- nornir_cli/plugin_commands/cmd_common.py | 61 +++--- nornir_cli/transform/function.py | 4 +- poetry.lock | 18 +- pyproject.toml | 3 +- tests/test_nornir_cli.py | 2 +- 18 files changed, 496 insertions(+), 145 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..02ed119 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,16 @@ +name: Release +on: + release: + types: [published] +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: '3.8' + architecture: x64 + - run: pip install poetry==1.1.4 + - run: poetry build + - run: poetry publish --username=__token__ --password=${{ secrets.PYPI_TOKEN }} diff --git a/README.md b/README.md index 6569bde..a74757f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) +[![PyPI](https://img.shields.io/pypi/v/nornir-cli.svg)](https://pypi.org/project/nornir-cli) +[![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://opensource.org/licenses/MIT) +[![Docs](https://img.shields.io/badge/docs-passing-green.svg)](https://timeforplanb123.github.io/nornir_cli/) nornir_cli ========== @@ -17,7 +20,9 @@ nornir_cli * **Manage your custom nornir runbooks** - Add custom nornir runbook to the `nornir_cli` `custom` group and run it for any hosts directly from the CLI + * 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 + * 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** @@ -32,10 +37,18 @@ nornir_cli Initialize Nornir, filter Inventory and start Task/Tasks chains or runbook/runbooks chains in one command +* **Json input. Json output** + + Json strings are everywhere! Ok, only in commands options + * **Custom Multi Commands with click** `nornir_cli` based on click Custom Multi Commands, so you can easily add your custom command by following some principles +* **Simple CLI network orchestrator** + + `nornir_cli` is a simple CLI orchestrator that you can use to interact with the SoT and manage your network + ## Quick Start #### Install @@ -67,7 +80,7 @@ docker build -t timeforplanb123/nornir_cli . docker run --rm -it timeforplanb123/nornir_cli sh # nornir_cli --version -nornir_cli, version 0.1.0 +nornir_cli, version 0.2.0 ``` diff --git a/docs/examples.md b/docs/examples.md index 6de6a91..6f89f93 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -130,27 +130,27 @@ FAILED : 0 #### Custom Nornir runbooks -[How to add a previously written Nornir runbook in `nornir_cli`](http://timeforplanb123.github.io/nornir_cli/useful/#how-to-add-custom-runbook) +[How to add a previously written Nornir runbook in `nornir_cli`](http://timeforplanb123.github.io/nornir_cli/useful/#how-to-add-custom-nornir-runbook) -[How to run custom runbook](https://timeforplanb123.github.io/nornir_cli/workflow/#runbooks) +[How to run custom runbook](https://timeforplanb123.github.io/nornir_cli/workflow/#custom-runbooks) And here is an example of this runbook: === "Nornir runbook example:" ```python - # nornir_cli/custom_commands/cmd_dhcp_snooping.py + # nornir_cli/custom_commands/dhcp/md_dhcp_snooping.py import os - import click from nornir_netmiko import netmiko_send_command, netmiko_send_config from nornir.core.plugins.connections import ConnectionPluginRegister from nornir_jinja2.plugins.tasks import template_file - from nornir_cli.common_commands import custom - from nornir_cli.plugin_commands.cmd_common import _get_color + from nornir_cli.common_commands import custom, _info - @click.command("dhcp_snooping") @custom def cli(ctx): + """ + Configure dhcp snooping + """ def _get_trusted_untrusted(task): ConnectionPluginRegister.auto_register() # Get parameters in format: @@ -195,27 +195,7 @@ And here is an example of this runbook: task = ctx.run(task=_get_trusted_untrusted, on_failed=True) # Show statistic - ch_sum = 0 - for host in ctx.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(ctx.data.failed_hosts) - ok_sum = len(ctx.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() + _info(ctx, task) ``` === "jinja2 template:" ```jinja diff --git a/docs/index.md b/docs/index.md index 65cffcf..8bf0380 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,9 @@ * **Manage your custom nornir runbooks** - Add custom nornir runbook to the `nornir_cli` `custom` group and run it for any hosts directly from the CLI + * 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 + * 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** @@ -20,10 +22,18 @@ Initialize Nornir, filter Inventory and start Task/Tasks chains or runbook/runbooks chains in one command +* **Json input. Json output** + + Json strings are everywhere! Ok, only in command options + * **Custom Multi Commands with click** `nornir_cli` based on click Custom Multi Commands, so you can easily add your custom command by following some principles +* **Simple CLI network orchestrator** + + `nornir_cli` is a simple CLI orchestrator that you can use to interact with the SoT and manage your network + ## Quick Start #### Install @@ -55,7 +65,7 @@ docker build -t timeforplanb123/nornir_cli . docker run --rm -it timeforplanb123/nornir_cli sh # nornir_cli --version -nornir_cli, version 0.1.0 +nornir_cli, version 0.2.0 ``` #### Simple Example diff --git a/docs/useful.md b/docs/useful.md index fe377f3..2590761 100644 --- a/docs/useful.md +++ b/docs/useful.md @@ -31,29 +31,125 @@ common_commands custom_commands __init__.py nornir_cli.py plugin_commands _ ## Custom runbooks -#### How to add custom runbook +#### How to add custom nornir runbook You can add a collection of your custom Nornir runbooks in `nornir_cli` and run them for any Hosts, managing the Inventory using `nornir_cli`, directly from the CLI. -All custom Nornir runbooks stored in `custom_commands` directory (see [Click Multi Commands feature](https://timeforplanb123.github.io/nornir_cli/useful/#click-multi-commands-feature)). To add your Nornir runbook to `custom` group you need to: +All custom Nornir runbooks stored in `custom_commands` directory (see [Click Multi Commands feature](https://timeforplanb123.github.io/nornir_cli/useful/#click-multi-commands-feature)). To create custom groups and add your Nornir runbook to these groups you need to: -* wrap your runbook in a wrapper and run it inside that wrapper +* take and wrap your runbook in a wrapper and run it inside that wrapper ```python - import click from nornir_cli.common_commands import custom - @click.command("your_command_name") @custom def cli(ctx): + """ + runbook description + """ def nornir_runbook(task): - ... + # code + # ... task = ctx.run(task=nornir_runbook) ``` -* name a file `cmd_something.py` (replace `something` on your own) and put it to `custom_commands` directory + `ctx` after decorating with `@custom` is an current `nornir.core.Nornir` object and `cli` is a new `click.command` -See [example](https://timeforplanb123.github.io/nornir_cli/examples/#custom-nornir-runbooks) and [Examples](https://timeforplanb123/nornir_cli/examples/). +* name a file with your runbook as `cmd_something.py` (replace `something` on your own). `something` between `cmd_` and `.py` will be command name + +* create directory tree in `custom_commands` direcotry and put your nornir runbooks there. Here the directories are new `nornir_cli` groups, and nornir runbooks are new commands. Easy-peasy. + + For example, i created two directories, `dhcp` and `mpls`, and put my runbooks there. Let's check `nornir_cli`: + + === "directory tree:" + ```text + $ tree ~/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/ + /home/user/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/ + ├── dhcp + │   ├── cmd_dhcp_snooping.py + │   └── templates + │   ├── dhcp_snooping.j2 + │   └── disp_int.template + ├── __init__.py + └── mpls + └── cmd_ldp_config.py + + 3 directories, 5 files + ``` + + === "nornir_cli:" + ```text + $ nornir_cli + Usage: nornir_cli [OPTIONS] COMMAND [ARGS]... + + Nornir CLI + + Orchestrate your Inventory and start Tasks and Runbooks + + Options: + --version Show the version and exit. + --help Show this message and exit. + + Commands: + dhcp + mpls + nornir-jinja2 nornir_jinja2 plugin + nornir-napalm nornir_napalm plugin + nornir-netmiko nornir_netmiko plugin + nornir-scrapli nornir_scrapli plugin + ``` + + === "nornir_cli dhcp:" + ```text + $ nornir_cli dhcp + Usage: nornir_cli dhcp [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... + + + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + dhcp_snooping Configure dhcp snooping + ``` + + === "nornir_cli mpls:" + ```text + $ nornir_cli mpls + Usage: nornir_cli mpls [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... + + + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + ldp_config Configure ldp + ``` + +**Runbook collections features:** + +* empty directories are not displayed as `nornir_cli` groups +* the commands are displayed only in the latest directories in the directory tree. This is based on the `command chains` ability of `nornir_cli` and on the fact that it's impossible to build `command chains` in parent and child groups. That is a fair constraint related with Click Multi Commands and Multi Commands Chaining +* you may have noticed, that in the example above, there is a `templates` directory in the `dhcp` directory and it was not displayed. `templates` and `__pycache__` are included in the `custom_exceptions`list in `nornir_cli.py`. But you can use these names for the parent directories. + + If you want to add your exceptions without fixing `custom_exceptions` list, use the `NORNIR_CLI_GRP_EXCEPTIONS` environment variable: + + ```text + # as instance, to add temp and tmp groups to custom_exceptions list + $ export NORNIR_CLI_GRP_EXCEPTIONS=temp,tmp + ``` + +* if to add runbooks to `custom_commands` without `runbook collection`, they will be in `custom` group +* all python modules, used in your runbook, must be installed in the virtual environment, otherwise the runbook will not be displayed as command in `nornir_cli` + +See [example](https://timeforplanb123.github.io/nornir_cli/examples/#custom-nornir-runbooks) and [Examples](https://timeforplanb123.github.io/nornir_cli/examples/). ## Click Complex Applications @@ -145,6 +241,9 @@ Start working with `nornir_cli` by exporting the environment variables: export NORNIR_CLI_USERNAME=username export NORNIR_CLI_PASSWORD=password ``` + +And with `NORNIR_CLI_GRP_EXCEPTIONS` environment variable you can exclude directoiries from being displayed in `Runbook collections` (see [here](https://timeforplanb123.github.io/nornir_cli/useful/#custom-runbooks)) + Or you can permanently declare environment variables using `.bash_profile` file: ```text @@ -159,6 +258,66 @@ source .bash_profile And now you can do `init` command +## What else can nornir_cli be useful + +#### Useful functions + +* `_info` + + use `_info` to add statistic to your nornir runbook + + ```python + from nornir_cli.common_commands import _info + + # code + # ... + + _info(nr, task) + # where is :nr: is nornir.core.Nornir object + # :task: is nornir.task.Task object + ``` + The `_info` function show statistic in the following format: + + ```text + dev_1 : ok=1 changed=0 failed=0 + dev_2 : ok=1 changed=0 failed=0 + dev_3 : ok=1 changed=0 failed=0 + + OK : 3 + CHANGED : 0 + FAILED : 0 + ``` + +* `_pickle_to_hidden_file` + + If you don't want to use [`runbook collections`](http://timeforplanb123.github.io/nornir_cli/workflow/#runbook-collections) you can use `nornir_cli` for inventory management only. + + You can get `nornir.core.Nornir` object with inventory, filter this inventory and save it using `nornir_cli`, and then use the result: + + ```text + # get nornir.core.Nornir object, filter inventory and save it + $ nornir_cli init nornir-scrapli filter -s -a 'name__contains=dev' + ``` + + ```python + from nornir_cli.common_commands import _pickle_to_hidden_file + + def cli(nr): + def task(task): + # code + # ... + + if __name__ == "__main__": + # get current nornir.core.Nornir object from nornir_cli + nr = _pickle_to_hidden_file("temp.pkl", mode="rb", dump=False) + # run task with this object + cli(nr) + ``` + +#### nornir_jinja2 plugin + +Why is the `nornir_jinja2` plugin here? And then, together with NetBox, this is a really useful thing. You can use NetBox as a variable source for jinja2 templates. Then `nornir_cli` can replace the tool for generating configs. It also motivates you to keep NetBox up-to-date as a Source of Truth. And we need such a motivation, based on the connectivity of different tools. + ## How to craft xml from yang When using `scrapli_netconf` from `nornir_cli`, you may find it useful to be able to get xml from yang. @@ -172,7 +331,7 @@ cd /to/directory/with/yang/models # use pyang tool # huawei is here as example only -$ pyang -f jstree -o huawei.ifm.yang huawei-ifm.html +$ pyang -f jstree -o huawei-ifm.html huawei-ifm.yang # open html in browser diff --git a/docs/workflow.md b/docs/workflow.md index bc280a4..14de4c4 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.1.0`, only Connection plugins are available: +For version `0.2.0`, only Connection plugins and `nornir_jinja2` are available: ```text $ nornir_cli --help Usage: nornir_cli [OPTIONS] COMMAND [ARGS]... @@ -38,7 +38,7 @@ Options: --help Show this message and exit. Commands: - custom custom nornir runbooks + nornir_jinja2 nornir_jinja2 plugin nornir-napalm nornir_napalm plugin nornir-netmiko nornir_netmiko plugin nornir-scrapli nornir_scrapli plugin @@ -196,12 +196,14 @@ $ nornir_cli nornir-scrapli show_inventory -i hosts -h -g --count 6 # groups list [] ``` -`-cou` or `--counts` shows hosts/groups list and/or hosts/groups/defaults inventory. +`-cou` or `--count` shows hosts/groups list and/or hosts/groups/defaults inventory. - `--count -100` - last 100 items - `--count 100` - first 100 items +- `--count 0` - all items (default value) + And you can invoke `show_inventory` command from `init` or `filter` commands with `-i / --inventory`, `-h / --hosts`, `-g / --groups` options. @@ -215,7 +217,7 @@ And you can invoke `show_inventory` command from `init` or `filter` commands wit Ok, now we have filtered inventory and let's start some Task based on Nornir Plugins: -At first, let's check all available Tasks/commands for Connection plugins, as instance: +At first, let's check all available Tasks/commands for current list of nornir plugins: === "norir-netmiko:" ```text @@ -309,6 +311,27 @@ At first, let's check all available Tasks/commands for Connection plugins, as in napalm_validate Gather information with napalm and validate it ``` +=== "nornir-jinja2:" + ```text + $ nornir_cli nornir-jinja2 + Usage: nornir_cli nornir-jinja2 [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 + [ARGS]...]... + + nornir_jinja2 plugin + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + template_file Renders contants of a file with jinja2. All the host data + is available in the template + + template_string Renders a string with jinja2. All the host data is + available in the template + ``` And start `netmiko_send_command`, for example: @@ -439,20 +462,55 @@ FAILED : 0 **Again, i repeat that each argument can be passed to the Task as a json string. This can be seen from the example above.** -#### Runbooks +#### Runbook collections -You can add a collection of your Nornir runbooks to a custom group and run them for any hosts from CLI. This is convinient. +You can create and manage a collection of your nornir runbooks using `nornir_cli`. -[**How to add your custom Nornir runbook in `nornir_cli`**](https://timeforplanb123.github.io/nornir_cli/useful/#how-to-add-custom-runbook) +Create any directory trees in the `custom_commands` directory and put your nornir runbooks there, following the simple rules. Then run them for any hosts from CLI, managing your inventory with `nornir_cli`. This is very similar to [`Ansible Roles`](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html){target="_blank"}. -For example, I have [Nornir runbook called `"dhcp_snooping"`](https://timeforplanb123.github.io/nornir_cli/examples/#custom-nornir-runbooks). Let's run it on all access switches from our NetBox Inventory: +For example, I have [Nornir runbook called `"cmd_dhcp_snooping.py"`](https://timeforplanb123.github.io/nornir_cli/examples/#custom-nornir-runbooks), and I want to add it to a `dhcp` group in `nornir_cli`. +Then, our directory tree: + +```text +$ tree ~/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/ +/home/user/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/ +├── dhcp +│   ├── cmd_dhcp_snooping.py +│   └── templates +│   ├── dhcp_snooping.j2 +│   └── disp_int.template +└── __init__.py + +2 directories, 4 files +``` -=== "our custom runbook:" +And our `nornir_cli` structure: + +=== "nornir_cli:" + ```text + $ nornir_cli + Usage: nornir_cli [OPTIONS] COMMAND [ARGS]... + + Nornir CLI + + Orchestrate your Inventory and start Tasks and Runbooks + + Options: + --version Show the version and exit. + --help Show this message and exit. + + Commands: + dhcp + nornir-jinja2 nornir_jinja2 plugin + nornir-napalm nornir_napalm plugin + nornir-netmiko nornir_netmiko plugin + nornir-scrapli nornir_scrapli plugin + ``` +=== "nornir_cli dhcp:" ```text - $ nornir_cli custom --help - Usage: nornir_cli custom [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... + $ nornir_cli dhcp + Usage: nornir_cli dhcp [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... - custom nornir runbooks Options: --help Show this message and exit. @@ -461,11 +519,14 @@ For example, I have [Nornir runbook called `"dhcp_snooping"`](https://timeforpla init Initialize a Nornir filter Do simple or advanced filtering show_inventory Show current inventory - dhcp_snooping + dhcp_snooping Configure dhcp snooping ``` + +Ok, let's run our `dhcp_snooping` command on all access switches from our NetBox Inventory: + === "get access switches:" ```text - $ nornir_cli nornir-netmiko init filter --hosts -a 'data__device_type__model__contains=S2320-28TP-EI-DC & name__contains=access' + $ nornir_cli dhcp init filter --hosts -s -a 'data__device_type__model__contains=S2320-28TP-EI-DC & name__contains=access' Are you sure you want to output all on stdout? [y/N]: y [ "access_1" @@ -473,10 +534,12 @@ For example, I have [Nornir runbook called `"dhcp_snooping"`](https://timeforpla ``` === "run dhcp_snooping:" ```text - $ nornir_cli custom dhcp_snooping + $ nornir_cli dhcp dhcp_snooping access_1 : ok=1 changed=1 failed=0 OK : 1 CHANGED : 1 FAILED : 0 ``` + +More detailed - [**How to add your custom Nornir runbook in `nornir_cli`**](https://timeforplanb123.github.io/nornir_cli/useful/#how-to-add-custom-nornir-runbook) diff --git a/examples/custom_runbooks/README.md b/examples/custom_runbooks/README.md index 51e0938..3188065 100644 --- a/examples/custom_runbooks/README.md +++ b/examples/custom_runbooks/README.md @@ -1,3 +1,5 @@ -How to add custom runbook +How to add custom runbook -Custom Nornir runbooks +Custom Nornir runbooks + +Note, that your nornir runbook will not appear in `nornir_cli` if the python modules you are using are not installed in the python environment diff --git a/examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py b/examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py index e41ef51..b8f7781 100644 --- a/examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py +++ b/examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py @@ -1,13 +1,10 @@ import os -import click from nornir_netmiko import netmiko_send_command, netmiko_send_config from nornir.core.plugins.connections import ConnectionPluginRegister from nornir_jinja2.plugins.tasks import template_file -from nornir_cli.common_commands import custom -from nornir_cli.plugin_commands.cmd_common import _get_color +from nornir_cli.common_commands import custom, _info -@click.command("dhcp_snooping") @custom def cli(ctx): def _get_trusted_untrusted(task): @@ -54,24 +51,4 @@ def _get_trusted_untrusted(task): task = ctx.run(task=_get_trusted_untrusted, on_failed=True) # Show statistic - ch_sum = 0 - for host in ctx.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(ctx.data.failed_hosts) - ok_sum = len(ctx.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() + _info(ctx, task) diff --git a/mkdocs.yml b/mkdocs.yml index f93f5c5..b4f0d31 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,10 +24,29 @@ theme: search_index_only: true language: en + features: + - navigation.top + - navigation.instant + palette: - scheme: preference - primary: black - accent: green + + # Light mode + - media: "(prefers-color-scheme: light)" + scheme: slate + primary: black + accent: green + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + + # Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: default + primary: black + accent: green + toggle: + icon: material/lightbulb + name: Switch to light mode font: text: Manrope code: Fira Mono @@ -37,9 +56,6 @@ theme: icon: repo: fontawesome/brands/github-alt -extra_css: - - stylesheets/extra.css - # Customization extra: social: @@ -47,7 +63,7 @@ extra: link: https://github.com/timeforplanb123 - icon: fontawesome/brands/twitter link: https://twitter.com/timeforplanb1 -# + # Extensions markdown_extensions: - markdown.extensions.admonition diff --git a/nornir_cli/__init__.py b/nornir_cli/__init__.py index b794fd4..d3ec452 100644 --- a/nornir_cli/__init__.py +++ b/nornir_cli/__init__.py @@ -1 +1 @@ -__version__ = '0.1.0' +__version__ = "0.2.0" diff --git a/nornir_cli/common_commands/__init__.py b/nornir_cli/common_commands/__init__.py index 7ed8e09..04737ea 100644 --- a/nornir_cli/common_commands/__init__.py +++ b/nornir_cli/common_commands/__init__.py @@ -8,6 +8,7 @@ _doc_generator, _validate_connection_options, custom, + _info, ) # from nornir_cli.common_commands.cmd_show_hosts import common_options @@ -22,4 +23,5 @@ "_doc_generator", "_validate_connection_options", "custom", + "_info", ) diff --git a/nornir_cli/common_commands/common.py b/nornir_cli/common_commands/common.py index 1a9476b..9d81a51 100644 --- a/nornir_cli/common_commands/common.py +++ b/nornir_cli/common_commands/common.py @@ -11,6 +11,7 @@ # custom decorator to get the current Nornir object and put it to ctx parameter def custom(f): + @click.command(help=f.__doc__) @click.pass_context def wrapper(ctx, *args, **kwargs): try: @@ -152,3 +153,38 @@ def _doc_generator(s): help="Number of elements you want to show", ), ] + + +def _get_color(f, ch): + if f: + color = "red" + elif ch: + color = "yellow" + else: + color = "green" + return color + + +# function showing statistic +def _info(ctx, task): + ch_sum = 0 + for host in ctx.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(ctx.data.failed_hosts) + ok_sum = len(ctx.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/nornir_cli/nornir_cli.py b/nornir_cli/nornir_cli.py index 71e5843..80c2090 100644 --- a/nornir_cli/nornir_cli.py +++ b/nornir_cli/nornir_cli.py @@ -6,7 +6,8 @@ from nornir_cli import __version__ -CMD_FOLDERS = ["common_commands", "custom_commands"] +# CMD_FOLDERS = ["common_commands", "custom_commands"] +CMD_FOLDERS = ["common_commands"] PACKAGE_NAME = "nornir_cli" @@ -30,14 +31,6 @@ bool: click.BOOL, } -# options callback -# def callback(ctx, param, value): -# if value != param.get_default(ctx): -# ctx.obj["kwargs"].update(dict([[param.name, _json_loads([value])[0]]])) -# ctx.obj["parameters"].append({ctx.obj["original"]: ctx.obj["kwargs"]}) -# ctx.obj["generator"] = ( -# dict(list(item.items())) for item in ctx.obj["parameters"] -# ) # get original function from original modules (check SETTINGS) def get_sources(plugin, l): @@ -71,7 +64,7 @@ def _get_cli(path, cmd_name, cmd_folder): # mini factory for the production of classes for our plugins # https://click.palletsprojects.com/en/7.x/commands/?highlight=multi%20command#custom-multi-commands -def class_factory(name, plugin, BaseClass=click.Group): +def class_factory(name, plugin, cmd_path=[], BaseClass=click.Group): def list_commands(self, ctx): ctx.obj[plugin] = __import__(plugin, fromlist=["tasks"]) return [ @@ -81,7 +74,7 @@ def list_commands(self, ctx): ] + list(ctx.obj[plugin].tasks.__all__) def list_custom_commands(self, ctx): - cmd_folders = _get_cmd_folder(CMD_FOLDERS) + cmd_folders = _get_cmd_folder(CMD_FOLDERS + cmd_path) return [ filename[4:-3] for filename in chain(*map(os.listdir, cmd_folders)) @@ -116,8 +109,12 @@ 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_FOLDERS): + for abs_path, rel_path in zip( + _get_cmd_folder(CMD_FOLDERS + cmd_path), + CMD_FOLDERS + [i.replace(os.sep, ".") for i in cmd_path], + ): command = _get_cli(f"{PACKAGE_NAME}.{rel_path}", cmd_name, abs_path) if command: return command @@ -138,9 +135,67 @@ def get_custom_command(self, ctx, cmd_name): # dynamically create a class for plugin/group and inherit it def dec(param=None): def wrapper(f): - return init_nornir_cli.group(cls=scls, chain=True)(f) - - scls = class_factory("LazyClass", param) + def finder(): + nonlocal gr + + nonlocal tree + + nonlocal path + + for p in tree: + if not [ex for ex in custom_exceptions if p[0].endswith(ex)] and not [ + p for p in p[1] if p not in custom_exceptions + ]: + if [ + filename + for filename in p[2] + if filename.endswith(".py") and filename.startswith("cmd_") + ]: + + cmd_path = p[0].split(path)[1] + + grps = cmd_path.split(os.sep)[1:] + for grp in grps[:-1]: + if grp in gr.commands: + gr = gr.commands[grp] + continue + gr = gr.group(name=grp)(f) + gr.group( + name=grps[-1], + cls=class_factory( + "LazyClass", param, ["custom_commands" + cmd_path] + ), + chain=True, + )(f) + gr = init_nornir_cli + finder() + + if f.__name__ == "custom": + path = next(_get_cmd_folder(["custom_commands"])) + tree = os.walk(path) + + root = next(tree) + + gr = init_nornir_cli + if not [d for d in root[1] if d not in custom_exceptions]: + if [ + filename + for filename in root[2] + if filename.endswith(".py") and filename.startswith("cmd_") + ]: + return gr.group(name=f.__name__, cls=scls, chain=True)(f) + + return finder() + + else: + init_nornir_cli.group(cls=scls, chain=True)(f) + + grp_exceptions = os.environ.get("NORNIR_CLI_GRP_EXCEPTIONS") + + custom_exceptions = ["__pycache__", "templates"] + if grp_exceptions: + custom_exceptions += grp_exceptions.split(",") + scls = class_factory("LazyClass", param, ["custom_commands"]) return wrapper @@ -195,9 +250,6 @@ def wrapper(f): default=default_value, show_default=True, required=False if default_value else True, - # expose_value=False, - # callback=callback, - # is_eager=True, type=PARAMETER_TYPES.setdefault(type(v.default), click.STRING), )(cmd) # last original functions with arguments @@ -261,9 +313,16 @@ def nornir_napalm(): pass +@dec("nornir_jinja2.plugins") +def nornir_jinja2(): + """ + nornir_jinja2 plugin + """ + pass + + @dec() def custom(): """ - custom nornir runbooks """ pass diff --git a/nornir_cli/plugin_commands/cmd_common.py b/nornir_cli/plugin_commands/cmd_common.py index 2407765..723ba94 100644 --- a/nornir_cli/plugin_commands/cmd_common.py +++ b/nornir_cli/plugin_commands/cmd_common.py @@ -2,18 +2,18 @@ from nornir_utils.plugins.functions import print_result import logging from nornir.core.plugins.connections import ConnectionPluginRegister -from nornir_cli.common_commands import _pickle_to_hidden_file, _json_loads +from nornir_cli.common_commands import _pickle_to_hidden_file, _json_loads, _info from tqdm import tqdm -def _get_color(f, ch): - if f: - color = "red" - elif ch: - color = "yellow" - else: - color = "green" - return color +# 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): @@ -74,24 +74,25 @@ def cli(ctx, pg_bar, show_result, *args, **kwargs): print() # show statistic - 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() + _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/nornir_cli/transform/function.py b/nornir_cli/transform/function.py index 15d0d17..1932930 100644 --- a/nornir_cli/transform/function.py +++ b/nornir_cli/transform/function.py @@ -1,4 +1,4 @@ def adapt_host_data(host): pass - #host.username="" - #host.password="" + # host.username="" + # host.password="" diff --git a/poetry.lock b/poetry.lock index 3045a58..f0df819 100644 --- a/poetry.lock +++ b/poetry.lock @@ -469,6 +469,18 @@ 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-jinja2" +version = "0.1.2" +description = "Jinja2 plugins for nornir" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +jinja2 = ">=2.11.2,<3.0.0" +nornir = ">=3,<4" + [[package]] name = "nornir-napalm" version = "0.1.2" @@ -1048,7 +1060,7 @@ pyyaml = "*" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "3c3d7a8f969ad56acd814d4d7b8808a296806259912fd559ebb7aa15182f99cd" +content-hash = "34dd68affcc65a33658358681dabc848e581fad06530a85ed178637a29d08be3" [metadata.files] appdirs = [ @@ -1330,6 +1342,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-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"}, +] nornir-napalm = [ {file = "nornir_napalm-0.1.2-py3-none-any.whl", hash = "sha256:313986bbb16eeaf3a80daf61f6a97e93ecebc47d0c586a2eb856ea91edc2cc1d"}, {file = "nornir_napalm-0.1.2.tar.gz", hash = "sha256:be7808a990242987500a65701edb626197c5d0b87f35d9eb5da7ce7e4d60fdd5"}, diff --git a/pyproject.toml b/pyproject.toml index 470db31..ea8cd7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nornir_cli" -version = "0.1.0" +version = "0.2.0" description = "Nornir CLI" license = "MIT" readme = "README.md" @@ -17,6 +17,7 @@ python = "^3.8" 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" nornir-scrapli = "2021.01.30" diff --git a/tests/test_nornir_cli.py b/tests/test_nornir_cli.py index a9a40b0..44e9e29 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.1.0"