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]:
'global.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(platform="linux") | F(platform="eos"))
print(linux_or_eos.inventory.hosts.keys())
dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'leaf00.cmh', 'host1.bma', 'host2.bma', 'spine00.bma', 'leaf00.bma'])
[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.