Useful
Click Multi Commands feature#
All commands are loaded 'lazily' from different plugins (Click Multi Commands + Click Multi Commands example). 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,print_result,write_result,write_results,write_file - 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 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 nornir runbook#
You can add a collection of your custom Nornir runbooks in nornir_cli and run them for any Hosts, managing the Inventory using nornir_cli, directly from the CLI.
All custom Nornir runbooks stored in custom_commands directory (see Click Multi Commands feature). To create custom groups and add your Nornir runbook to these groups you need to:
-
take and wrap your runbook in a wrapper and run it inside that wrapper
from nornir_cli.common_commands import custom @custom def cli(ctx): """ runbook description """ def nornir_runbook(task): # code # ... task = ctx.nornir.run(task=nornir_runbook)cliis a newclick.commandctxafter decorating with@customis anCustomContextclass object. It has 2 attributes, by default: -ctx.nornir- currentnornir.core.Nornir. This is a mutable object, but it can containnornir.core.Nornirclass object only -ctx.result- currentnornir.core.task.Resultobject. This is a mutable object. It works with built-in commands, such aprint_result,write_result,write_resultsAt any time, you can create a new attribute (for example,
ctx.new_attr_0,ctx.new_attr_1, etc.) and save any data/python object to it. The state ofctxobject is saved and you can pass it between your custom Nornir runbooks. This is very useful and allows you to divide a large task into several small ones and run them in the required order from thenornir_cliinterfaceAn important point is that only
ctx.nornirobject errors are processed, other errors are not processed to make code debugging easier -
name a file with your runbook as
cmd_something.py(replacesomethingon your own).somethingbetweencmd_and.pywill be command name -
create directory tree in
custom_commandsdirecotry and put your nornir runbooks there. Here the directories are newnornir_cligroups, and nornir runbooks are new commands. Easy-peasy.For example, i created two directories,
dhcpandmpls, and put my runbooks there. Let's checknornir_cli:$ tree ~/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/ /home/user/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/ ├── dhcp │ ├── cmd_dhcp_snooping.py │ └── templates │ ├── dhcp_snooping.j2 │ └── disp_int.template ├── __init__.py └── mpls └── cmd_ldp_config.py 3 directories, 5 files$ nornir_cli Usage: nornir_cli [OPTIONS] COMMAND [ARGS]... Nornir CLI Orchestrate your Inventory and start Tasks and Runbooks Options: --version Show the version and exit. --help Show this message and exit. Commands: dhcp mpls nornir-f5 nornir_f5 plugin nornir-http nornir_http plugin nornir-jinja2 nornir_jinja2 plugin nornir-napalm nornir_napalm plugin nornir-netconf nornir_netconf plugin nornir-netmiko nornir_netmiko plugin nornir-paramiko nornir_paramiko plugin nornir-pyez nornir_pyez plugin nornir-pyxl nornir_pyxl plugin nornir-scrapli nornir_scrapli plugin$ nornir_cli dhcp Usage: nornir_cli dhcp [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... Options: --help Show this message and exit. Commands: change_credentials Change username and password dhcp_snooping Configure dhcp snooping filter Do simple or advanced filtering init Initialize a Nornir print_result print_result from nornir_utils show_inventory Show current inventory write_file Write_file, but not from nornir_utils write_result Write `Result` object to file write_results Write `Result` object to files$ nornir_cli mpls Usage: nornir_cli mpls [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]... Options: --help Show this message and exit. Commands: change_credentials Change username and password filter Do simple or advanced filtering init Initialize a Nornir print_result print_result from nornir_utils show_inventory Show current inventory write_file Write_file, but not from nornir_utils write_result Write `Result` object to file write_results Write `Result` object to files ldp_config Configure ldp
Runbook collections features:
- empty directories are not displayed as
nornir_cligroups - the commands are displayed only in the latest directories in the directory tree. This is based on the
command chainsability ofnornir_cliand on the fact that it's impossible to buildcommand chainsin parent and child groups. That is a fair constraint related with Click Multi Commands and Multi Commands Chaining -
you may have noticed, that in the example above, there is a
templatesdirectory in thedhcpdirectory and it was not displayed.templatesand__pycache__are included in thecustom_exceptionslist innornir_cli.py. But you can use these names for the parent directories.If you want to add your exceptions without fixing
custom_exceptionslist, use theNORNIR_CLI_GRP_EXCEPTIONSenvironment variable:# as instance, to add temp and tmp groups to custom_exceptions list $ export NORNIR_CLI_GRP_EXCEPTIONS=temp,tmp -
if to add runbooks to
custom_commandswithoutrunbook collection, they will be incustomgroup - all python modules, used in your runbook, must be installed in the virtual environment, otherwise the runbook will not be displayed as command in
nornir_cli
And let's look at an simple example:
$ tree ~/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/
/home/user/virtenvs/py3.8.4/lib/python3.8/site-packages/nornir_cli/custom_commands/
├── cmd_first_command.py
├── cmd_second_command.py
└── __init__.py
1 directories, 3 files
# InitNornir with NetBox Inventory
$ nornir_cli custom init -u username -p password -c None -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}'
# filter dev_1
$ nornir_cli custom filter --hosts -s name=dev_1
[
"dev_1"
]
from nornir.core.plugins.connections import ConnectionPluginRegister
from nornir_cli.common_commands import custom
from nornir_netmiko import netmiko_send_command
@custom
def cli(ctx):
ConnectionPluginRegister.auto_register()
res = ctx.nornir.run(
task=netmiko_send_command, name="display clock", command_string="disp clock"
)
# we will use print_result command for res
ctx.result = res
# create new attributes
ctx.simple_object = "simple object"
ctx.complex_object = "complex object"
from nornir.core.plugins.connections import ConnectionPluginRegister
from nornir_cli.common_commands import custom
from nornir_netmiko import netmiko_send_command
@custom
def cli(ctx):
ConnectionPluginRegister.auto_register()
res = ctx.nornir.run(
task=netmiko_send_command, name="display interface brief", command_string="disp int br"
)
# we will use print_result command for res
ctx.result = res
# print attributes from first_command
print()
print("ctx.simple_object from first_command:", ctx.simple_object, end=f"\n{'-' * 10}\n")
print("ctx.complex_object from first_command:", ctx.complex_object)
print()
$ nornir_cli custom first_command print_result second_command print_result
display clock*******************************************************************
* dev_1 ** changed : False *****************************************************
vvvv display clock ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
2021-10-04 11:55:46
Monday
Time Zone(Moscow) : UTC+03:00
^^^^ END display clock ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ctx.simple_object from first_command: simple object
----------
ctx.complex_object from first_command: complex object
display interface brief*********************************************************
* dev_1 ** changed : False *****************************************************
vvvv display interface brief ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
...
InUti/OutUti: input utility/output utility
Interface PHY Protocol InUti OutUti inErrors outErrors
Ethernet0/0/1 up up 0.01% 0.18% 0 0
...
^^^^ END display interface brief ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Click Complex Applications#
About Click Complex Application
An important role in nornir_cli is given to the Context object.
As instance, if we run that command:
$ 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]]}'
{
'queue_functions':
[
{
<function send_interactive at 0x7f01839ba280>:
{
'interact_events': <class 'inspect._empty'>,
'failed_when_contains': None,
'privilege_level': '',
'timeout_ops': None
}
}
],
'queue_parameters':
{
<function send_interactive at 0x7f01839ba280>:
{
'interact_events': <class 'inspect._empty'>,
'failed_when_contains': None,
'privilege_level': '', 'timeout_ops': None
}
},
'nornir_scrapli': <module 'nornir_scrapli' from '/home/user/virtenvs/3.8.4/lib/python3.8/site-packages/nornir_scrapli/__init__.py'>,
'original': <function send_interactive at 0x7f01839ba280>,
'queue_functions_generator': <generator object decorator.<locals>.wrapper.<locals>.<genexpr> at 0x7f0182fafba0>
# required_options for send_command
'required_options': ['command']}
# required_options for send_interactive
'required_options': ['interact_events']}
}
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 expressionctx.obj["required_options"]- list with required options for last plugin command
If we run custom runbok (as instance, it's called dhcp_snooping from example above), then most likely we use a @custom decorator:
$ nornir_cli custom dhcp_snooping
.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:
# 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"
# 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"
# 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"}'
# 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?
# 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:
export NORNIR_CLI_USERNAME=username
export NORNIR_CLI_PASSWORD=password
And with NORNIR_CLI_GRP_EXCEPTIONS environment variable you can exclude directoiries from being displayed in Runbook collections (see here)
Or you can permanently declare environment variables using .bash_profile file:
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
What else can nornir_cli be useful#
Useful functions#
-
print_statuse
print_statto add statistic to your nornir runbookThefrom nornir_cli.common_commands import print_stat # code # ... print_stat(nr, result) # where is :nr: is nornir.core.Nornir object # :result: is nornir.core.task.Result objectprint_statfunction show statistic in the following format:dev_1 : ok=1 changed=0 failed=0 dev_2 : ok=1 changed=0 failed=0 dev_3 : ok=1 changed=0 failed=0 OK : 3 CHANGED : 0 FAILED : 0 -
print_resultIt is the same function as
print_resultfromnornir_utils, but with count parameter.count- number of sorted results. It's acceptable to use numbers with minus sign (-5 as example), then results will be from the end of results list. Withcountparameter you can output firstnresults or latestnresults.from nornir_cli.common_commands import print_result # code # ... print_result(result, vars=["result", "diff"], count=-10) # :result: is nornir.core.task.Result object -
write_resultResult can be written to a file using
write_resultfunction for all hosts from current Inventory.By default,
write_resulttries to create a file in the current directory or in the specified directory, if file doesn't exist.write_resultfunction has many parameters. For example, you can exclude errors from a file, write "diff" to another file or output it, use different write modes, limit entries number, etc.from nornir_cli.common_commands import write_result # code # ... write_result(result, filename="result.txt", vars=["result", "diff"], count=-10, no_errors=True) # :result: is nornir.core.task.Result object -
write_resultsResult can be written to a files with hostnames as filenames using
write_resultsfunction for all hosts from current Inventory. For example, it is usefull for diagnostic commands, that run manyshow somethingordisplay somethingcommands.By default,
write_resultstries to create a specified directory, if it doesn't exist.write_resultscommand has the same parameters aswrite_result, but uses-dor--dirnameinstead of-for--filename:from nornir_cli.common_commands import write_results # code # ... write_results(result, dirname="results", vars=["result", "diff"], no_errors=True) # :result: is nornir.core.task.Result object -
_pickle_to_hidden_fileIf you don't want to use
runbook collectionsyou can usenornir_clifor inventory management only.You can get
nornir.core.Nornirobject with inventory, filter this inventory and save it usingnornir_cli, and then use the result:# get nornir.core.Nornir object, filter inventory and save it $ nornir_cli init nornir-scrapli filter -s -a 'name__contains=dev'from nornir_cli.common_commands import _pickle_to_hidden_file def cli(nr): def task(task): # code # ... if __name__ == "__main__": # get current nornir.core.Nornir object from nornir_cli nr = _pickle_to_hidden_file("temp.pkl", mode="rb", dump=False) # run task with this object cli(nr)
nornir_jinja2 plugin#
Why is the nornir_jinja2 plugin here? And then, together with NetBox, this is a really useful thing. You can use NetBox as a variable source for jinja2 templates. Then nornir_cli can replace the tool for generating configs. It also motivates you to keep NetBox up-to-date as a Source of Truth. And we need such a motivation, based on the connectivity of different tools.
How to craft xml from yang#
When using scrapli_netconf from nornir_cli, you may find it useful to be able to get xml from yang.
Easy way to get xml from yang:
- get a model for your vedor. As instance, here
- export yang to html, and copy the path:
cd /to/directory/with/yang/models # use pyang tool # huawei is here as example only $ pyang -f jstree -o huawei-ifm.html huawei-ifm.yang # open html in browser $ open huawei-ifm.html - find the path and remove unwanted:
$ echo "/ifm:ifm/ifm:interfaces/ifm:interface/ifm:ifName" | sed 's/ifm://g' /ifm/interfaces/interface/ifName - filter the pyang output sample-xml-skeleton using the --sample-xml-skeleton-path and get xml:
$ pyang -f sample-xml-skeleton --sample-xml-skeleton-path \ "/ifm/interfaces/interface/ifName" huawei-ifm.yang | tr -d "\n" \ | sed -r 's/>\s+</></g' <?xml version='1.0' encoding='UTF-8'?><data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <ifm xmlns="http://www.huawei.com/netconf/vrp/huawei-ifm"><interfaces><interface><ifName/> </interface></interfaces></ifm></data> - run
scrapli_netconf,nornir_netconf,nornir_pyezfromnornir_cli
Thanks hellt for this tutorial.
Sources:
Command exceptions#
nornir_cli v1.3.0 includes some commands, that require a unique python runner:
nornir-netmiko netmiko_send_command with use_timing option:
Current python runner (see nornir_cli/plugin_commands/cmd_common.py) does not check the output, by default, so, now, it will not be possible to add conditions to the check, as described in the example.
use_timing option works, but it doesn't make sense.
You can use nornir-scrapli send_interactive method instead of nornir-netmiko netmiko_send_command with use_timing option. See example here
nornir-scrapli cfg_load_config:
scrapli cfg_load_config has **kwargs parameters, that depends on platforms. Python runner for nornir_cli version <=1.2.0 did not support passing any additional arguments, but starting from nornir_cli version 1.3.0, you can pass any additional arguments as a json string:
$ nornir_cli nornir-scrapli cfg_load_config '{"config": "..." "any":"arguments"}'
**kwargs parameters.
nornir-pyxl pyxl_map_data:
pyxl_map_data command was excluded from nornir_cli - Nested Dict Magic Key is not supported now.