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---
  2host1.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
 21host2.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
 40spine00.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
 53spine01.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
 66leaf00.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
 80leaf01.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
 94host1.bma:
 95    groups:
 96        - bma
 97    platform: linux
 98    data:
 99        site: bma
100        role: host
101        type: host
102
103host2.bma:
104    groups:
105        - bma
106    platform: linux
107    data:
108        site: bma
109        role: host
110        type: host
111
112spine00.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
125spine01.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
138leaf00.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
151leaf01.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---
 2global:
 3    data:
 4        domain: global.local
 5        asn: 1
 6
 7eu:
 8    data:
 9        asn: 65100
10
11bma:
12    groups:
13        - eu
14        - global
15
16cmh:
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---
2data:
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---
 2global:
 3    data:
 4        domain: global.local
 5        asn: 1
 6
 7eu:
 8    data:
 9        asn: 65100
10
11bma:
12    groups:
13        - eu
14        - global
15
16cmh:
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:

  1. Use a filter function.

  2. 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.