From f274f965039a10c38632742c27e7eb2970f74221 Mon Sep 17 00:00:00 2001 From: timeforplanb123 Date: Fri, 26 Mar 2021 13:38:31 +0300 Subject: [PATCH] Oh! It's first commit --- .github/workflows/docs.yml | 16 + .gitignore | 29 + Dockerfile | 6 + LICENSE | 21 + README.md | 114 ++ docs/examples.md | 329 ++++ docs/index.md | 100 + docs/useful.md | 204 ++ docs/workflow.md | 482 +++++ examples/custom_runbooks/README.md | 3 + .../dhcp_snooping/cmd_dhcp_snooping.py | 77 + .../dhcp_snooping/templates/dhcp_snooping.j2 | 21 + .../dhcp_snooping/templates/disp_int.template | 11 + mkdocs.yml | 87 + nornir_cli/__init__.py | 1 + nornir_cli/common_commands/__init__.py | 25 + nornir_cli/common_commands/cmd_filter.py | 106 + nornir_cli/common_commands/cmd_init.py | 150 ++ .../common_commands/cmd_show_inventory.py | 69 + nornir_cli/common_commands/common.py | 154 ++ nornir_cli/custom_commands/__init__.py | 0 nornir_cli/nornir_cli.py | 269 +++ nornir_cli/plugin_commands/__init__.py | 0 nornir_cli/plugin_commands/cmd_common.py | 97 + nornir_cli/transform/function.py | 4 + poetry.lock | 1722 +++++++++++++++++ pyproject.toml | 41 + tests/__init__.py | 0 tests/test_nornir_cli.py | 5 + 29 files changed, 4143 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/examples.md create mode 100644 docs/index.md create mode 100644 docs/useful.md create mode 100644 docs/workflow.md create mode 100644 examples/custom_runbooks/README.md create mode 100644 examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py create mode 100644 examples/custom_runbooks/dhcp_snooping/templates/dhcp_snooping.j2 create mode 100644 examples/custom_runbooks/dhcp_snooping/templates/disp_int.template create mode 100644 mkdocs.yml create mode 100644 nornir_cli/__init__.py create mode 100644 nornir_cli/common_commands/__init__.py create mode 100644 nornir_cli/common_commands/cmd_filter.py create mode 100644 nornir_cli/common_commands/cmd_init.py create mode 100644 nornir_cli/common_commands/cmd_show_inventory.py create mode 100644 nornir_cli/common_commands/common.py create mode 100644 nornir_cli/custom_commands/__init__.py create mode 100644 nornir_cli/nornir_cli.py create mode 100644 nornir_cli/plugin_commands/__init__.py create mode 100644 nornir_cli/plugin_commands/cmd_common.py create mode 100644 nornir_cli/transform/function.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_nornir_cli.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f1e1945 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,16 @@ +name: docs +on: + push: + branches: + - master + - main +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.x + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d7120d --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Distribution / packaging +build/ +wheels/ +sdist/ +dist/ + +# Service files +config.yaml +nornir.log +.temp.pkl + +*.swp +nornir_cli/__pycache__/ +nornir_cli/*.swp + +nornir_cli/**/__pycache__/ +nornir_cli/**/*.swp + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# IPython +profile_default/ +ipython_config.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ba525fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.8-slim-buster + +WORKDIR /app + +COPY . . +RUN pip3 install . diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d74844d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Pavel Shemetov + +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/README.md b/README.md new file mode 100644 index 0000000..6569bde --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) + +nornir_cli +========== + +--- + +**Documentation**: https://timeforplanb123.github.io/nornir_cli + +--- + +**nornir_cli** is CLI tool based on Nornir framework, +Nornir Plugins and Click + + +## Features + +* **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 + +* **Manage Inventory** + + Get Inventory, filter Inventory, output Inventory and save Inventory state from the CLI. + This is really useful for large, structured Inventory - for example, NetBox with nornir_netbox plugin. + +* **Run Nornir Plugins** + + Run Tasks based on Nornir Plugins from the CLI, check result and statistic + +* **Build a chain of `nornir_cli` commands** + + Initialize Nornir, filter Inventory and start Task/Tasks chains or runbook/runbooks chains in one command + +* **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 + +## Quick Start + +#### Install + +Please, at first, check the dependencies in `pyproject.toml` and create new virtual environment if necessary and then: + +**with pip:** + +```text +pip install nornir_cli +``` + +**with git:** + +```text +git clone https://github.com/timeforplanb123/nornir_cli.git +cd nornir_cli +pip install . +# or +poetry install +``` + +**with Docker:** + +```text +git clone https://github.com/timeforplanb123/nornir_cli.git +cd nornir_cli +docker build -t timeforplanb123/nornir_cli . +docker run --rm -it timeforplanb123/nornir_cli sh + +# nornir_cli --version +nornir_cli, version 0.1.0 + +``` + +#### Simple Example + + +#### config.yaml +```yaml +# Simple Nornir configuration file +inventory: + plugin: SimpleInventory + options: + host_file: "inventory/hosts.yaml" +``` +#### hosts.yaml +```yaml +# Single host inventory +dev_1: + hostname: 10.1.0.1 + username: username + password: password + # huawei is just an example here + platform: huawei +``` +#### nornir_cli +```text +# As instance, let's run netmiko_send_command + +$ nornir_cli nornir-netmiko init netmiko_send_command --command_string "display clock" + +netmiko_send_command************************************************************ +* dev_1 ** changed : False ***************************************************** +vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +2021-03-17 14:04:22+03:00 +Wednesday +Time Zone(Moscow) : UTC+03:00 +^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=0 failed=0 + +OK : 1 +CHANGED : 0 +FAILED : 0 +``` diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..6de6a91 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,329 @@ +## Tasks and runbooks + +#### Single Task + +=== "one:" + ```text + # as one comand with config.yaml + + $ nornir_cli nornir-netmiko init -u username -p password \ + filter --hosts -a 'name__contains=dev_1 device_role__name__contains=leaf' \ + netmiko_send_command --command_string "display clock" + Are you sure you want to output all on stdout? [y/N]: y + [ + "dev_1" + ] + netmiko_send_command************************************************************ + * dev_1 ** changed : False ******************** + vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO + 2021-03-21 23:15:23+03:00 + Sunday + Time Zone(Moscow) : UTC+03:00 + ^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + dev_1 : ok=1 changed=0 failed=0 + + OK : 1 + CHANGED : 0 + FAILED : 0 + ``` +=== "two:" + ```text + # as one comand without config.yaml + + $ nornir_cli nornir-netmiko init -u username -p password -c "" -f \ + 'inventory={"plugin":"NetBoxInventory2", "options": {"nb_url": "your_netbox_domain", \ + "nb_token": "your_netbox_token", "ssl_verify": false}} \ + runner={"plugin": "threaded", "options": {"num_workers": 50}} \ + logging={"enabled":true, "level": "DEBUG", "to_console": true}' \ + filter --hosts -a 'name__contains=dev_1 \ + device_role__name__contains=leaf' netmiko_send_command --command_string "display clock" + + netmiko_send_command************************************************************ + * dev_1 ** changed : False ******************** + vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO + 2021-03-21 23:20:53+03:00 + Sunday + Time Zone(Moscow) : UTC+03:00 + ^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + dev_1 : ok=1 changed=0 failed=0 + + OK : 1 + CHANGED : 0 + FAILED : 0 + ``` +=== "three:" + ```text + # as many commands with config.yaml + + $ nornir_cli nornir-netmiko init -u username -p password + + $ nornir_cli nornir-netmiko filter --hosts -a -s 'name__contains=dev_1 device_role__name__contains=leaf' + + Are you sure you want to output all on stdout? [y/N]: y + [ + "dev_1" + ] + + $ nornir_cli nornir-netmiko netmiko_send_command --command_string "display clock" + + netmiko_send_command************************************************************ + * dev_1 ** changed : False ******************** + vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO + 2021-03-21 23:30:13+03:00 + Sunday + Time Zone(Moscow) : UTC+03:00 + ^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + dev_1 : ok=1 changed=0 failed=0 + + OK : 1 + CHANGED : 0 + FAILED : 0 + ``` +=== "four:" + ```text + # of course, the same thing can be done without a configuration file + ``` + +#### Tasks chains + +```text +# any chain of commands in a group/plugin is possible + +$ nornir_cli nornir-scrapli init -u username -p password \ +-co '{"scrapli": {"platform": "huawei_vrp", "extras":{"ssh_config_file": true}}}' \ +filter --hosts -s 'name=dev_1' send_command --command "display clock" \ +send_interactive --interact_events '[["save", "Are you sure to continue?[Y/N]", \ +false], ["Y", "Save the configuration successfully.", true]]' +Are you sure you want to output all on stdout? [y/N]: y +[ + "dev_1" +] + +send_command******************************************************************** +* dev_1 ** changed : False ******************** +vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +2021-03-19 14:12:38+03:00 +Friday +Time Zone(Moscow) : UTC+03:00 +^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=0 failed=0 + +OK : 1 +CHANGED : 0 +FAILED : 0 + +send_interactive**************************************************************** +* dev_1 ** changed : True ********************* +vvvv send_interactive ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +^^^^ END send_interactive ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=1 failed=0 + +OK : 1 +CHANGED : 1 +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 run custom runbook](https://timeforplanb123.github.io/nornir_cli/workflow/#runbooks) + +And here is an example of this runbook: +=== "Nornir runbook example:" + ```python + # nornir_cli/custom_commands/cmd_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 + + + @click.command("dhcp_snooping") + @custom + def cli(ctx): + def _get_trusted_untrusted(task): + ConnectionPluginRegister.auto_register() + # Get parameters in format: + # [ { 'description': 'NNI', + # 'mac_address': 'xxxx-yyyy-zzzz', + # 'mtu': '', + # 'name': 'Ethernet0/0/1'},] + intfs = task.run( + task=netmiko_send_command, + name="interfaces list", + command_string="disp int", + use_textfsm=True, + textfsm_template=os.path.join( + os.getcwd(), "nornir_cli/custom_commands/templates/disp_int.template" + ), + ) + # Get trusted interfaces + task.host["trusted"] = [ + i["name"] for i in intfs.result if "NNI" in i["description"] + ] + # Get untrusted interfaces + task.host["untrusted"] = [ + i["name"] + for i in intfs.result + if "NNI" not in i["description"] and not i["mtu"] + ] + # Render j2 template + template = task.run( + task=template_file, + path="nornir_cli/custom_commands/templates", + template="dhcp_snooping.j2", + ) + # Configure commands from j2 template + task.host["template"] = template.result + task.run( + task=netmiko_send_config, + name="Configure dhcp snooping", + config_commands=template.result, + cmd_verify=False, + exit_config_mode=False, + ) + + 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() + ``` +=== "jinja2 template:" + ```jinja + # nornir_cli/custom_commands/templates/dhcp_snooping.j2 + + dhcp enable + dhcp snooping enable + # + {% for intf in host.untrusted %} + interface {{ intf }} + dhcp snooping enable no-user-binding + Y + dhcp snooping check dhcp-chaddr enable + {% endfor %} + # + {% for intf in host.trusted %} + interface {{ intf }} + dhcp snooping enable no-user-binding + Y + dhcp snooping trusted + {% endfor %} + # + q + q + save + Y + ``` +=== "textfsm template:" + ```text + # nornir_cli/custom_commands/templates/disp_int.template + + Value NAME (\S+) + Value DESCRIPTION (.*) + Value MAC_ADDRESS (\w+-\w+-\w+) + Value MTU (\d+) + + Start + ^\S+ current state.* -> Continue.Record + ^${NAME} current state.* + ^Description:${DESCRIPTION} + ^.* Maximum Transmit Unit is ${MTU} + ^.* Hardware address is ${MAC_ADDRESS} + ``` +#### Textfsm + +```text +$ nornir_cli nornir-netmiko netmiko_send_command --command_string "disp int" --use_textfsm True --textfsm_template nornir_cli/custom_commands/templates/disp_int.template + +netmiko_send_command************************************************************ +* dev_1 ** changed : False ******************** +vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +[ { 'description': 'NNI', + 'mac_address': 'f898-ef49-b5d0', + 'mtu': '', + 'name': 'Ethernet0/0/1'}, + { 'description': '', + 'mac_address': 'f898-ef49-b5d0', + 'mtu': '', + 'name': 'Ethernet0/0/2'}, + + ... + +] +^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=0 failed=0 + +OK : 1 +CHANGED : 0 +FAILED : 0 +``` + +## --help + +The help option can be used anywhere, for example: + +```text +$ nornir_cli nornir-netmiko init -u username -p password filter \ +--hosts -a -s 'name__contains=dev_1 device_role__name__contains=leaf' netmiko_save_config --help + +Usage: nornir_cli nornir-netmiko netmiko_save_config [OPTIONS] + + Execute Netmiko save_config method + + Arguments: cmd(str, optional): Command used to save the configuration. + confirm(bool, optional): Does device prompt for confirmation before + executing save operation confirm_response(str, optional): Response + send to device when it prompts for confirmation + + Returns: nornir result(optional), statistic, progress bar(optional) + +Options: + --pg_bar [default: False] + --show_result / --no_show_result + [default: True] + --cmd TEXT [default: ] + --confirm BOOLEAN [default: False] + --confirm_response TEXT [default: ] + --help Show this message and exit. +``` + +## Logging + +By default, Nornir logs to a `nornir.log` file. +For logging to console configure `logging` parameter in `config.yaml` or do `init` from dictionary, as instance: +```text +$ nornir_cli nornir-netmiko init -c "" -f 'inventory={"plugin":"NetBoxInventory2", \ +"options": {"nb_url": "http://your_netbox_domain", "nb_token": "your_netbox_token", \ +"ssl_verify": false}} runner={"plugin": "threaded", "options": {"num_workers": 50}} \ +logging={"enabled":true, "level": "DEBUG", "to_console": true}' +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..65cffcf --- /dev/null +++ b/docs/index.md @@ -0,0 +1,100 @@ +**`nornir_cli`** is CLI tool based on [Nornir](https://github.com/nornir-automation/nornir){target="_blank"} framework, [Nornir Plugins](https://nornir.tech/nornir/plugins/){target="_blank"} and [Click](https://github.com/pallets/click){target="_blank"} + +## Features + +* **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 + +* **Manage Inventory** + + Get Inventory, filter Inventory, output Inventory and save Inventory state from the CLI. + This is really useful for large, structured Inventory - for example, [NetBox](https://github.com/netbox-community/netbox +){target="_blank"} with [nornir_netbox plugin](https://github.com/wvandeun/nornir_netbox){target="_blank"}. + +* **Run Nornir Plugins** + + Run Tasks based on Nornir Plugins from the CLI, check result and statistic + +* **Build a chain of `nornir_cli` commands** + + Initialize Nornir, filter Inventory and start Task/Tasks chains or runbook/runbooks chains in one command + +* **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 + +## Quick Start + +#### Install + +Please, at first, check the dependencies in `pyproject.toml` and create new virtual environment if necessary and then: + +**with pip:** + +```text +pip install nornir_cli +``` + +**with git:** + +```text +git clone https://github.com/timeforplanb123/nornir_cli.git +cd nornir_cli +pip install . +# or +poetry install +``` + +**with Docker:** + +```text +git clone https://github.com/timeforplanb123/nornir_cli.git +cd nornir_cli +docker build -t timeforplanb123/nornir_cli . +docker run --rm -it timeforplanb123/nornir_cli sh + +# nornir_cli --version +nornir_cli, version 0.1.0 +``` + +#### Simple Example + +=== "config.yaml" + ```yaml + # Simple Nornir configuration file + inventory: + plugin: SimpleInventory + options: + host_file: "inventory/hosts.yaml" + ``` +=== "hosts.yaml" + ```yaml + # Single host inventory + dev_1: + hostname: 10.1.0.1 + username: username + password: password + # huawei is just an example here + platform: huawei + ``` +=== "nornir_cli" + ```text + # As instance, let's run netmiko_send_command + + $ nornir_cli nornir-netmiko init netmiko_send_command --command_string "display clock" + + netmiko_send_command************************************************************ + * dev_1 ** changed : False ***************************************************** + vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO + 2021-03-17 14:04:22+03:00 + Wednesday + Time Zone(Moscow) : UTC+03:00 + ^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + dev_1 : ok=1 changed=0 failed=0 + + OK : 1 + CHANGED : 0 + FAILED : 0 + ``` diff --git a/docs/useful.md b/docs/useful.md new file mode 100644 index 0000000..fe377f3 --- /dev/null +++ b/docs/useful.md @@ -0,0 +1,204 @@ +## Click Multi Commands feature + +All commands are loaded 'lazily' from different plugins (about [Click Multi Commands](https://click.palletsprojects.com/en/7.x/commands/?highlight=multi%20command#custom-multi-commands){target="_blank"} + [Click Multi Commands example](https://github.com/pallets/click/tree/master/examples/complex){target="_blank"}. This applies to Nornir plugins and manually written plugins. + + +In `nornir_cli` Click Multi Commands feature is implemented through class inheritance, created dynamically from `class_factory` function in `nornir_cli/nornir_cli.py`. +So, you can easily implement your command in `nornir_cli`, by following the rules described in `class_factory` function. +Add your command to one of the directories: + +* **nornir_cli/common_commands** - here are the commands common to all groups/plugins. Here you will find such commands like `init`, `filter`, `show_inventory` +* **nornir_cli/plugin_commands** - here are the commands, that run single Tasks based on Nornir plugins +* **nornir_cli/custom_commands** - directory for your custom commands based on Nornir + + +Where is these directories? +If you installed `nornir_cli` from pip: +``` +$ pip show nornir_cli +... +Location: /home/user/virtenvs/3.8.4/lib/python3.8/site-packages +... +$ ls /home/user/virtenvs/3.8.4/lib/python3.8/site-packages/nornir_cli +common_commands custom_commands __init__.py nornir_cli.py plugin_commands __pycache__ transform +``` + +If you installed `nornir_cli` from git: +``` +$ ls nornir_cli +common_commands custom_commands __init__.py nornir_cli.py plugin_commands __pycache__ transform +``` + +## Custom runbooks + +#### How to add custom 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: + +* 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): + def nornir_runbook(task): + ... + task = ctx.run(task=nornir_runbook) + ``` + +* name a file `cmd_something.py` (replace `something` on your own) and put it to `custom_commands` directory + +See [example](https://timeforplanb123.github.io/nornir_cli/examples/#custom-nornir-runbooks) and [Examples](https://timeforplanb123/nornir_cli/examples/). + +## Click Complex Applications + +[About Click Complex Application](https://click.palletsprojects.com/en/7.x/complex/#){target="_blank"} + +[Click complex app example](https://github.com/pallets/click/tree/1cb86096124299579156f2c983efe05585f1a01b/examples/complex){target="_blank"} + +An important role in `nornir_cli` is given to the [Context object](https://click.palletsprojects.com/en/7.x/complex/#contexts){target="_blank"}. +As instance, if we run that command: +```text +$ nornir_cli nornir-scrapli init -u username -p password \ +-co '{"scrapli": {"platform": "huawei_vrp", "extras":{"ssh_config_file": true}}}' \ +filter --hosts -s 'name=dev_1' send_command --command "display clock" \ +send_interactive --interact_events '[["save", "Are you sure to continue?[Y/N]", false], \ +["Y", "Save the configuration successfully.", true]]' +``` +Context object will be: +```python +{'queue_functions': [{: {'interact_events': , 'failed_when_contains': None, 'privilege_level': '', 'timeout_ops': None}}], 'queue_parameters': {: {'interact_events': , 'failed_when_contains': None, 'privilege_level': '', 'timeout_ops': None}}, 'nornir_scrapli': , 'original': , 'queue_functions_generator': .wrapper.. at 0x7f0182fafba0>} +``` + +* `ctx.obj["queue_functions"]` - queue(list) of dictionaries, where the key is original function, and the value is a set of argumetns. For Commands chains. +* `ctx.obj["queue_parameters"]` - last original function with arguments (from chain of commands) +* `ctx.obj[plugin]` - where plugin is "nornir_scrapli" +* `ctx.obj["original"]` - last original function without arguments (from chain of commands) +* `ctx.obj["queue_functions_generator"]` - ctx.obj["queue_functions"] in the form of a generator expression + +If we run custom runbok (as instance, it's called [`dhcp_snooping`](https://timeforplanb123.github.io/nornir_cli/examples/#custom-nornir-runbooks) from example above), then most likely we use a `@custom` decorator: +```text +$ nornir_cli custom dhcp_snooping +``` +and there is no Context object, but the first argument will be Nornir object. [See example](https://timeforplanb123.github.io/nornir_cli/examples/#custom-nornir-runbooks). + +## .temp.pkl + +`.temp.pkl` - is temporary file that stores the last Nornir object with your Inventory. +The Nornir object gets into this file after you run `init` command or `filter` command with `-s / --save` option. + +As instance: + +=== "init only:" + ```text + # this command saves the Nornir object with full Inventory to the .temp.pkl file + # and runs send_command Task for all Hosts from Inventoy + + $ nornir_cli nornir-scrapli init send_command --command "display clock" + ``` +=== "init and filter:" + ```text + # this command saves the Nornir object with full Inventory to the .temp.pkl file + # and then saves the Nornir object with filtered Inventory to the .temp.pkl file + # and runs send_command Task for all Hosts from filtered Inventory + + $ nornir_cli nornir-scrapli init filter -s -a 'name__contains=spine' send_command --command "display clock" + ``` +=== "filter only:" + ```text + # if we started init earlier, then we already have the Nornir object. + # It will be enough to filter the Nornir Inventory and save it to temp.pkl + + $ nornir_cli nornir-scrapli filter -a 'name__contains=leaf' send_command --command "display clock" send_command --command "display device" + ``` +=== "without init and filter:" + ```text + # if you run nornir_cli without init, filter commands, + # the nornir_cli will try to take the last Nornir object from this file (temp.pkl) + + $ nornir_cli nornir-scrapli send_command --command "display clock" + ``` + +Where is `temp.pkl`? + +```text +# if nornir_cli was installed via pip + +$ la /home/user/virtenvs/3.8.4/lib/python3.8/site-packages/nornir_cli/common_commands/ +cmd_filter.py cmd_init.py cmd_show_inventory.py common.py __init__.py __pycache__ .temp.pkl +``` + +## Transform function + +The transform function is implemented "on the knee". But, if you want to use it, then add your code to `adapt_host_data` from `nornir_cli/transform/fucntion.py` file and specify it in the `init`. + +## Environment Variables +The username and password can be taken from the environment variables. +Start working with `nornir_cli` by exporting the environment variables: + +```text +export NORNIR_CLI_USERNAME=username +export NORNIR_CLI_PASSWORD=password +``` +Or you can permanently declare environment variables using `.bash_profile` file: + +```text +cd ~ +open .bash_profile +# add to file +export NORNIR_CLI_USERNAME=username +export NORNIR_CLI_PASSWORD=password +# save the text file and refresh the bash profile +source .bash_profile +``` + +And now you can do `init` command + +## 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. +Easy way to get xml from yang: + +* get a model for your vedor. As instance, [here](https://github.com/YangModels/yang){target="_blank"} +* export yang to html, and copy the path: +```text +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 + +# open html in browser + +$ open huawei-ifm.html +``` +* find the path and remove unwanted: +```text +$ echo "/ifm:ifm/ifm:interfaces/ifm:interface/ifm:ifName" | sed 's/ifm://g' +/ifm/interfaces/interface/ifName +``` +* filter the [pyang](https://github.com/mbj4668/pyang){target="_blank"} output sample-xml-skeleton using the --sample-xml-skeleton-path and get xml: +```text +$ pyang -f sample-xml-skeleton --sample-xml-skeleton-path \ +"/ifm/interfaces/interface/ifName" huawei-ifm.yang | tr -d "\n" \ +| sed -r 's/>\s+ + + +``` +* [run `scrapli_netconf` from `nornir_cli`](https://timeforplanb123.github.io/nornir_cli/workflow/#what-about-netconf) + +Thanks [hellt](https://github.com/hellt){target="_blank"} for this tutorial. + +Sources: + +* [zero](https://github.com/hpreston/python_networking/blob/master/data_manipulation/yang/pyang-examples.sh){target="_blank"} +* [one](https://netdevops.me/nokia-yang-tree/){target="_blank"} +* [two](https://twitter.com/rganascim/status/1223221183753134080?s=09){target="_blank"} diff --git a/docs/workflow.md b/docs/workflow.md new file mode 100644 index 0000000..bc280a4 --- /dev/null +++ b/docs/workflow.md @@ -0,0 +1,482 @@ +**`nornir_cli`** has a workflow that is familiar to the nornir user. There is: + +* [Initializing Nornir](https://timeforplanb123.github.io/nornir_cli/workflow/#initializing-nornir) +* [Inventory](https://timeforplanb123.github.io/nornir_cli/workflow/#inventory) +* [Tasks and runbooks](https://timeforplanb123.github.io/nornir_cli/workflow/#tasks-and-runbooks) + +You can run a workflow as a single command or you can split it into parts + +## Initializing Nornir + +You can initialize nornir with a configuration file, with code or with a combination of both. + +#### With configuration file + +Let's start with a configuration file. It is a typical Nornir configuration file. + +By default, `nornir_cli` uses `config.yaml` in your current working directory. But you can specify path to your own configuration file with option `--config_file` or `-c`: +```text +# with default config.yaml in current working directory +$ nornir_cli nornir-netmiko init + +# with path to config_file +$ 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: +```text +$ nornir_cli --help +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: + custom custom nornir runbooks + nornir-napalm nornir_napalm plugin + nornir-netmiko nornir_netmiko plugin + nornir-scrapli nornir_scrapli plugin +``` +#### Without a configuration file + +You can initialize nornir programmatically without a configuration file. + +`-f` or `--from_dict` option waits json string: +```text +$ nornir_cli nornir-netmiko init -c "" -f 'inventory={"plugin":"NetBoxInventory2", \ +"options": {"nb_url": "http://your_netbox_domain", "nb_token": "your_netbox_token", \ +"ssl_verify": false}} runner={"plugin": "threaded", "options": {"num_workers": 50}} \ +logging={"enabled":true, "level": "DEBUG", "to_console": true}' +``` + +Or with a combination of both methods: + +#### Both ways +```text +$ nornir_cli nornir-netmiko init -f 'runner={"plugin": "threaded", "options": {"num_workers": 50}}' +``` + +## Inventory +`nornir_cli` follows `Nornir` [filtering interface](https://nornir.readthedocs.io/en/latest/tutorial/inventory.html#Filtering-the-inventory){target="_blank"}. + +`init` returns [nornir.core.Nornir](https://github.com/nornir-automation/nornir/blob/e4f6b8c6258ae2dcfb286098b30652c7a31ecf30/nornir/core/__init__.py#L18){target="_blank"} object, which saved to `temp.pkl` file for future reference. Now, let's filter inventory: + +#### Filtering + +=== "config.yaml:" + ```yaml + # we will use NetBox Inventory + inventory: + plugin: NetBoxInventory2 + options: + nb_url: "http://your_nebox_domain" + nb_token: "your_netbox_token" + ssl_verify: False + ``` +=== "simple filtering:" + ```text + # At first, let's do the init command with Nornir NetBox plugin + # -u / --username and -p / --password will assign a username and password in Defaults + # -co / --connection_options will assign options for Scrapli framework + # huawei is here just as an example + $ nornir_cli nornir-scrapli init -u username -p password -co '{"scrapli": {"platform": "huawei_vrp", "extras":{"ssh_config_file": true}}}' + + # let's filter something + $ nornir_cli nornir-scrapli filter --hosts name=dev_1 + Are you sure you want to output all on stdout? [y/N]: y + [ + "dev_1" + ] + ``` +=== "simple filtering with json string:" + ```text + # huawei is here just as an example + $ nornir_cli nornir-scrapli filter --count 1 --inventory=hosts --hosts 'primary_ip={"address": "10.1.0.1/32", "family": 4, "id": 13, "url": "http://your_netbox_domain/api/ipam/ip-addresses/13/"} name=dev_1' + { + "dev_1": { + "name": "dev_1", + "connection_options": {}, + "groups": [], + "data": { + "id": 13, + "name": "dev_1", + "display_name": "dev_1", + "device_type": { + + ... + + "hostname": "10.1.0.1", + "port": null, + "username": "username", + "password": "password", + "platform": "huawei" + } + } + [ + "dev_1" + ] + ``` +=== "advanced filtering:" + ```text + # -a / --advanced option enables advanced filtering + $ nornir_cli nornir-scrapli filter --hosts -a 'name__contains=dev_1 device_role__name__contains=spine' + Are you sure you want to output all on stdout? [y/N]: y + [ + "dev_1" + ] + + # the same with & + $ nornir_cli nornir-scrapli filter --hosts -a 'name__contains=dev_1 & device_role__name__contains=spine' + Are you sure you want to output all on stdout? [y/N]: y + [ + "dev_1" + ] + + # or | + $ nornir_cli nornir-scrapli filter --hosts -a 'name__contains=dev_1 | name__contains=dev_2' + Are you sure you want to output all on stdout? [y/N]: y + [ + "dev_1" + "dev_2" + ] + + # another example + $ nornir_cli nornir-netmiko filter --hosts --count 3 -a 'data__device_type__model__contains=S2320-28TP-EI-DC & name__contains=dev_ | data__device_type__model__contains=S2320-28TP-EI-DC & name__contains=access' + [ + "dev_1" + "dev_2" + "access_1" + ] + ``` + +**IMPORTANT:** if you want to save the Inventory state after filtering for future references, please use `--save` or `-s` option. + +#### Show inventory + +As you may have already noticed, it's possible to view the current inventory state with show_inventory command: +```text +$ nornir_cli nornir-scrapli show_inventory -i hosts -h -g --count 6 +# hosts inventory +{ + "dev_1": { + "name": "dev_1", + "connection_options": {}, + "groups": [], + "data": { + "id": 13, + "name": "dev_1", + "display_name": "dev_1", + "device_type": { + + ... + + "hostname": "10.1.0.1", + "port": null, + "username": "username", + "password": "password", + "platform": "huawei" + } +} + ... # there is 6 Hosts Inventory in json format +# hosts list +[ + "dev_1" + "dev_2" + "dev_3" + "dev_4" + "dev_5" + "access_1" +] +# groups list +[] +``` +`-cou` or `--counts` shows hosts/groups list and/or hosts/groups/defaults inventory. + +- `--count -100` - last 100 items + +- `--count 100` - first 100 items + +And you can invoke `show_inventory` command from `init` or `filter` commands with `-i / --inventory`, `-h / --hosts`, `-g / --groups` options. + + +## Tasks and runbooks + + +**I must say that each argument can be passed to the Task as a json string. It's important.** + + +#### Single tasks + +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: + +=== "norir-netmiko:" + ```text + $ nornir_cli nornir-netmiko --help + Usage: nornir_cli nornir-netmiko [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 + [ARGS]...]... + + nornir_netmiko plugin + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + netmiko_commit Execute Netmiko commit method + netmiko_file_transfer Execute Netmiko file_transfer method + netmiko_save_config Execute Netmiko save_config method + netmiko_send_command Execute Netmiko send_command method (or + send_command_timing) + + netmiko_send_config Execute Netmiko send_config_set method (or + send_config_from_file) + ``` +=== "nornir-scrapli:" + ```text + $ nornir_cli nornir-scrapli --help + Usage: nornir_cli nornir-scrapli [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 + [ARGS]...]... + + nornir_scrapli plugin + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + get_prompt Get current prompt from device using scrapli + netconf_capabilities Retrieve the device config with scrapli_netconf + netconf_commit Commit the device config with scrapli_netconf + netconf_delete_config Send a "delete-config" rcp to the device with + scrapli_netconf + + netconf_discard Discard the device config with scrapli_netconf + netconf_edit_config Edit config from the device with scrapli_netconf + netconf_get Get from the device with scrapli_netconf + netconf_get_config Get config from the device with scrapli_netconf + netconf_lock Lock the device with scrapli_netconf + netconf_rpc Send a "bare" rcp to the device with + scrapli_netconf + + netconf_unlock Unlock the device with scrapli_netconf + netconf_validate Send a "validate" rcp to the device with + scrapli_netconf + + send_command Send a single command to device using scrapli + send_commands Send a list of commands to device using scrapli + send_commands_from_file Send a list of commands from a file to device using + scrapli + + send_config Send a config to device using scrapli + send_configs Send configs to device using scrapli + send_configs_from_file Send configs from a file to device using scrapli + send_interactive Send inputs in an interactive fashion using + scrapli; usually used to handle prompts + + ``` +=== "nornir-napalm:" + ```text + $ nornir_cli nornir-napalm --help + Usage: nornir_cli nornir-napalm [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 + [ARGS]...]... + + nornir_napalm plugin + + Options: + --help Show this message and exit. + + Commands: + init Initialize a Nornir + filter Do simple or advanced filtering + show_inventory Show current inventory + napalm_cli Run commands on remote devices using napalm + napalm_configure Loads configuration into a network devices using napalm + napalm_get Gather information from network devices using napalm + napalm_ping Executes ping on the device and returns a dictionary with + the result + + napalm_validate Gather information with napalm and validate it + ``` + +And start `netmiko_send_command`, for example: + +```text +$ nornir_cli nornir-netmiko netmiko_send_command --command_string "display clock" +netmiko_send_command************************************************************ +* dev_1 ** changed : False ******************** +vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +2021-03-19 12:57:11+03:00 +Friday +Time Zone(Moscow) : UTC+03:00 +^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=0 failed=0 + +OK : 1 +CHANGED : 0 +FAILED : 0 +``` + +Before, we added connection options for scrapli in `init` command. +Let's check out some command: + +```text +$ nornir_cli nornir-scrapli send_command --command "display clock" +send_command******************************************************************** +* dev_1 ** changed : False ******************** +vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +2021-03-19 13:18:53+03:00 +Friday +Time Zone(Moscow) : UTC+03:00 +^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=0 failed=0 + +OK : 1 +CHANGED : 0 +FAILED : 0 +``` + +#### What about netconf? + +[Here is easy way to get xml tree from yang](https://timeforplanb123.github.io/nornir_cli/useful/#how-to-craft-xml-from-yang) + +`dev_3`, as instance, understands netconf. Get `nornir.core.Nornir` object for `dev_3` and run some netconf command with Scrapli: +```text +$ nornir_cli nornir-scrapli init -u username -p password -co '{"scrapli": {"platform": "huawei_vrp", "extras":{"ssh_config_file": true}}}' filter --hosts -s 'name=dev_3' +Are you sure you want to output all on stdout? [y/N]: y +[ + "dev_3" +] + +$ nornir_cli nornir-scrapli netconf_get --filter_='' --filter_type subtree + +... +netconf_get********************************************************************* +* dev_3 ** changed : False ************************************************ +vvvv netconf_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO + + + + + + 1 + Virtual-Template0 + + + ... + + + 717 + GigabitEthernet4/1/1.514 + + + + + + +^^^^ END netconf_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_3 : ok=1 changed=0 failed=0 + +OK : 1 +CHANGED : 0 +FAILED : 0 +``` + +#### Commands chains + +And, of course, you can run any commands chains, even those scary ones: + +```text +$ nornir_cli nornir-scrapli init -u username -p password \ +-co '{"scrapli": {"platform": "huawei_vrp", "extras":{"ssh_config_file": true}}}' \ +filter --hosts -s 'name=dev_1' send_command --command "display clock" \ +send_interactive --interact_events '[["save", "Are you sure to continue?[Y/N]", false], \ +["Y", "Save the configuration successfully.", true]]' +Are you sure you want to output all on stdout? [y/N]: y +[ + "dev_1" +] + +send_command******************************************************************** +* dev_1 ** changed : False ******************** +vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +2021-03-19 14:12:38+03:00 +Friday +Time Zone(Moscow) : UTC+03:00 +^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=0 failed=0 + +OK : 1 +CHANGED : 0 +FAILED : 0 + +send_interactive**************************************************************** +* dev_1 ** changed : True ********************* +vvvv send_interactive ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO +^^^^ END send_interactive ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +dev_1 : ok=1 changed=1 failed=0 + +OK : 1 +CHANGED : 1 +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 + +You can add a collection of your Nornir runbooks to a custom group and run them for any hosts from CLI. This is convinient. + +[**How to add your custom Nornir runbook in `nornir_cli`**](https://timeforplanb123.github.io/nornir_cli/useful/#how-to-add-custom-runbook) + +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: + +=== "our custom runbook:" + ```text + $ nornir_cli custom --help + Usage: nornir_cli custom [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... + + custom nornir runbooks + + 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 + ``` +=== "get access switches:" + ```text + $ nornir_cli nornir-netmiko init filter --hosts -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" + ] + ``` +=== "run dhcp_snooping:" + ```text + $ nornir_cli custom dhcp_snooping + access_1 : ok=1 changed=1 failed=0 + + OK : 1 + CHANGED : 1 + FAILED : 0 + ``` diff --git a/examples/custom_runbooks/README.md b/examples/custom_runbooks/README.md new file mode 100644 index 0000000..51e0938 --- /dev/null +++ b/examples/custom_runbooks/README.md @@ -0,0 +1,3 @@ +How to add custom runbook + +Custom Nornir runbooks diff --git a/examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py b/examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py new file mode 100644 index 0000000..e41ef51 --- /dev/null +++ b/examples/custom_runbooks/dhcp_snooping/cmd_dhcp_snooping.py @@ -0,0 +1,77 @@ +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 + + +@click.command("dhcp_snooping") +@custom +def cli(ctx): + def _get_trusted_untrusted(task): + ConnectionPluginRegister.auto_register() + # Get parameters in format: + # [ { 'description': 'NNI', + # 'mac_address': 'xxxx-yyyy-zzzz', + # 'mtu': '', + # 'name': 'Ethernet0/0/1'},] + intfs = task.run( + task=netmiko_send_command, + name="interfaces list", + command_string="disp int", + use_textfsm=True, + textfsm_template=os.path.join( + os.getcwd(), "nornir_cli/custom_commands/templates/disp_int.template" + ), + ) + # Get trusted interfaces + task.host["trusted"] = [ + i["name"] for i in intfs.result if "NNI" in i["description"] + ] + # Get untrusted interfaces + task.host["untrusted"] = [ + i["name"] + for i in intfs.result + if "NNI" not in i["description"] and not i["mtu"] + ] + # Render j2 template + template = task.run( + task=template_file, + path="nornir_cli/custom_commands/templates", + template="dhcp_snooping.j2", + ) + # Configure commands from j2 template + task.host["template"] = template.result + task.run( + task=netmiko_send_config, + name="Configure dhcp snooping", + config_commands=template.result, + cmd_verify=False, + exit_config_mode=False, + ) + + 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() diff --git a/examples/custom_runbooks/dhcp_snooping/templates/dhcp_snooping.j2 b/examples/custom_runbooks/dhcp_snooping/templates/dhcp_snooping.j2 new file mode 100644 index 0000000..50d233f --- /dev/null +++ b/examples/custom_runbooks/dhcp_snooping/templates/dhcp_snooping.j2 @@ -0,0 +1,21 @@ +dhcp enable +dhcp snooping enable +# +{% for intf in host.untrusted %} +interface {{ intf }} + dhcp snooping enable no-user-binding + Y + dhcp snooping check dhcp-chaddr enable +{% endfor %} +# +{% for intf in host.trusted %} +interface {{ intf }} + dhcp snooping enable no-user-binding + Y + dhcp snooping trusted +{% endfor %} +# +q +q +save +Y diff --git a/examples/custom_runbooks/dhcp_snooping/templates/disp_int.template b/examples/custom_runbooks/dhcp_snooping/templates/disp_int.template new file mode 100644 index 0000000..bbc5229 --- /dev/null +++ b/examples/custom_runbooks/dhcp_snooping/templates/disp_int.template @@ -0,0 +1,11 @@ +Value NAME (\S+) +Value DESCRIPTION (.*) +Value MAC_ADDRESS (\w+-\w+-\w+) +Value MTU (\d+) + +Start + ^\S+ current state.* -> Continue.Record + ^${NAME} current state.* + ^Description:${DESCRIPTION} + ^.* Maximum Transmit Unit is ${MTU} + ^.* Hardware address is ${MAC_ADDRESS} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f93f5c5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,87 @@ +site_name: nornir_cli +nav: + - Home: index.md + - Workflow: workflow.md + - Useful: useful.md + - Examples: examples.md + +site_author: Pavel Shemetov +site_description: >- + nornir_cli is CLI tool based on Nornir framework and Nornir Plugins +# Repository +repo_name: timeforplanb123/nornir_cli +repo_url: https://github.com/timeforplanb123/nornir_cli +edit_uri: "" +theme: + name: material + + # 404 page + static_templates: + - 404.html + + # Don't include MkDocs' JavaScript + include_search_page: false + search_index_only: true + + language: en + palette: + scheme: preference + primary: black + accent: green + font: + text: Manrope + code: Fira Mono + # icon: + logo: + favicon: + icon: + repo: fontawesome/brands/github-alt + +extra_css: + - stylesheets/extra.css + +# Customization +extra: + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/timeforplanb123 + - icon: fontawesome/brands/twitter + link: https://twitter.com/timeforplanb1 +# +# Extensions +markdown_extensions: + - markdown.extensions.admonition + - markdown.extensions.attr_list + - markdown.extensions.codehilite: + guess_lang: false + - markdown.extensions.def_list + - markdown.extensions.footnotes + - markdown.extensions.meta + - markdown.extensions.toc: + permalink: "#" + - pymdownx.arithmatex + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.critic + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + - pymdownx.highlight: + linenums_style: pymdownx-inline + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.magiclink: + repo_url_shorthand: true + user: squidfunk + repo: mkdocs-material + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.snippets: + check_paths: true + - pymdownx.superfences + - pymdownx.tabbed + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde diff --git a/nornir_cli/__init__.py b/nornir_cli/__init__.py new file mode 100644 index 0000000..b794fd4 --- /dev/null +++ b/nornir_cli/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.0' diff --git a/nornir_cli/common_commands/__init__.py b/nornir_cli/common_commands/__init__.py new file mode 100644 index 0000000..7ed8e09 --- /dev/null +++ b/nornir_cli/common_commands/__init__.py @@ -0,0 +1,25 @@ +# from nornir_cli.common_commands.cmd_init import _pickle_to_hidden_file +# from nornir_cli.common_commands.cmd_init import common_options +from nornir_cli.common_commands.common import ( + common_options, + _pickle_to_hidden_file, + _json_loads, + _get_lists, + _doc_generator, + _validate_connection_options, + custom, +) + +# from nornir_cli.common_commands.cmd_show_hosts import common_options +from nornir_cli.common_commands import cmd_show_inventory + +__all__ = ( + "_pickle_to_hidden_file", + "common_options", + "cmd_show_inventory", + "_json_loads", + "_get_lists", + "_doc_generator", + "_validate_connection_options", + "custom", +) diff --git a/nornir_cli/common_commands/cmd_filter.py b/nornir_cli/common_commands/cmd_filter.py new file mode 100644 index 0000000..c2f2557 --- /dev/null +++ b/nornir_cli/common_commands/cmd_filter.py @@ -0,0 +1,106 @@ +import click +from nornir.core.filter import F +from itertools import takewhile, dropwhile +from nornir_cli.common_commands import ( + cmd_show_inventory, + common_options, + _pickle_to_hidden_file, + _json_loads, + _get_lists, +) +from nornir_cli.common_commands.common import ( + SHOW_INVENTORY_OPTIONS, +) + +ERROR_MESSAGE = ( + "Filter optiions. There should be something like...\n\n" + "Simple filtering:\n" + " nornir_cli nornir-netmiko filter site=cmh role=spine\n\n" + "Simple filtering with json:\n" + " nornir_cli nornir-netmiko filter --hosts " + '\'primary_ip={"address": "10.1.129.71/32", "family": 4, "id": 4482, ' + '"url": "http://netbox-domain/api/ipam/ip-addresses/4482/"} name=spine_1\'\n\n' + "Advanced filtering:\n" + " nornir_cli nornir-netmiko filter -a " + "'name__contains=cmh device_role__name__contains=access'\n" + "The same:\n" + " nornir_cli nornir-netmiko filter -a " + "'name__contains=cmh & device_role__name__contains=access'\n" + "Or:\n" + " nornir_cli nornir-netmiko filter -a " + "'name__contains=cmh | name__contains=access'\n\n" + "where is optional command" +) + + +# add quotes for filter values +def _get_quotes(t): + return ", ".join(["{}='{}'".format(*_) for _ in [__.split("=") for __ in t]]) + + +# get Nornir object after advanced filter +def _get_obj_after_adv_filter(nr, t): + body = "" + t = t.split() + while True: + try: + begin = takewhile(lambda x: len(x) > 2, t) + body += f"F({_get_quotes(begin)}) " + t = dropwhile(lambda x: len(x) > 2, t) + body += f"{next(t)} " + t = tuple(t) + except StopIteration: + break + exec(f"o = nr.filter({body})") + return locals()["o"] + + +@click.command( + context_settings=dict( + ignore_unknown_options=True, + ), + short_help="Do simple or advanced filtering", +) +@click.option( + "-a", + "--advanced_filter", + is_flag=True, + help="Use an advanced filtering (string)", +) +@common_options(SHOW_INVENTORY_OPTIONS) +@click.option( + "-s", + "--save", + is_flag=True, + help="Save filtered Nornir object to pickle file for later use", +) +@click.argument("f", required=False) +@click.pass_context +# Leftover argumetns via ctx.args doesn't work. Oh, really? :'( https://github.com/pallets/click/issues/473 +def cli(ctx, advanced_filter, f, save, **kwargs): + """ + Do simple or advanced filtering + that will enable us to operate on groups of hosts + based on their properties. + """ + try: + ctx.obj["nornir"] = _pickle_to_hidden_file("temp.pkl", mode="rb", dump=False) + if advanced_filter: + ctx.obj["nornir"] = _get_obj_after_adv_filter(ctx.obj["nornir"], f) + else: + d = dict( + [_json_loads(i) for i in (value.split("=") for value in _get_lists(f))] + ) + ctx.obj["nornir"] = ctx.obj["nornir"].filter(**d) + if save: + _pickle_to_hidden_file("temp.pkl", obj=ctx.obj["nornir"]) + + # run show_inventory command + if any(kwargs.values()): + ctx.invoke(cmd_show_inventory.cli, **kwargs) + except (ValueError, IndexError): + raise ctx.fail( + click.BadParameter( + f"{ERROR_MESSAGE}", + ).format_message(), + ) diff --git a/nornir_cli/common_commands/cmd_init.py b/nornir_cli/common_commands/cmd_init.py new file mode 100644 index 0000000..356bed5 --- /dev/null +++ b/nornir_cli/common_commands/cmd_init.py @@ -0,0 +1,150 @@ +import click +from nornir import InitNornir +from nornir.core.plugins.inventory import TransformFunctionRegister +from nornir_cli.common_commands import ( + cmd_show_inventory, + common_options, + _pickle_to_hidden_file, + _json_loads, + _get_lists, +) +from nornir_cli.common_commands.common import ( + CONNECTION_OPTIONS, + SHOW_INVENTORY_OPTIONS, +) +from nornir_cli.transform.function import adapt_host_data + + +ERROR_MESSAGE = ( + "Init options (these may be options in configuration file or init options).\n" + "If the configuration file is correct or missing, check the command format" + "(use json syntax with '--from_dict' / '-f' option).\n" + "There should be something like...\n\n" + "With default configuration file:\n" + " nornir_cli custom init\n\n" + "Specifying the path to config file:\n" + " nornir_cli nornir-netmiko init -c ~/config.yaml\n\n" + "Without a configuration file. And use json syntax here" + "(programmatically initializing):\n" + ' nornir_cli nornir-scrapli init -c "" -f ' + '\'inventory={"plugin":"SimpleInventory",options": {"host_file": ' + '"/home/user/inventory/hosts.yaml", "group_file": ' + '"/home/user/inventory/groups.yaml", ' + '"defaults_file": "/home/user/inventory/defaults.yaml"}}\'\n\n' + "With combination of both methods:\n" + ' nornir_cli nornir-napalm init -f \'logging={"enabled": true, "level":' + '"DEBUG",' + '"to_console": false}' +) + + +@click.command("init", short_help="Initialize a Nornir") +@click.option( + "--config_file", + "-c", + default="config.yaml", + show_default=True, + type=click.Path(exists=False), + help="Path to configuration file", +) +@click.option( + "-f", + "--from_dict", + help="InitNornir dictionary arguments (json string)", +) +@common_options(CONNECTION_OPTIONS) +@click.option( + "--dry_run", + "-d", + default=False, + show_default=True, + type=bool, + help="Whether to simulate changes or not", +) +@click.option( + "-u", + "--username", + envvar="NORNIR_CLI_USERNAME", + help="Default username", +) +@click.option( + "-p", + "--password", + envvar="NORNIR_CLI_PASSWORD", + help="Default password", +) +@common_options(SHOW_INVENTORY_OPTIONS) +@click.pass_context +def cli( + ctx, + config_file, + dry_run, + username, + password, + from_dict, + connection_options, + **kwargs, +): + """ + Initialize nornir with a configuration file, with code + or with a combination of both. + """ + + ctx.ensure_object(dict) + + try: + + TransformFunctionRegister.register("adapt_host_data", adapt_host_data) + + if from_dict: + d = dict( + [ + _json_loads(i) + for i in (value.split("=") for value in _get_lists(from_dict)) + ] + ) + cf = ( + None + if not config_file or config_file == "None" or "null" + else config_file + ) + ctx.obj["nornir"] = InitNornir(config_file=cf, dry_run=dry_run, **d) + else: + ctx.obj["nornir"] = InitNornir(config_file=config_file, dry_run=dry_run) + + defaults = ctx.obj["nornir"].inventory.defaults + + if username: + defaults.username = username + if password: + defaults.password = password + + if connection_options: + for param in ctx.obj["nornir"].inventory.hosts.values(): + param.connection_options = connection_options + + _pickle_to_hidden_file("temp.pkl", obj=ctx.obj["nornir"]) + + # run show_inventory command + if any(kwargs.values()): + ctx.invoke(cmd_show_inventory.cli, **kwargs) + + except (ValueError, IndexError, TypeError, KeyError): + raise ctx.fail( + click.BadParameter( + f"{ERROR_MESSAGE}", + ).format_message(), + ) + except (AttributeError): + ctx.fail( + f"File '{config_file}' is empty", + ) + except (FileNotFoundError): + raise ctx.fail( + click.BadParameter( + f"Path '{config_file}' does not exist", + param_hint="'--config_file' / '-c'", + ).format_message(), + ) + except Exception as err: + raise click.ClickException(err) diff --git a/nornir_cli/common_commands/cmd_show_inventory.py b/nornir_cli/common_commands/cmd_show_inventory.py new file mode 100644 index 0000000..96508a4 --- /dev/null +++ b/nornir_cli/common_commands/cmd_show_inventory.py @@ -0,0 +1,69 @@ +import click +from itertools import islice +from nornir_cli.common_commands import ( + _pickle_to_hidden_file, + common_options, +) +from nornir_cli.common_commands.common import ( + SHOW_INVENTORY_OPTIONS, +) +import json + + +def _get_inventory(nr_obj, count, **kwargs): + + d = { # "inventory": nr_obj.inventory.dict().setdefault(kwargs["inventory"]).items(), + # "groups": nr_obj.inventory.groups, + # "hosts": nr_obj.inventory.hosts, + str: dict, + bool: list, + } + + if not any(kwargs.values()): + kwargs = { + "hosts": True, + } + + for k, v in kwargs.items(): + if v: + try: + o = nr_obj.inventory.dict()[k] + except KeyError: + o = nr_obj.inventory.dict()[v].items() + l = len(o) + _ = [0, count or l] if count >= 0 else [l + count, l] + json_string = json.dumps( + d[type(v)](islice(o, *_)), indent=4, ensure_ascii=False + ).encode("utf8") + yield json_string.decode() + + +@click.command() +@common_options(SHOW_INVENTORY_OPTIONS) +@click.pass_context +def cli(ctx, count, hosts, groups, inventory): + """ + Show current inventory + """ + + try: + nr = ctx.obj["nornir"] + except KeyError: + nr = _pickle_to_hidden_file("temp.pkl", mode="rb", dump=False) + + if not count or ctx.resilient_parsing: + click.confirm("Are you sure you want to output all on stdout?", abort=True) + + d = { + x: y + for x, y in zip(("inventory", "hosts", "groups"), (inventory, hosts, groups)) + } + try: + for item in _get_inventory(nr, count, **d): + print(item) + except ValueError: + raise ctx.fail( + click.BadParameter( + f"count cannot be equal to {count}", + ).format_message(), + ) diff --git a/nornir_cli/common_commands/common.py b/nornir_cli/common_commands/common.py new file mode 100644 index 0000000..1a9476b --- /dev/null +++ b/nornir_cli/common_commands/common.py @@ -0,0 +1,154 @@ +import click +import pickle +import functools +import platform +import json +import os +from itertools import takewhile, dropwhile +import re +from nornir.core.inventory import ConnectionOptions + + +# custom decorator to get the current Nornir object and put it to ctx parameter +def custom(f): + @click.pass_context + def wrapper(ctx, *args, **kwargs): + try: + ctx = ctx.obj["nornir"] + except KeyError: + ctx = _pickle_to_hidden_file("temp.pkl", mode="rb", dump=False) + return f(ctx, *args, **kwargs) + + return wrapper + + +# the decorator wraps the fucntion with general options +def common_options(options): + def wrapper(f): + return functools.reduce(lambda x, option: option(x), options, f) + + return wrapper + + +def _pickle_to_hidden_file(file_name, mode="wb", obj=None, dump=True): + try: + dot = "." if platform.system() not in "Windows" else "" + hidden_file_name = dot + file_name + path_to_hidden_file = os.path.abspath( + os.path.join(os.path.dirname(__file__), hidden_file_name) + ) + with open(path_to_hidden_file, mode) as hf: + if dump: + pickle.dump(obj, hf) + else: + return pickle.load(hf) + except (pickle.UnpicklingError, FileNotFoundError, EOFError): + raise click.ClickException("Uhm...At first, run init to get the nornir_object") + + +# load json or not json object +def _json_loads(ls): + l = [] + for i in ls: + try: + l.append(json.loads(i)) + except (json.JSONDecodeError, TypeError): + if i == "None": + i = None + l.append(i) + continue + return l + + +# validate ConnectionOptions +def _validate_connection_options(ctx, param, value): + if value is not None: + try: + value = { + connection_plugin: ConnectionOptions(**connection_params) + for connection_plugin, connection_params in _json_loads([value])[ + 0 + ].items() + } + return value + except (TypeError, AttributeError): + ctx.fail( + click.BadParameter( + f"'--connection_options' / '-co': {value}", + ).format_message(), + ) + + +def _get_cmd_folder(cmd_folders): + for cmd_folder in cmd_folders: + yield os.path.abspath(os.path.join(os.path.dirname(__file__), cmd_folder)) + + +# get list with filter objects +def _get_lists(s): + body = [] + s = s.split() + while True: + if not s: + break + begin = list(takewhile(lambda x: "=" in x, s)) + s = list(dropwhile(lambda x: "=" in x, s)) + begin.extend([i for i in takewhile(lambda x: "=" not in x, s)]) + body.append("".join(begin)) + s = list(dropwhile(lambda x: "=" not in x, s)) + return body + + +# docstring generator for Commands +def _doc_generator(s): + regex = ( + r".*kwargs: (?P.*)" + r"|.*task: (?P.*)" + r"|(?P.*Returns.*)" + r"|(?P^\S+:.*)" + r"|(?P.* – .*)" + ) + for line in s.split("\n"): + match = re.search(regex, line.lstrip()) + if match: + if match.lastgroup == "returns": + yield f" Returns: nornir result(optional), statistic, progress bar(optional)" + break + elif match.lastgroup == "task": + continue + elif match.lastgroup == "kwargs": + yield f"\n other options/arguments: {match.group(1)}" + elif match.lastgroup == "colon" or "dash": + yield f"\n{line}" + else: + yield line.strip() + + +# common options +CONNECTION_OPTIONS = [ + click.option( + "-co", + "--connection_options", + callback=_validate_connection_options, + help="Specify any connection parameters (json string)", + ), +] + +# common options +SHOW_INVENTORY_OPTIONS = [ + click.option( + "-i", + "--inventory", + type=click.Choice(["hosts", "groups", "defaults"]), + help="Show hosts, groups or defaults inventory", + ), + click.option("-h", "--hosts", is_flag=True, help="Show hosts list"), + click.option("-g", "--groups", is_flag=True, help="Show groups list"), + click.option( + "-cou", + "--count", + type=click.INT, + default=0, + help="Number of elements you want to show", + ), +] diff --git a/nornir_cli/custom_commands/__init__.py b/nornir_cli/custom_commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nornir_cli/nornir_cli.py b/nornir_cli/nornir_cli.py new file mode 100644 index 0000000..71e5843 --- /dev/null +++ b/nornir_cli/nornir_cli.py @@ -0,0 +1,269 @@ +import os +import click +import inspect +from itertools import chain +from nornir_cli.common_commands import _doc_generator +from nornir_cli import __version__ + + +CMD_FOLDERS = ["common_commands", "custom_commands"] + +PACKAGE_NAME = "nornir_cli" + +SETTINGS = { + "nornir_scrapli": { + "NetconfScrape": "scrapli_netconf.driver", + "NetworkDriver": "scrapli.driver", + "GenericDriver": "scrapli.driver", + }, + "nornir_netmiko": { + "BaseConnection": "netmiko", + "file_transfer": "netmiko", + }, +} + +PARAMETER_TYPES = { + str: click.STRING, + type(None): click.STRING, + int: click.INT, + float: click.FLOAT, + 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): + try: + for key, value in SETTINGS[plugin].items(): + m = lambda: None + m.sub = __import__(value, fromlist=[key]) + o = getattr(m.sub, key, None) + source_f = [f for f in o.__dict__.keys() if not f.startswith("_")] + command = sorted( + [i for i in source_f if l[l.find("_") + 1 : :] in i], key=len + ) + if command: + return getattr(o, command[0], None) + elif inspect.isfunction(o): + return o + except KeyError: + return + + +def _get_cmd_folder(cmd_folders): + for cmd_folder in cmd_folders: + yield os.path.abspath(os.path.join(os.path.dirname(__file__), cmd_folder)) + + +def _get_cli(path, cmd_name, cmd_folder): + if f"cmd_{cmd_name}.py" in os.listdir(cmd_folder): + obj = __import__(f"{path}.cmd_{cmd_name}", None, None, ["cli"]) + return obj.cli + + +# 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 list_commands(self, ctx): + ctx.obj[plugin] = __import__(plugin, fromlist=["tasks"]) + return [ + filename[4:-3] + for filename in os.listdir(next(_get_cmd_folder(["common_commands"]))) + if filename.endswith(".py") and filename.startswith("cmd_") + ] + list(ctx.obj[plugin].tasks.__all__) + + def list_custom_commands(self, ctx): + cmd_folders = _get_cmd_folder(CMD_FOLDERS) + return [ + filename[4:-3] + for filename in chain(*map(os.listdir, cmd_folders)) + if filename.endswith(".py") and filename.startswith("cmd_") + ] + + def get_command(self, ctx, cmd_name): + ctx.obj["kwargs"] = {} + try: + # init, filter, show_inventory, etc + command = _get_cli( + f"{PACKAGE_NAME}.common_commands", + cmd_name, + next(_get_cmd_folder(["common_commands"])), + ) + if command: + return command + + # nornir-plugin commands + plugin_command = _get_cli( + f"{PACKAGE_NAME}.plugin_commands", + "common", + next(_get_cmd_folder(["plugin_commands"])), + ) + + ctx.obj[plugin] = __import__(plugin, fromlist=["tasks"]) + ctx.obj["original"] = getattr(ctx.obj[plugin].tasks, cmd_name, None) + + # decorate the command and cover it with click.Options + return decorator(plugin, ctx)(plugin_command) + except (ImportError, AttributeError): + return + + def get_custom_command(self, ctx, cmd_name): + try: + for abs_path, rel_path in zip(_get_cmd_folder(CMD_FOLDERS), CMD_FOLDERS): + command = _get_cli(f"{PACKAGE_NAME}.{rel_path}", cmd_name, abs_path) + if command: + return command + except (ImportError, AttributeError): + return + + newclass = type( + name, + (BaseClass,), + { + "list_commands": list_commands if plugin else list_custom_commands, + "get_command": get_command if plugin else get_custom_command, + }, + ) + return newclass + + +# 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) + return wrapper + + +# +def decorator(plugin, ctx): + def wrapper(f): + # methods with a large and complex __doc__ :( + method_exceptions = ("send_interactive",) + + short_help = obj_or.__doc__.split("\n")[1].strip(", ., :") + + 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:") : :]) + ) + ) + + cmd = click.command(name=obj_or.__name__, short_help=short_help)(f) + + click.option( + "--pg_bar", + is_flag=True, + show_default=True, + )(cmd) + click.option( + "--show_result/--no_show_result", + default=True, + show_default=True, + )(cmd) + + # get original function from the main module + original_function = get_sources(plugin, obj_or.__name__) + sig0 = inspect.signature(obj_or) + p = dict(sig0.parameters) + if original_function: + sig = inspect.signature(original_function) + k = dict(sig.parameters) + p.update(k) + all_dict = { + key: value + 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 + click.option( + "--" + k, + 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 + 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 + obj_or = ctx.obj["original"] + + ctx.obj["queue_parameters"] = {} + + ctx.obj["queue_parameters"][obj_or] = {} + + return wrapper + + +@click.group() +@click.version_option(version=__version__) +@click.pass_context +def init_nornir_cli(ctx): + """ + Nornir CLI + + Orchestrate your Inventory and start Tasks and Runbooks + """ + + ctx.ensure_object(dict) + # list of dictionaries with original function (key) and set of arguments (value) + ctx.obj["queue_functions"] = [] + # last original functions with arguments + ctx.obj["queue_parameters"] = {} + + +@dec("nornir_netmiko") +def nornir_netmiko(): + """ + nornir_netmiko plugin + """ + pass + + +@dec("nornir_scrapli") +def nornir_scrapli(): + """ + nornir_scrapli plugin + """ + pass + + +@dec("nornir_napalm.plugins") +def nornir_napalm(): + """ + nornir_napalm plugin + """ + pass + + +@dec() +def custom(): + """ + custom nornir runbooks + """ + pass diff --git a/nornir_cli/plugin_commands/__init__.py b/nornir_cli/plugin_commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nornir_cli/plugin_commands/cmd_common.py b/nornir_cli/plugin_commands/cmd_common.py new file mode 100644 index 0000000..2407765 --- /dev/null +++ b/nornir_cli/plugin_commands/cmd_common.py @@ -0,0 +1,97 @@ +import click +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 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: + pg_bar.update() + + +@click.pass_context +def cli(ctx, pg_bar, show_result, *args, **kwargs): + ConnectionPluginRegister.auto_register() + + # 'None' = None + kwargs.update({k: v for k, v in zip(kwargs, _json_loads(kwargs.values()))}) + + current_func_params = next(ctx.obj["queue_functions_generator"]) + + # try to pass not all arguments, but only the necessary ones + if kwargs == list(current_func_params.values())[0]: + function, parameters = list(current_func_params.items())[0] + else: + param_iterator = iter(current_func_params.values()) + params = list(next(param_iterator).items()) + function, parameters = list(current_func_params)[0], { + key: value for key, value in kwargs.items() if (key, value) not in params + } + + # get the current Nornir object from Commands chain or from temp.pkl file + try: + nr = ctx.obj["nornir"] + except KeyError: + nr = _pickle_to_hidden_file("temp.pkl", mode="rb", dump=False) + + nr.config.logging.configure() + + # pg_bar option + if pg_bar: + with tqdm( + total=len(nr.inventory.hosts), + desc="processing: ", + ) as pb: + task = nr.run( + task=multiple_progress_bar, + method=function, + pg_bar=pb, + **parameters, + ) + print() + else: + task = nr.run( + task=function, + **parameters, + ) + + # show_result option + if show_result: + print_result(task, severity_level=logging.DEBUG) + 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() diff --git a/nornir_cli/transform/function.py b/nornir_cli/transform/function.py new file mode 100644 index 0000000..15d0d17 --- /dev/null +++ b/nornir_cli/transform/function.py @@ -0,0 +1,4 @@ +def adapt_host_data(host): + pass + #host.username="" + #host.password="" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3045a58 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1722 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "20.3.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.14.5" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "ciscoconfparse" +version = "1.5.30" +description = "Parse, Audit, Query, Build, and Modify Cisco IOS-style configurations" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = "*" +dnspython = "*" +passlib = "*" + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "cryptography" +version = "3.4.6" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "dill" +version = "0.3.3" +description = "serialize all of python" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*" + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "dnspython" +version = "2.1.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dnssec = ["cryptography (>=2.6)"] +doh = ["requests", "requests-toolbelt"] +idna = ["idna (>=2.1)"] +curio = ["curio (>=1.2)", "sniffio (>=1.1)"] +trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] + +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "genie" +version = "20.9" +description = "Genie: THE standard pyATS Library System" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +dill = "*" +"genie.libs.clean" = ">=20.9.0,<20.10.0" +"genie.libs.conf" = ">=20.9.0,<20.10.0" +"genie.libs.filetransferutils" = ">=20.9.0,<20.10.0" +"genie.libs.health" = ">=20.9.0,<20.10.0" +"genie.libs.ops" = ">=20.9.0,<20.10.0" +"genie.libs.parser" = ">=20.9.0,<20.10.0" +"genie.libs.sdk" = ">=20.9.0,<20.10.0" +jsonpickle = "*" +netaddr = "*" +PrettyTable = "*" +tqdm = "*" + +[package.extras] +dev = ["coverage", "restview", "sphinx", "sphinx-rtd-theme"] +full = ["genie.libs.conf", "genie.libs.clean", "genie.libs.health", "genie.libs.filetransferutils", "genie.libs.ops", "genie.libs.parser", "genie.libs.sdk", "pyats.robot (>=20.9.0,<20.10.0)", "genie.libs.robot (>=20.9.0,<20.10.0)", "genie.telemetry (>=20.9.0,<20.10.0)", "genie.trafficgen (>=20.9.0,<20.10.0)"] +robot = ["pyats.robot (>=20.9.0,<20.10.0)", "genie.libs.robot (>=20.9.0,<20.10.0)"] + +[[package]] +name = "genie.libs.clean" +version = "20.9.1" +description = "Genie Library for device clean support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +genie = "*" +pysnmp = "*" + +[package.extras] +dev = ["coverage", "paramiko", "restview", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-mockautodoc"] + +[[package]] +name = "genie.libs.conf" +version = "20.9" +description = "Genie libs Conf: Libraries to configures topology through Python object attributes" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +dev = ["coverage", "restview", "sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "genie.libs.filetransferutils" +version = "20.9" +description = "Genie libs FileTransferUtils: Genie FileTransferUtils Libraries" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +unicon = "*" + +[package.extras] +dev = ["coverage", "restview", "sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "genie.libs.health" +version = "20.9" +description = "pyATS Health Check for monitoring device health status" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +genie = "*" + +[package.extras] +dev = ["coverage", "paramiko", "restview", "sphinx", "sphinx-rtd-theme", "sphinxcontrib-mockautodoc"] + +[[package]] +name = "genie.libs.ops" +version = "20.9" +description = "Genie libs Ops: Libraries to retrieve operational state of the topology" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +dev = ["coverage", "restview", "sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "genie.libs.parser" +version = "20.9" +description = "Genie libs Parser: Genie Parser Libraries" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +xmltodict = "*" + +[package.extras] +dev = ["coverage", "restview", "sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "genie.libs.sdk" +version = "20.9.1" +description = "Genie libs sdk: Libraries containing all Triggers and Verifications" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +"ruamel.yaml" = "*" + +[package.extras] +dev = ["coverage", "restview", "sphinx", "sphinxcontrib-napoleon", "sphinx-rtd-theme", "xmltodict"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "jinja2" +version = "2.11.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +name = "jsonpickle" +version = "2.0.0" +description = "Python library for serializing any arbitrary object graph into JSON" +category = "main" +optional = false +python-versions = ">=2.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["coverage (<5)", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "ecdsa", "feedparser", "numpy", "pandas", "pymongo", "sklearn", "sqlalchemy", "enum34", "jsonlib"] +"testing.libs" = ["demjson", "simplejson", "ujson", "yajl"] + +[[package]] +name = "junos-eznc" +version = "2.5.4" +description = "Junos 'EZ' automation for non-programmers" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +jinja2 = ">=2.7.1" +lxml = ">=3.2.4" +ncclient = ">=0.6.3" +netaddr = "*" +paramiko = ">=1.15.2" +pyparsing = "*" +pyserial = "*" +PyYAML = ">=5.1" +scp = ">=0.7.0" +six = "*" +transitions = "*" +yamlordereddictloader = "*" + +[[package]] +name = "lxml" +version = "4.6.2" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "markupsafe" +version = "1.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" + +[[package]] +name = "more-itertools" +version = "8.7.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "napalm" +version = "3.2.0" +description = "Network Automation and Programmability Abstraction Layer with Multivendor support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cffi = ">=1.11.3" +ciscoconfparse = "*" +future = "*" +jinja2 = "*" +junos-eznc = ">=2.2.1" +lxml = ">=4.3.0" +netaddr = "*" +netmiko = ">=3.1.0" +paramiko = ">=2.6.0" +pyeapi = ">=0.8.2" +pyYAML = "*" +requests = ">=2.7.0" +scp = "*" +textfsm = "*" + +[[package]] +name = "ncclient" +version = "0.6.10" +description = "Python library for NETCONF clients" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +lxml = ">=3.3.0" +paramiko = ">=1.15.0" +six = "*" + +[[package]] +name = "netaddr" +version = "0.8.0" +description = "A network address manipulation library for Python" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "netmiko" +version = "3.3.3" +description = "Multi-vendor library to simplify Paramiko SSH connections to network devices" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ntc-templates = "*" +paramiko = ">=2.6.0" +pyserial = "*" +scp = ">=0.13.2" +tenacity = "*" + +[package.extras] +test = ["pyyaml (>=5.1.2)", "pytest (>=5.1.2)"] + +[[package]] +name = "nornir" +version = "3.1.0" +description = "Pluggable multi-threaded framework with inventory management to help operate collections of devices" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +mypy_extensions = ">=0.4.1,<0.5.0" +"ruamel.yaml" = ">=0.16,<0.17" +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-napalm" +version = "0.1.2" +description = "NAPALM's plugins for nornir" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +napalm = ">=3,<4" +nornir = ">=3,<4" + +[[package]] +name = "nornir-netbox" +version = "0.2.1" +description = "Netbox plugin for Nornir" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +nornir = ">=3,<4" +requests = ">=2.23.0,<3.0.0" + +[[package]] +name = "nornir-netmiko" +version = "0.1.1" +description = "Netmiko's plugins for nornir" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +netmiko = ">=3.1.0,<4.0.0" + +[[package]] +name = "nornir-scrapli" +version = "2021.1.30" +description = "scrapli Nornir plugin" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +nornir = ">=3.0.0,<4.0.0" +nornir-utils = ">=0.1.0" +ntc-templates = ">=1.1.0,<2.0.0" +scrapli = ">=2021.01.30" +scrapli-community = ">=2021.01.30" +scrapli-netconf = ">=2021.01.30" +textfsm = ">=1.1.0,<2.0.0" + +[package.extras] +genie = ["genie (>=20.2)", "pyats (>=20.2)"] + +[[package]] +name = "nornir-utils" +version = "0.1.2" +description = "Collection of plugins and functions for nornir that don't require external dependencies" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +colorama = ">=0.4.3,<0.5.0" +nornir = ">=3,<4" + +[[package]] +name = "ntc-templates" +version = "1.7.0" +description = "Package to return structured data from the output of network devices." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +textfsm = ">=1.1.0" + +[package.extras] +dev = ["pytest", "pyyaml", "black", "yamllint", "ruamel.yaml"] + +[[package]] +name = "packaging" +version = "20.9" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "paramiko" +version = "2.7.2" +description = "SSH2 protocol library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"] +totp = ["cryptography"] + +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "ply" +version = "3.11" +description = "Python Lex & Yacc" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "prettytable" +version = "2.1.0" +description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +wcwidth = "*" + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycryptodomex" +version = "3.10.1" +description = "Cryptographic library for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyeapi" +version = "0.8.4" +description = "Python Client for eAPI" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +netaddr = "*" + +[package.extras] +dev = ["check-manifest", "pep8", "pyflakes", "twine"] +test = ["coverage", "mock"] + +[[package]] +name = "pynacl" +version = "1.4.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +cp2110 = ["hidapi"] + +[[package]] +name = "pysmi" +version = "0.3.4" +description = "SNMP SMI/MIB Parser" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ply = "*" + +[[package]] +name = "pysnmp" +version = "4.4.12" +description = "SNMP library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pyasn1 = ">=0.2.3" +pycryptodomex = "*" +pysmi = "*" + +[[package]] +name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.extras] +checkqa-mypy = ["mypy (==v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pyyaml" +version = "5.4.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "regex" +version = "2020.11.13" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "ruamel.yaml" +version = "0.16.13" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel.yaml.clib" +version = "0.2.2" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "scp" +version = "0.13.3" +description = "scp module for paramiko" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +paramiko = "*" + +[[package]] +name = "scrapli" +version = "2021.1.30" +description = "Fast, flexible, sync/async, Python 3.6+ screen scraping client specifically for network devices" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +asyncssh = ["asyncssh (>=2.2.1,<3.0.0)"] +community = ["scrapli-community (>=2021.01.30a1)"] +full = ["textfsm (>=1.1.0,<2.0.0)", "ntc-templates (>=1.1.0,<2.0.0)", "ttp (>=0.5.0,<1.0.0)", "paramiko (>=2.6.0,<3.0.0)", "ssh2-python (>=0.23.0,<1.0.0)", "asyncssh (>=2.2.1,<3.0.0)", "scrapli-community (>=2021.01.30a1)", "genie (>=20.2)", "pyats (>=20.2)"] +genie = ["genie (>=20.2)", "pyats (>=20.2)"] +paramiko = ["paramiko (>=2.6.0,<3.0.0)"] +ssh2 = ["ssh2-python (>=0.23.0,<1.0.0)"] +textfsm = ["textfsm (>=1.1.0,<2.0.0)", "ntc-templates (>=1.1.0,<2.0.0)"] +ttp = ["ttp (>=0.5.0,<1.0.0)"] + +[[package]] +name = "scrapli-community" +version = "2021.1.30" +description = "" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +scrapli = ">=2021.01.30" + +[[package]] +name = "scrapli-netconf" +version = "2021.1.30" +description = "Netconf driver built on scrapli" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +lxml = ">=4.5.1,<5.0.0" +scrapli = ">=2021.01.30" + +[package.extras] +asyncssh = ["scrapli[asyncssh] (>=2021.01.30)"] +full = ["scrapli[paramiko] (>=2021.01.30)", "scrapli[ssh2] (>=2021.01.30)", "scrapli[asyncssh] (>=2021.01.30)"] +paramiko = ["scrapli[paramiko] (>=2021.01.30)"] +ssh2 = ["scrapli[ssh2] (>=2021.01.30)"] + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tenacity" +version = "7.0.0" +description = "Retry code until it succeeds" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "textfsm" +version = "1.1.0" +description = "Python module for parsing semi-structured text into python tables." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +future = "*" +six = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tqdm" +version = "4.59.0" +description = "Fast, Extensible Progress Meter" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +telegram = ["requests"] + +[[package]] +name = "transitions" +version = "0.8.7" +description = "A lightweight, object-oriented Python state machine implementation with many extensions." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[package.extras] +diagrams = ["pygraphviz"] +test = ["pytest"] + +[[package]] +name = "ttp" +version = "0.6.0" +description = "Template Text Parser" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "typed-ast" +version = "1.4.2" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "unicon" +version = "21.2" +description = "Unicon Connection Library" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +dill = "*" +pyyaml = "*" +"unicon.plugins" = ">=21.2.0,<21.3.0" + +[package.extras] +dev = ["cisco-distutils", "coverage", "restview", "sphinx", "sphinxcontrib-napoleon", "sphinxcontrib-mockautodoc", "sphinx-rtd-theme"] +pyats = ["pyats"] +robot = ["robotframework"] + +[[package]] +name = "unicon.plugins" +version = "21.2" +description = "Unicon Connection Library Plugins" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pyyaml = "*" +unicon = ">=21.2.0,<21.3.0" + +[package.extras] +dev = ["setuptools", "pip", "wheel", "coverage", "restview", "sphinx", "sphinxcontrib-napoleon", "sphinxcontrib-mockautodoc", "sphinx-rtd-theme"] + +[[package]] +name = "urllib3" +version = "1.26.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "xmltodict" +version = "0.12.0" +description = "Makes working with XML feel like you are working with JSON" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "yamlordereddictloader" +version = "0.4.0" +description = "YAML loader and dump for PyYAML allowing to keep keys order." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pyyaml = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "3c3d7a8f969ad56acd814d4d7b8808a296806259912fd559ebb7aa15182f99cd" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] +cffi = [ + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, + {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, + {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, + {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, + {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, + {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, + {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, + {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, + {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +ciscoconfparse = [ + {file = "ciscoconfparse-1.5.30-py3-none-any.whl", hash = "sha256:1b7b98c0b1b1cb05fadc9c27711d79d4faee9cf4770a72b5798d7167bb97ccc2"}, + {file = "ciscoconfparse-1.5.30.tar.gz", hash = "sha256:e3c9ad5ff7563c6d3b4542d2826f4b07feb0e46ea5c698886ab8c4d55cc28815"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +cryptography = [ + {file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"}, + {file = "cryptography-3.4.6-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964"}, + {file = "cryptography-3.4.6-cp36-abi3-win32.whl", hash = "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2"}, + {file = "cryptography-3.4.6-cp36-abi3-win_amd64.whl", hash = "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0"}, + {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b"}, + {file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df"}, + {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336"}, + {file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724"}, + {file = "cryptography-3.4.6.tar.gz", hash = "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87"}, +] +dill = [ + {file = "dill-0.3.3-py2.py3-none-any.whl", hash = "sha256:78370261be6ea49037ace8c17e0b7dd06d0393af6513cc23f9b222d9367ce389"}, + {file = "dill-0.3.3.zip", hash = "sha256:efb7f6cb65dba7087c1e111bb5390291ba3616741f96840bfc75792a1a9b5ded"}, +] +dnspython = [ + {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, + {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +genie = [ + {file = "genie-20.9-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:1232bf36926992bb1cccd3a0aaaa87615d4fc417da25a64011063e44b9750fc3"}, + {file = "genie-20.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7cf6d23d1725c0657c390089a63843100a24688ad13aa7a91023fa3ad7b78da7"}, + {file = "genie-20.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a8dbb824250284fdbe24ecf2de9c9818b577f9097d317655c4e603c8fb09bfd3"}, + {file = "genie-20.9-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:b6ba067bfdc052f546085c30ae924dc40e2a4980274edc695973875b87a48f08"}, + {file = "genie-20.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c3c2724e06fa3e866cdef6168b46a977d5453f2d7cca43f276684611ee014215"}, + {file = "genie-20.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:848fec1bafa91c80e44f26281167d65baff9019ae0d899fbc6c413990abd0b29"}, + {file = "genie-20.9-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:9e46d90882972aa8e736fdb4a0c2afe75948f817ac30d162549f7098a5a3ded0"}, + {file = "genie-20.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2eb259aafa5bb2c55de52b7a2fb59f355e93d3b75d641a02e43ad73556ec5b39"}, + {file = "genie-20.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4bc7ef827d6e27316c58aff2d3ca5edbc09ddf685d6da8efd14b403ec350958"}, + {file = "genie-20.9-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:393c344d8959e8d030e4cbbacfbf3154971f8681fca9efb1451ef68f38da870f"}, + {file = "genie-20.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4a63b8e1e0be69bc1d6191b01787f7ea0446d30a9e032b3ccb5952e54da072d8"}, + {file = "genie-20.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:134ca4bd8f0345cd7a6028c8eb795a113cccfb79b7ef3662c1612875cfe57f17"}, +] +"genie.libs.clean" = [ + {file = "genie.libs.clean-20.9.1-py3-none-any.whl", hash = "sha256:162ebbb891381bcaa280fdd2fbf7885f55fd74c71030b2c9a5e0d6957067e7d3"}, +] +"genie.libs.conf" = [ + {file = "genie.libs.conf-20.9-py3-none-any.whl", hash = "sha256:558de4afa0585f23e1613d3c6902e3d234d415df0eee156765cf1f82c925b51e"}, +] +"genie.libs.filetransferutils" = [ + {file = "genie.libs.filetransferutils-20.9-py3-none-any.whl", hash = "sha256:72360be7839a8d07aa05f0719ab52adb654c5a316514cec806f4af8d6ab38ecc"}, +] +"genie.libs.health" = [ + {file = "genie.libs.health-20.9-py3-none-any.whl", hash = "sha256:7829bec3316e73183682ba6f1504c143545c817338fb9ee8c10a88a2430195eb"}, +] +"genie.libs.ops" = [ + {file = "genie.libs.ops-20.9-py3-none-any.whl", hash = "sha256:ff114e0cc393d72bbb86ff9a10f46e642f15adc7e061a716b6be511fb35b7fb2"}, +] +"genie.libs.parser" = [ + {file = "genie.libs.parser-20.9-py3-none-any.whl", hash = "sha256:9af6d84bd6915e3935b34d842e99d8a061b63d740b952127f8ba47a9c9c25a8c"}, +] +"genie.libs.sdk" = [ + {file = "genie.libs.sdk-20.9.1-py3-none-any.whl", hash = "sha256:ebfe2211b8788c85937808f381c1b71a0cf80852210b26cee40287622ace2930"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +jinja2 = [ + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, +] +jsonpickle = [ + {file = "jsonpickle-2.0.0-py2.py3-none-any.whl", hash = "sha256:c1010994c1fbda87a48f8a56698605b598cb0fc6bb7e7927559fc1100e69aeac"}, + {file = "jsonpickle-2.0.0.tar.gz", hash = "sha256:0be49cba80ea6f87a168aa8168d717d00c6ca07ba83df3cec32d3b30bfe6fb9a"}, +] +junos-eznc = [ + {file = "junos-eznc-2.5.4.tar.gz", hash = "sha256:bf036d0af9ee5c5e4f517cb5fc902fe891fa120e18f459805862c53d4a97193a"}, + {file = "junos_eznc-2.5.4-py2.py3-none-any.whl", hash = "sha256:e05c36d56d8b8d13b1fb3bb763828bb3ee80fa1dcadc3a6762e8e2568504676d"}, +] +lxml = [ + {file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"}, + {file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"}, + {file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"}, + {file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"}, + {file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"}, + {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"}, + {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"}, + {file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"}, + {file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"}, + {file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"}, + {file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"}, + {file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"}, + {file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"}, + {file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"}, + {file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"}, + {file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"}, + {file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"}, + {file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"}, + {file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"}, + {file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"}, + {file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"}, + {file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"}, + {file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"}, + {file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"}, + {file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"}, + {file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"}, + {file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"}, + {file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"}, + {file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"}, + {file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"}, + {file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"}, + {file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"}, + {file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"}, + {file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a"}, + {file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75"}, + {file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"}, + {file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +more-itertools = [ + {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, + {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +napalm = [ + {file = "napalm-3.2.0-py2.py3-none-any.whl", hash = "sha256:6ab07a8a360150198b3c33c2fa4e92a9658cda9821736d24f2399343425d2af3"}, + {file = "napalm-3.2.0.tar.gz", hash = "sha256:6e69a505f5d4a678c42f048d13153afabf10b7fec72868e3bfb53ff4089206fc"}, +] +ncclient = [ + {file = "ncclient-0.6.10.tar.gz", hash = "sha256:67b1eba5a6c7c6075746d8c33d4e8f4ded17604034c1fcd1c78996ef52bf66ff"}, +] +netaddr = [ + {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"}, + {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"}, +] +netmiko = [ + {file = "netmiko-3.3.3-py3-none-any.whl", hash = "sha256:be165b81930aa779a2a34f4437af9f9807553030d2e3ef8da002314fb6b1366d"}, + {file = "netmiko-3.3.3.tar.gz", hash = "sha256:ead6ea70b46307cce03d121d4da96fd862f76619474d6e962d1019560bb4c5a1"}, +] +nornir = [ + {file = "nornir-3.1.0-py3-none-any.whl", hash = "sha256:0eed0ca73122ce8f4b90a560ada79d81174845a8a5320972292417eb19efa587"}, + {file = "nornir-3.1.0.tar.gz", hash = "sha256:f96b0e2b8983045eef5345de5a6e61198ed2658a0e42049e4bd9a127b2bae034"}, +] +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"}, +] +nornir-netbox = [ + {file = "nornir_netbox-0.2.1-py3-none-any.whl", hash = "sha256:b635afa6ffd5871d220338848f44b1ac3c6cdeab57f41798ca85f731d257e81a"}, + {file = "nornir_netbox-0.2.1.tar.gz", hash = "sha256:53e1c15b66fc5c91c0a698e9fa95c08c76e60dc8a9389c3142a3ac035dbbd02c"}, +] +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-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"}, +] +nornir-utils = [ + {file = "nornir_utils-0.1.2-py3-none-any.whl", hash = "sha256:98915e781d4135e424fe1d4617dc231ada9a7c732c8e51e408f53fd3d37a13b0"}, + {file = "nornir_utils-0.1.2.tar.gz", hash = "sha256:23ae95c4805b0ce8a5ed32935f3f86027e5701175e7740ab8b3a79946c5d90b2"}, +] +ntc-templates = [ + {file = "ntc_templates-1.7.0-py3-none-any.whl", hash = "sha256:08ffeafd02a1d3e42f4f12f910f0b4c64b93783d76ece186c9a63389403a450b"}, + {file = "ntc_templates-1.7.0.tar.gz", hash = "sha256:4e098f83b35624751f4f6aaa4eed9eefd50a742f3d009c86f4bb22843a6a72fc"}, +] +packaging = [ + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, +] +paramiko = [ + {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, + {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, +] +passlib = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +ply = [ + {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, + {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, +] +prettytable = [ + {file = "prettytable-2.1.0-py3-none-any.whl", hash = "sha256:bb5abc72bdfae6f3cdadb04fb7726f6915af0ddb7c897a41d4ad7736d9bfd8fd"}, + {file = "prettytable-2.1.0.tar.gz", hash = "sha256:5882ed9092b391bb8f6e91f59bcdbd748924ff556bb7c634089d5519be87baa0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pycryptodomex = [ + {file = "pycryptodomex-3.10.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:4344ab16faf6c2d9df2b6772995623698fb2d5f114dace4ab2ff335550cf71d5"}, + {file = "pycryptodomex-3.10.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34"}, + {file = "pycryptodomex-3.10.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0bd35af6a18b724c689e56f2dbbdd8e409288be71952d271ba3d9614b31d188c"}, + {file = "pycryptodomex-3.10.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ec9901d19cadb80d9235ee41cc58983f18660314a0eb3fc7b11b0522ac3b6c4a"}, + {file = "pycryptodomex-3.10.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c2b680987f418858e89dbb4f09c8c919ece62811780a27051ace72b2f69fb1be"}, + {file = "pycryptodomex-3.10.1-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:a6584ae58001d17bb4dc0faa8a426919c2c028ef4d90ceb4191802ca6edb8204"}, + {file = "pycryptodomex-3.10.1-cp27-cp27m-win32.whl", hash = "sha256:4195604f75cdc1db9bccdb9e44d783add3c817319c30aaff011670c9ed167690"}, + {file = "pycryptodomex-3.10.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9f713ffb4e27b5575bd917c70bbc3f7b348241a351015dbbc514c01b7061ff7e"}, + {file = "pycryptodomex-3.10.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:418f51c61eab52d9920f4ef468d22c89dab1be5ac796f71cf3802f6a6e667df0"}, + {file = "pycryptodomex-3.10.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8a98e02cbf8f624add45deff444539bf26345b479fc04fa0937b23cd84078d91"}, + {file = "pycryptodomex-3.10.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:dbd2c361db939a4252589baa94da4404d45e3fc70da1a31e541644cdf354336e"}, + {file = "pycryptodomex-3.10.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:564063e3782474c92cbb333effd06e6eb718471783c6e67f28c63f0fc3ac7b23"}, + {file = "pycryptodomex-3.10.1-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:e4a1245e7b846e88ba63e7543483bda61b9acbaee61eadbead5a1ce479d94740"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b8eb85b3cc7f083d87978c264d10ff9de3b4bfc46f1c6fdc2792e7d7ebc87bb"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-manylinux1_i686.whl", hash = "sha256:f3bb267df679f70a9f40f17d62d22fe12e8b75e490f41807e7560de4d3e6bf9f"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:04265a7a84ae002001249bd1de2823bcf46832bd4b58f6965567cb8a07cf4f00"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:72f44b5be46faef2a1bf2a85902511b31f4dd7b01ce0c3978e92edb2cc812a82"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:e090a8609e2095aa86978559b140cf8968af99ee54b8791b29ff804838f29f10"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:20c45a30f3389148f94edb77f3b216c677a277942f62a2b81a1cc0b6b2dde7fc"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-win32.whl", hash = "sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38"}, + {file = "pycryptodomex-3.10.1-cp35-abi3-win_amd64.whl", hash = "sha256:3dfce70c4e425607ae87b8eae67c9c7dbba59a33b62d70f79417aef0bc5c735b"}, + {file = "pycryptodomex-3.10.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:940db96449d7b2ebb2c7bf190be1514f3d67914bd37e54e8d30a182bd375a1a9"}, + {file = "pycryptodomex-3.10.1-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:d8fae5ba3d34c868ae43614e0bd6fb61114b2687ac3255798791ce075d95aece"}, + {file = "pycryptodomex-3.10.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:f2abeb4c4ce7584912f4d637b2c57f23720d35dd2892bfeb1b2c84b6fb7a8c88"}, + {file = "pycryptodomex-3.10.1-pp27-pypy_73-win32.whl", hash = "sha256:36dab7f506948056ceba2d57c1ade74e898401960de697cefc02f3519bd26c1b"}, + {file = "pycryptodomex-3.10.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37ec1b407ec032c7a0c1fdd2da12813f560bad38ae61ad9c7ce3c0573b3e5e30"}, + {file = "pycryptodomex-3.10.1-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:00a584ee52bf5e27d540129ca9bf7c4a7e7447f24ff4a220faa1304ad0c09bcd"}, + {file = "pycryptodomex-3.10.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:961333e7ee896651f02d4692242aa36b787b8e8e0baa2256717b2b9d55ae0a3c"}, + {file = "pycryptodomex-3.10.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:2959304d1ce31ab303d9fb5db2b294814278b35154d9b30bf7facc52d6088d0a"}, + {file = "pycryptodomex-3.10.1.tar.gz", hash = "sha256:541cd3e3e252fb19a7b48f420b798b53483302b7fe4d9954c947605d0a263d62"}, +] +pyeapi = [ + {file = "pyeapi-0.8.4.tar.gz", hash = "sha256:c33ad1eadd8ebac75f63488df9412081ce0b024c9e1da12a37196a5c60427c54"}, +] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyserial = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] +pysmi = [ + {file = "pysmi-0.3.4-py2.4.egg", hash = "sha256:13d937620529468d34d525089abbd569f3b34dd808085d34586e63fbbda40fdb"}, + {file = "pysmi-0.3.4-py2.5.egg", hash = "sha256:887f34233d284a3298f67e2e01db0af8817f358501c724f02175ae53a1f4f106"}, + {file = "pysmi-0.3.4-py2.6.egg", hash = "sha256:d6688b6121b71f18ca19040bfd27feca5f7a502598de953ec09a8e3f06c36a4e"}, + {file = "pysmi-0.3.4-py2.7.egg", hash = "sha256:abbd70efb8cf24d6c0bc782c996cd38f0c4905714edf8d03acbaa8ccd9c74eb7"}, + {file = "pysmi-0.3.4-py2.py3-none-any.whl", hash = "sha256:2ec6ebd41aaef562695e7d0058763c6e1e8c1fbf8710804c11ef3a857fc9cad7"}, + {file = "pysmi-0.3.4-py3.1.egg", hash = "sha256:ed44154e5d247edfad09bbe24a398cec54b6c393df2fa5103cf59ccf9278855a"}, + {file = "pysmi-0.3.4-py3.2.egg", hash = "sha256:9d989714a8ad40253b1adbb410067c339b4c77ecd004daa071055e200fe9a798"}, + {file = "pysmi-0.3.4-py3.3.egg", hash = "sha256:9fb837bb5662b11cce8d44e2dfff8604b97276ff9614aa74d5a33d1cea96d627"}, + {file = "pysmi-0.3.4-py3.4.egg", hash = "sha256:6e8524b80c6641bb9d81319e54bb0b75f47543e7d3bc7e8f12dcda960d7359c8"}, + {file = "pysmi-0.3.4-py3.5.egg", hash = "sha256:e4dc8155ccf2b7eaf96aef4b31f7558ce1cf41b3bb27d0cbe341b3c157531088"}, + {file = "pysmi-0.3.4-py3.6.egg", hash = "sha256:817c572dc8262a4b2ccff34b27e42ee55e48f74be42d22308b637aa236ce1785"}, + {file = "pysmi-0.3.4-py3.7.egg", hash = "sha256:7203220305bab6a20e2811fca927031440d3b1f9a4a1a5a2a12c033b911dc594"}, + {file = "pysmi-0.3.4.tar.gz", hash = "sha256:bd15a15020aee8376cab5be264c26330824a8b8164ed0195bd402dd59e4e8f7c"}, +] +pysnmp = [ + {file = "pysnmp-4.4.12-py2.4.egg", hash = "sha256:5459b70e849eb4d14bd4c8e475fc67bfa4cd6676fc53aa2f307194319ce77602"}, + {file = "pysnmp-4.4.12-py2.5.egg", hash = "sha256:13177be8030bbc751d94b1f844cb8d8c2d62844dfd86383576e20f09864427b2"}, + {file = "pysnmp-4.4.12-py2.6.egg", hash = "sha256:c8d8a403e45a752daefed4635044b137c4390c68ffef54307e2390d8ac9e62e4"}, + {file = "pysnmp-4.4.12-py2.7.egg", hash = "sha256:3fa5f96a47986658564fa5c4a75b9ea617397729028f2a40b88972139a3e67f7"}, + {file = "pysnmp-4.4.12-py2.py3-none-any.whl", hash = "sha256:d90946c5d7c55b1ddc03e065a99a2ed36e748dcf166ca04ee4126b8f25fc057a"}, + {file = "pysnmp-4.4.12-py3.1.egg", hash = "sha256:8019dba728fbb5216645e1c1bd7bccaf25e938243d5dc7e28dbcc6ac029ff8b9"}, + {file = "pysnmp-4.4.12-py3.2.egg", hash = "sha256:e41b7dbd7e54030dd99a0780728bac3335f4d95c3f28f9b8df827f6075b8ccec"}, + {file = "pysnmp-4.4.12-py3.3.egg", hash = "sha256:ce2f260a41a107f08264a53c74940e0ad8ae7e35d9e10f33746716ac5c028592"}, + {file = "pysnmp-4.4.12-py3.4.egg", hash = "sha256:5e48994f48c5800022fff5d380976d88fcd27c17f1635ea4ef1d52796b1acd08"}, + {file = "pysnmp-4.4.12-py3.5.egg", hash = "sha256:578bb1dab0e67dfc583a61f721a9f6378c51b576ed6a90b229aefd60a9f965f6"}, + {file = "pysnmp-4.4.12-py3.6.egg", hash = "sha256:89ef66e511a0f770ec9bc7df8bebe6bebb3a3dc3b7394b7d408e42f92f74458b"}, + {file = "pysnmp-4.4.12-py3.7.egg", hash = "sha256:396b80187e6175481982b83f87d2de28c7068937d6772b5e2679cb56853bf7d7"}, + {file = "pysnmp-4.4.12.tar.gz", hash = "sha256:0c3dbef2f958caca96071fe5c19de43e9c1b0484ab02a0cf08b190bcee768ba9"}, +] +pytest = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] +pyyaml = [ + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, +] +regex = [ + {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, + {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, + {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, + {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, + {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, + {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, + {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, + {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, + {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, + {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, + {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, + {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, + {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +"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"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win32.whl", hash = "sha256:30dca9bbcbb1cc858717438218d11eafb78666759e5094dd767468c0d577a7e7"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win_amd64.whl", hash = "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1236df55e0f73cd138c0eca074ee086136c3f16a97c2ac719032c050f7e0622f"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win32.whl", hash = "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2fd336a5c6415c82e2deb40d08c222087febe0aebe520f4d21910629018ab0f3"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:75f0ee6839532e52a3a53f80ce64925ed4aed697dd3fa890c4c918f3304bd4f4"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win32.whl", hash = "sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8be05be57dc5c7b4a0b24edcaa2f7275866d9c907725226cdde46da09367d923"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win32.whl", hash = "sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1f8c0a4577c0e6c99d208de5c4d3fd8aceed9574bb154d7a2b21c16bb924154c"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win32.whl", hash = "sha256:46d6d20815064e8bb023ea8628cfb7402c0f0e83de2c2227a88097e239a7dffd"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6c0a5dc52fc74eb87c67374a4e554d4761fd42a4d01390b7e868b30d21f4b8bb"}, + {file = "ruamel.yaml.clib-0.2.2.tar.gz", hash = "sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7"}, +] +scp = [ + {file = "scp-0.13.3-py2.py3-none-any.whl", hash = "sha256:f2fa9fb269ead0f09b4e2ceb47621beb7000c135f272f6b70d3d9d29928d7bf0"}, + {file = "scp-0.13.3.tar.gz", hash = "sha256:8bd748293d7362073169b96ce4b8c4f93bcc62cfc5f7e1d949e01e406a025bd4"}, +] +scrapli = [ + {file = "scrapli-2021.1.30-py3-none-any.whl", hash = "sha256:31a35daa75212953efb8cf7d7ff582f93aae12d2b957056c9ec185d4f6f5e586"}, + {file = "scrapli-2021.1.30.tar.gz", hash = "sha256:aac7e8ae764f098a77d8d14fa4bda1cd886318b7293507e56a05f007d3e2e6c4"}, +] +scrapli-community = [ + {file = "scrapli_community-2021.1.30-py3-none-any.whl", hash = "sha256:df5493203ad4b58147f4c685af8886fa1db2a095140ab7ab9d93ab05d87d48bf"}, + {file = "scrapli_community-2021.1.30.tar.gz", hash = "sha256:1b473ed0ac356f07ab33339013b63efe00af0d246bb7be356f0d6cb2989e7f77"}, +] +scrapli-netconf = [ + {file = "scrapli_netconf-2021.1.30-py3-none-any.whl", hash = "sha256:5ed9867e180cca35d2f8a769bcbf9fae6bde11e56b29a62e9cda1af4c487d099"}, + {file = "scrapli_netconf-2021.1.30.tar.gz", hash = "sha256:45c32ebf06c588571c9cfc33c15b3e23c2293d01e88e99b0e5129817ecd7ea50"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +tenacity = [ + {file = "tenacity-7.0.0-py2.py3-none-any.whl", hash = "sha256:a0ce48587271515db7d3a5e700df9ae69cce98c4b57c23a4886da15243603dd8"}, + {file = "tenacity-7.0.0.tar.gz", hash = "sha256:5bd16ef5d3b985647fe28dfa6f695d343aa26479a04e8792b9d3c8f49e361ae1"}, +] +textfsm = [ + {file = "textfsm-1.1.0-py2.py3-none-any.whl", hash = "sha256:0aef3f9cad3d03905915fd62bff358c42b7dc35c863ff2cb0b5324c2b746cc24"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tqdm = [ + {file = "tqdm-4.59.0-py2.py3-none-any.whl", hash = "sha256:9fdf349068d047d4cfbe24862c425883af1db29bcddf4b0eeb2524f6fbdb23c7"}, + {file = "tqdm-4.59.0.tar.gz", hash = "sha256:d666ae29164da3e517fcf125e41d4fe96e5bb375cd87ff9763f6b38b5592fe33"}, +] +transitions = [ + {file = "transitions-0.8.7-py2.py3-none-any.whl", hash = "sha256:626229df01ea91dc0b28491bf5af7cc0aba6c2aa7289658e55ea77288258fa49"}, + {file = "transitions-0.8.7.tar.gz", hash = "sha256:8c60ec0828cd037820726283cad5d4d77a5e31514e058b51250420e9873e9bc7"}, +] +ttp = [ + {file = "ttp-0.6.0.tar.gz", hash = "sha256:727308fa93d1a0fc60aa1474a22fd39df323a03379ab6c33ef451d8f9075c877"}, +] +typed-ast = [ + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, + {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, + {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, + {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, + {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, + {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, + {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, + {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, + {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, + {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +unicon = [ + {file = "unicon-21.2-cp35-cp35m-macosx_10_10_x86_64.whl", hash = "sha256:c1ccf64ee39bf22a34589d3973624fcfd0f56ec09285219532891bb94eeeca2d"}, + {file = "unicon-21.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:474105083de02705d8be2e8ff6ab2faa24ed1fff1ab13967a27ee90f94172b52"}, + {file = "unicon-21.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d4f7961d7d3d4fd16745c7f84be9e1ed65c162298894604aaf06b44a865d90d"}, + {file = "unicon-21.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:3bcc5d2d6d929ffeb5a0bbaabc91723c830507feaf02925cb9503cb86f7d517b"}, + {file = "unicon-21.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:64616d5fbceea35e42cced3954246a0ca55562e6ce16370c89dbb6aba39992e1"}, + {file = "unicon-21.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:00feaaa11aa7975ab7109b4a96772cca4cadf4a5fc3b16f335fca6a4d579b36b"}, + {file = "unicon-21.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0e40220190b513c78a7347a8eed63a9ecafe8d15e0a23e60dbcd5e107fc00321"}, + {file = "unicon-21.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ede6bd79094454931ce0908f054b10453dddc6a1a7c0d4ad1f110d5841482c25"}, + {file = "unicon-21.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c123d461ffdc4c923091058f04d28e578026c76cbb032904cb744423476fec8b"}, + {file = "unicon-21.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:e0873be485787850d85c6d282604831b86bf052a5fad231353f188136df84bc0"}, + {file = "unicon-21.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a9b6a7ebc1c11ef3aa119901ef2ad2e88663258a33bf9bde744d0b332550eac8"}, + {file = "unicon-21.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4a2ff17eabbed0925a799aa1eefe84730c020f8d84217bfaed8ca8c053faedb0"}, +] +"unicon.plugins" = [ + {file = "unicon.plugins-21.2-py3-none-any.whl", hash = "sha256:eeafaaa069e99e132de35c29272a7c2cbc469d0ef9c04401fb63b06698678cbc"}, +] +urllib3 = [ + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +xmltodict = [ + {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"}, + {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, +] +yamlordereddictloader = [ + {file = "yamlordereddictloader-0.4.0.tar.gz", hash = "sha256:7f30f0b99ea3f877f7cb340c570921fa9d639b7f69cba18be051e27f8de2080e"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..470db31 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[tool.poetry] +name = "nornir_cli" +version = "0.1.0" +description = "Nornir CLI" +license = "MIT" +readme = "README.md" +keywords = ["nornir", "click", "nornir cli", "nornir-plugin"] +repository = "https://github.com/timeforplanb123/nornir_cli" +documentation = "https://timeforplanb123.github.io/nornir_cli/" +authors = ["Pavel Shemetov "] +classifiers = [ + "Programming Language :: Python :: 3.8", +] + +[tool.poetry.dependencies] +python = "^3.8" +click = "^7.1.2" +nornir = "3.1.0" +nornir-utils = "0.1.2" +nornir-netmiko = "0.1.1" +netmiko = "3.3.3" +nornir-scrapli = "2021.01.30" +scrapli = "2021.01.30" +scrapli-netconf = "2021.01.30" +scrapli-community = "2021.01.30" +nornir-napalm = "0.1.2" +ttp = "^0.6.0" +# initially, nornir_cli was created to work with NetBox inventory ^_- +nornir-netbox = "^0.2.0" +genie = "20.9" + +[tool.poetry.dev-dependencies] +pytest = "^5.2" +black = "^20.8b1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +nornir_cli = 'nornir_cli.nornir_cli:init_nornir_cli' diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_nornir_cli.py b/tests/test_nornir_cli.py new file mode 100644 index 0000000..a9a40b0 --- /dev/null +++ b/tests/test_nornir_cli.py @@ -0,0 +1,5 @@ +from nornir_cli import __version__ + + +def test_version(): + assert __version__ == '0.1.0'