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 an InventoryElement object. You can see the schema of the object by executing:

[3]:
from nornir.core.deserializer.inventory import InventoryElement
import json
print(json.dumps(InventoryElement.schema(), indent=4))
{
    "title": "InventoryElement",
    "type": "object",
    "properties": {
        "hostname": {
            "title": "Hostname",
            "type": "string"
        },
        "port": {
            "title": "Port",
            "type": "integer"
        },
        "username": {
            "title": "Username",
            "type": "string"
        },
        "password": {
            "title": "Password",
            "type": "string"
        },
        "platform": {
            "title": "Platform",
            "type": "string"
        },
        "groups": {
            "title": "Groups",
            "default": [],
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "data": {
            "title": "Data",
            "default": {},
            "type": "object"
        },
        "connection_options": {
            "title": "Connection_Options",
            "default": {},
            "type": "object",
            "additionalProperties": {
                "$ref": "#/definitions/ConnectionOptions"
            }
        }
    },
    "definitions": {
        "ConnectionOptions": {
            "title": "ConnectionOptions",
            "type": "object",
            "properties": {
                "hostname": {
                    "title": "Hostname",
                    "type": "string"
                },
                "port": {
                    "title": "Port",
                    "type": "integer"
                },
                "username": {
                    "title": "Username",
                    "type": "string"
                },
                "password": {
                    "title": "Password",
                    "type": "string"
                },
                "platform": {
                    "title": "Platform",
                    "type": "string"
                },
                "extras": {
                    "title": "Extras",
                    "type": "object"
                }
            }
        }
    }
}

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 InventoryElement 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:

  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 object to create incrementally a complext query object. 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.