Inventory¶
The Inventory is arguably the most important piece of nornir. Let’s see how it works. To begin with the inventory is comprised of hosts, groups and defaults.
In this tutorial we are using the SimpleInventory plugin. This inventory plugin stores all the relevant data in three files. Let’s start by checking them:
[2]:
# hosts file
%highlight_file inventory/hosts.yaml
[2]:
1 --- 2 host1.cmh: 3 hostname: 127.0.0.1 4 port: 2201 5 username: vagrant 6 password: vagrant 7 platform: linux 8 groups: 9 - cmh 10 data: 11 site: cmh 12 role: host 13 type: host 14 nested_data: 15 a_dict: 16 a: 1 17 b: 2 18 a_list: [1, 2] 19 a_string: "asdasd" 20 21 host2.cmh: 22 hostname: 127.0.0.1 23 port: 2202 24 username: vagrant 25 password: vagrant 26 platform: linux 27 groups: 28 - cmh 29 data: 30 site: cmh 31 role: host 32 type: host 33 nested_data: 34 a_dict: 35 b: 2 36 c: 3 37 a_list: [1, 2] 38 a_string: "qwe" 39 40 spine00.cmh: 41 hostname: 127.0.0.1 42 username: vagrant 43 password: vagrant 44 port: 12444 45 platform: eos 46 groups: 47 - cmh 48 data: 49 site: cmh 50 role: spine 51 type: network_device 52 53 spine01.cmh: 54 hostname: 127.0.0.1 55 username: vagrant 56 password: "" 57 platform: junos 58 port: 12204 59 groups: 60 - cmh 61 data: 62 site: cmh 63 role: spine 64 type: network_device 65 66 leaf00.cmh: 67 hostname: 127.0.0.1 68 username: vagrant 69 password: vagrant 70 port: 12443 71 platform: eos 72 groups: 73 - cmh 74 data: 75 site: cmh 76 role: leaf 77 type: network_device 78 asn: 65100 79 80 leaf01.cmh: 81 hostname: 127.0.0.1 82 username: vagrant 83 password: "" 84 port: 12203 85 platform: junos 86 groups: 87 - cmh 88 data: 89 site: cmh 90 role: leaf 91 type: network_device 92 asn: 65101 93 94 host1.bma: 95 groups: 96 - bma 97 platform: linux 98 data: 99 site: bma 100 role: host 101 type: host 102 103 host2.bma: 104 groups: 105 - bma 106 platform: linux 107 data: 108 site: bma 109 role: host 110 type: host 111 112 spine00.bma: 113 hostname: 127.0.0.1 114 username: vagrant 115 password: vagrant 116 port: 12444 117 platform: eos 118 groups: 119 - bma 120 data: 121 site: bma 122 role: spine 123 type: network_device 124 125 spine01.bma: 126 hostname: 127.0.0.1 127 username: vagrant 128 password: "" 129 port: 12204 130 platform: junos 131 groups: 132 - bma 133 data: 134 site: bma 135 role: spine 136 type: network_device 137 138 leaf00.bma: 139 hostname: 127.0.0.1 140 username: vagrant 141 password: vagrant 142 port: 12443 143 platform: eos 144 groups: 145 - bma 146 data: 147 site: bma 148 role: leaf 149 type: network_device 150 151 leaf01.bma: 152 hostname: 127.0.0.1 153 username: vagrant 154 password: wrong_password 155 port: 12203 156 platform: junos 157 groups: 158 - bma 159 data: 160 site: bma 161 role: leaf 162 type: network_device
The hosts file is basically a map where the outermost key is the name of the host and then a Host
object. You can see the schema of the object by executing:
[3]:
from nornir.core.inventory import Host
import json
print(json.dumps(Host.schema(), indent=4))
{
"name": "str",
"connection_options": {
"$connection_type": {
"extras": {
"$key": "$value"
},
"hostname": "str",
"port": "int",
"username": "str",
"password": "str",
"platform": "str"
}
},
"groups": [
"$group_name"
],
"data": {
"$key": "$value"
},
"hostname": "str",
"port": "int",
"username": "str",
"password": "str",
"platform": "str"
}
The groups_file
follows the same rules as the hosts_file
.
[4]:
# groups file
%highlight_file inventory/groups.yaml
[4]:
1 --- 2 global: 3 data: 4 domain: global.local 5 asn: 1 6 7 eu: 8 data: 9 asn: 65100 10 11 bma: 12 groups: 13 - eu 14 - global 15 16 cmh: 17 data: 18 asn: 65000 19 vlans: 20 100: frontend 21 200: backend
Finally, the defaults file has the same schema as the Host
we described before but without outer keys to denote individual elements. We will see how the data in the groups and defaults file is used later on in this tutorial.
[5]:
# defaults file
%highlight_file inventory/defaults.yaml
[5]:
1 --- 2 data: 3 domain: acme.local
Accessing the inventory¶
You can access the inventory with the inventory
attribute:
[6]:
from nornir import InitNornir
nr = InitNornir(config_file="config.yaml")
print(nr.inventory.hosts)
{'host1.cmh': Host: host1.cmh, 'host2.cmh': Host: host2.cmh, 'spine00.cmh': Host: spine00.cmh, 'spine01.cmh': Host: spine01.cmh, 'leaf00.cmh': Host: leaf00.cmh, 'leaf01.cmh': Host: leaf01.cmh, 'host1.bma': Host: host1.bma, 'host2.bma': Host: host2.bma, 'spine00.bma': Host: spine00.bma, 'spine01.bma': Host: spine01.bma, 'leaf00.bma': Host: leaf00.bma, 'leaf01.bma': Host: leaf01.bma}
The inventory has two dict-like attributes hosts
and groups
that you can use to access the hosts and groups respectively:
[7]:
nr.inventory.hosts
[7]:
{'host1.cmh': Host: host1.cmh,
'host2.cmh': Host: host2.cmh,
'spine00.cmh': Host: spine00.cmh,
'spine01.cmh': Host: spine01.cmh,
'leaf00.cmh': Host: leaf00.cmh,
'leaf01.cmh': Host: leaf01.cmh,
'host1.bma': Host: host1.bma,
'host2.bma': Host: host2.bma,
'spine00.bma': Host: spine00.bma,
'spine01.bma': Host: spine01.bma,
'leaf00.bma': Host: leaf00.bma,
'leaf01.bma': Host: leaf01.bma}
[8]:
nr.inventory.groups
[8]:
{'global': Group: global,
'eu': Group: eu,
'bma': Group: bma,
'cmh': Group: cmh}
[9]:
nr.inventory.hosts["leaf01.bma"]
[9]:
Host: leaf01.bma
Hosts and groups are also dict-like objects:
[10]:
host = nr.inventory.hosts["leaf01.bma"]
host.keys()
[10]:
dict_keys(['site', 'role', 'type', 'asn', 'domain'])
[11]:
host["site"]
[11]:
'bma'
Inheritance model¶
Let’s see how the inheritance models works by example. Let’s start by looking again at the groups file:
[12]:
# groups file
%highlight_file inventory/groups.yaml
[12]:
1 --- 2 global: 3 data: 4 domain: global.local 5 asn: 1 6 7 eu: 8 data: 9 asn: 65100 10 11 bma: 12 groups: 13 - eu 14 - global 15 16 cmh: 17 data: 18 asn: 65000 19 vlans: 20 100: frontend 21 200: backend
The host leaf01.bma
belongs to the group bma
which in turn belongs to the groups eu
and global
. The host spine00.cmh
belongs to the group cmh
which doesn’t belong to any other group.
Data resolution works by iterating recursively over all the parent groups and trying to see if that parent group (or any of it’s parents) contains the data. For instance:
[13]:
leaf01_bma = nr.inventory.hosts["leaf01.bma"]
leaf01_bma["domain"] # comes from the group `global`
[13]:
'acme.local'
[14]:
leaf01_bma["asn"] # comes from group `eu`
[14]:
65100
Values in defaults
will be returned if neither the host nor the parents have a specific value for it.
[15]:
leaf01_cmh = nr.inventory.hosts["leaf01.cmh"]
leaf01_cmh["domain"] # comes from defaults
[15]:
'acme.local'
If nornir can’t resolve the data you should get a KeyError as usual:
[16]:
try:
leaf01_cmh["non_existent"]
except KeyError as e:
print(f"Couldn't find key: {e}")
Couldn't find key: 'non_existent'
You can also try to access data without recursive resolution by using the data
attribute. For example, if we try to access leaf01_cmh.data["domain"]
we should get an error as the host itself doesn’t have that data:
[17]:
try:
leaf01_cmh.data["domain"]
except KeyError as e:
print(f"Couldn't find key: {e}")
Couldn't find key: 'domain'
Filtering the inventory¶
So far we have seen that nr.inventory.hosts
and nr.inventory.groups
are dict-like objects that we can use to iterate over all the hosts and groups or to access any particular one directly. Now we are going to see how we can do some fancy filtering that will enable us to operate on groups of hosts based on their properties.
The simpler way of filtering hosts is by <key, value>
pairs. For instance:
[18]:
nr.filter(site="cmh").inventory.hosts.keys()
[18]:
dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh'])
You can also filter using multiple <key, value>
pairs:
[19]:
nr.filter(site="cmh", role="spine").inventory.hosts.keys()
[19]:
dict_keys(['spine00.cmh', 'spine01.cmh'])
Filter is cumulative:
[20]:
nr.filter(site="cmh").filter(role="spine").inventory.hosts.keys()
[20]:
dict_keys(['spine00.cmh', 'spine01.cmh'])
Or:
[21]:
cmh = nr.filter(site="cmh")
cmh.filter(role="spine").inventory.hosts.keys()
[21]:
dict_keys(['spine00.cmh', 'spine01.cmh'])
[22]:
cmh.filter(role="leaf").inventory.hosts.keys()
[22]:
dict_keys(['leaf00.cmh', 'leaf01.cmh'])
You can also grab the children of a group:
[23]:
nr.inventory.children_of_group("eu")
[23]:
{Host: host1.bma,
Host: host2.bma,
Host: leaf00.bma,
Host: leaf01.bma,
Host: spine00.bma,
Host: spine01.bma}
Advanced filtering¶
Sometimes you need more fancy filtering. For those cases you have two options:
- Use a filter function.
- Use a filter object.
Filter functions¶
The filter_func
parameter let’s you run your own code to filter the hosts. The function signature is as simple as my_func(host)
where host is an object of type Host and it has to return either True
or False
to indicate if you want to host or not.
[24]:
def has_long_name(host):
return len(host.name) == 11
nr.filter(filter_func=has_long_name).inventory.hosts.keys()
[24]:
dict_keys(['spine00.cmh', 'spine01.cmh', 'spine00.bma', 'spine01.bma'])
[25]:
# Or a lambda function
nr.filter(filter_func=lambda h: len(h.name) == 9).inventory.hosts.keys()
[25]:
dict_keys(['host1.cmh', 'host2.cmh', 'host1.bma', 'host2.bma'])
Filter Object¶
You can also use a filter objects to incrementally create a complex query objects. Let’s see how it works by example:
[26]:
# first you need to import the F object
from nornir.core.filter import F
[27]:
# hosts in group cmh
cmh = nr.filter(F(groups__contains="cmh"))
print(cmh.inventory.hosts.keys())
dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh'])
[28]:
# devices running either linux or eos
linux_or_eos = nr.filter(F(nornir_nos="linux") | F(nornir_nos="eos"))
print(linux_or_eos.inventory.hosts.keys())
dict_keys([])
[29]:
# spines in cmh
cmh_and_spine = nr.filter(F(groups__contains="cmh") & F(role="spine"))
print(cmh_and_spine.inventory.hosts.keys())
dict_keys(['spine00.cmh', 'spine01.cmh'])
[30]:
# cmh devices that are not spines
cmh_and_not_spine = nr.filter(F(groups__contains="cmh") & ~F(role="spine"))
print(cmh_and_not_spine.inventory.hosts.keys())
dict_keys(['host1.cmh', 'host2.cmh', 'leaf00.cmh', 'leaf01.cmh'])
You can also access nested data and even check if dicts/lists/strings contains elements. Again, let’s see by example:
[31]:
nested_string_asd = nr.filter(F(nested_data__a_string__contains="asd"))
print(nested_string_asd.inventory.hosts.keys())
dict_keys(['host1.cmh'])
[32]:
a_dict_element_equals = nr.filter(F(nested_data__a_dict__c=3))
print(a_dict_element_equals.inventory.hosts.keys())
dict_keys(['host2.cmh'])
[33]:
a_list_contains = nr.filter(F(nested_data__a_list__contains=2))
print(a_list_contains.inventory.hosts.keys())
dict_keys(['host1.cmh', 'host2.cmh'])
You can basically access any nested data by separating the elements in the path with two underscores __
. Then you can use __contains
to check if an element exists or if a string has a particular substring.