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:

  1. [2]:
  1. # hosts file
  2. %highlight_file inventory/hosts.yaml
  1. [2]:
  1. 1 ---
  2. 2 host1.cmh:
  3. 3 hostname: 127.0.0.1
  4. 4 port: 2201
  5. 5 username: vagrant
  6. 6 password: vagrant
  7. 7 platform: linux
  8. 8 groups:
  9. 9 - cmh
  10. 10 data:
  11. 11 site: cmh
  12. 12 role: host
  13. 13 type: host
  14. 14 nested_data:
  15. 15 a_dict:
  16. 16 a: 1
  17. 17 b: 2
  18. 18 a_list: [1, 2]
  19. 19 a_string: "asdasd"
  20. 20
  21. 21 host2.cmh:
  22. 22 hostname: 127.0.0.1
  23. 23 port: 2202
  24. 24 username: vagrant
  25. 25 password: vagrant
  26. 26 platform: linux
  27. 27 groups:
  28. 28 - cmh
  29. 29 data:
  30. 30 site: cmh
  31. 31 role: host
  32. 32 type: host
  33. 33 nested_data:
  34. 34 a_dict:
  35. 35 b: 2
  36. 36 c: 3
  37. 37 a_list: [1, 2]
  38. 38 a_string: "qwe"
  39. 39
  40. 40 spine00.cmh:
  41. 41 hostname: 127.0.0.1
  42. 42 username: vagrant
  43. 43 password: vagrant
  44. 44 port: 12444
  45. 45 platform: eos
  46. 46 groups:
  47. 47 - cmh
  48. 48 data:
  49. 49 site: cmh
  50. 50 role: spine
  51. 51 type: network_device
  52. 52
  53. 53 spine01.cmh:
  54. 54 hostname: 127.0.0.1
  55. 55 username: vagrant
  56. 56 password: ""
  57. 57 platform: junos
  58. 58 port: 12204
  59. 59 groups:
  60. 60 - cmh
  61. 61 data:
  62. 62 site: cmh
  63. 63 role: spine
  64. 64 type: network_device
  65. 65
  66. 66 leaf00.cmh:
  67. 67 hostname: 127.0.0.1
  68. 68 username: vagrant
  69. 69 password: vagrant
  70. 70 port: 12443
  71. 71 platform: eos
  72. 72 groups:
  73. 73 - cmh
  74. 74 data:
  75. 75 site: cmh
  76. 76 role: leaf
  77. 77 type: network_device
  78. 78 asn: 65100
  79. 79
  80. 80 leaf01.cmh:
  81. 81 hostname: 127.0.0.1
  82. 82 username: vagrant
  83. 83 password: ""
  84. 84 port: 12203
  85. 85 platform: junos
  86. 86 groups:
  87. 87 - cmh
  88. 88 data:
  89. 89 site: cmh
  90. 90 role: leaf
  91. 91 type: network_device
  92. 92 asn: 65101
  93. 93
  94. 94 host1.bma:
  95. 95 groups:
  96. 96 - bma
  97. 97 platform: linux
  98. 98 data:
  99. 99 site: bma
  100. 100 role: host
  101. 101 type: host
  102. 102
  103. 103 host2.bma:
  104. 104 groups:
  105. 105 - bma
  106. 106 platform: linux
  107. 107 data:
  108. 108 site: bma
  109. 109 role: host
  110. 110 type: host
  111. 111
  112. 112 spine00.bma:
  113. 113 hostname: 127.0.0.1
  114. 114 username: vagrant
  115. 115 password: vagrant
  116. 116 port: 12444
  117. 117 platform: eos
  118. 118 groups:
  119. 119 - bma
  120. 120 data:
  121. 121 site: bma
  122. 122 role: spine
  123. 123 type: network_device
  124. 124
  125. 125 spine01.bma:
  126. 126 hostname: 127.0.0.1
  127. 127 username: vagrant
  128. 128 password: ""
  129. 129 port: 12204
  130. 130 platform: junos
  131. 131 groups:
  132. 132 - bma
  133. 133 data:
  134. 134 site: bma
  135. 135 role: spine
  136. 136 type: network_device
  137. 137
  138. 138 leaf00.bma:
  139. 139 hostname: 127.0.0.1
  140. 140 username: vagrant
  141. 141 password: vagrant
  142. 142 port: 12443
  143. 143 platform: eos
  144. 144 groups:
  145. 145 - bma
  146. 146 data:
  147. 147 site: bma
  148. 148 role: leaf
  149. 149 type: network_device
  150. 150
  151. 151 leaf01.bma:
  152. 152 hostname: 127.0.0.1
  153. 153 username: vagrant
  154. 154 password: wrong_password
  155. 155 port: 12203
  156. 156 platform: junos
  157. 157 groups:
  158. 158 - bma
  159. 159 data:
  160. 160 site: bma
  161. 161 role: leaf
  162. 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:

  1. [3]:
  1. from nornir.core.inventory import Host
  2. import json
  3. print(json.dumps(Host.schema(), indent=4))
  1. {
  2. "name": "str",
  3. "connection_options": {
  4. "$connection_type": {
  5. "extras": {
  6. "$key": "$value"
  7. },
  8. "hostname": "str",
  9. "port": "int",
  10. "username": "str",
  11. "password": "str",
  12. "platform": "str"
  13. }
  14. },
  15. "groups": [
  16. "$group_name"
  17. ],
  18. "data": {
  19. "$key": "$value"
  20. },
  21. "hostname": "str",
  22. "port": "int",
  23. "username": "str",
  24. "password": "str",
  25. "platform": "str"
  26. }

The groups_file follows the same rules as the hosts_file.

  1. [4]:
  1. # groups file
  2. %highlight_file inventory/groups.yaml
  1. [4]:
  1. 1 ---
  2. 2 global:
  3. 3 data:
  4. 4 domain: global.local
  5. 5 asn: 1
  6. 6
  7. 7 eu:
  8. 8 data:
  9. 9 asn: 65100
  10. 10
  11. 11 bma:
  12. 12 groups:
  13. 13 - eu
  14. 14 - global
  15. 15
  16. 16 cmh:
  17. 17 data:
  18. 18 asn: 65000
  19. 19 vlans:
  20. 20 100: frontend
  21. 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.

  1. [5]:
  1. # defaults file
  2. %highlight_file inventory/defaults.yaml
  1. [5]:
  1. 1 ---
  2. 2 data:
  3. 3 domain: acme.local

Accessing the inventory

You can access the inventory with the inventory attribute:

  1. [6]:
  1. from nornir import InitNornir
  2. nr = InitNornir(config_file="config.yaml")
  3. print(nr.inventory.hosts)
  1. {'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:

  1. [7]:
  1. nr.inventory.hosts
  1. [7]:
  1. {'host1.cmh': Host: host1.cmh,
  2. 'host2.cmh': Host: host2.cmh,
  3. 'spine00.cmh': Host: spine00.cmh,
  4. 'spine01.cmh': Host: spine01.cmh,
  5. 'leaf00.cmh': Host: leaf00.cmh,
  6. 'leaf01.cmh': Host: leaf01.cmh,
  7. 'host1.bma': Host: host1.bma,
  8. 'host2.bma': Host: host2.bma,
  9. 'spine00.bma': Host: spine00.bma,
  10. 'spine01.bma': Host: spine01.bma,
  11. 'leaf00.bma': Host: leaf00.bma,
  12. 'leaf01.bma': Host: leaf01.bma}
  1. [8]:
  1. nr.inventory.groups
  1. [8]:
  1. {'global': Group: global,
  2. 'eu': Group: eu,
  3. 'bma': Group: bma,
  4. 'cmh': Group: cmh}
  1. [9]:
  1. nr.inventory.hosts["leaf01.bma"]
  1. [9]:
  1. Host: leaf01.bma

Hosts and groups are also dict-like objects:

  1. [10]:
  1. host = nr.inventory.hosts["leaf01.bma"]
  2. host.keys()
  1. [10]:
  1. dict_keys(['site', 'role', 'type', 'asn', 'domain'])
  1. [11]:
  1. host["site"]
  1. [11]:
  1. 'bma'

Inheritance model

Let’s see how the inheritance models works by example. Let’s start by looking again at the groups file:

  1. [12]:
  1. # groups file
  2. %highlight_file inventory/groups.yaml
  1. [12]:
  1. 1 ---
  2. 2 global:
  3. 3 data:
  4. 4 domain: global.local
  5. 5 asn: 1
  6. 6
  7. 7 eu:
  8. 8 data:
  9. 9 asn: 65100
  10. 10
  11. 11 bma:
  12. 12 groups:
  13. 13 - eu
  14. 14 - global
  15. 15
  16. 16 cmh:
  17. 17 data:
  18. 18 asn: 65000
  19. 19 vlans:
  20. 20 100: frontend
  21. 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:

  1. [13]:
  1. leaf01_bma = nr.inventory.hosts["leaf01.bma"]
  2. leaf01_bma["domain"] # comes from the group `global`
  1. [13]:
  1. 'global.local'
  1. [14]:
  1. leaf01_bma["asn"] # comes from group `eu`
  1. [14]:
  1. 65100

Values in defaults will be returned if neither the host nor the parents have a specific value for it.

  1. [15]:
  1. leaf01_cmh = nr.inventory.hosts["leaf01.cmh"]
  2. leaf01_cmh["domain"] # comes from defaults
  1. [15]:
  1. 'acme.local'

If nornir can’t resolve the data you should get a KeyError as usual:

  1. [16]:
  1. try:
  2. leaf01_cmh["non_existent"]
  3. except KeyError as e:
  4. print(f"Couldn't find key: {e}")
  1. 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:

  1. [17]:
  1. try:
  2. leaf01_cmh.data["domain"]
  3. except KeyError as e:
  4. print(f"Couldn't find key: {e}")
  1. 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:

  1. [18]:
  1. nr.filter(site="cmh").inventory.hosts.keys()
  1. [18]:
  1. dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh'])

You can also filter using multiple <key, value> pairs:

  1. [19]:
  1. nr.filter(site="cmh", role="spine").inventory.hosts.keys()
  1. [19]:
  1. dict_keys(['spine00.cmh', 'spine01.cmh'])

Filter is cumulative:

  1. [20]:
  1. nr.filter(site="cmh").filter(role="spine").inventory.hosts.keys()
  1. [20]:
  1. dict_keys(['spine00.cmh', 'spine01.cmh'])

Or:

  1. [21]:
  1. cmh = nr.filter(site="cmh")
  2. cmh.filter(role="spine").inventory.hosts.keys()
  1. [21]:
  1. dict_keys(['spine00.cmh', 'spine01.cmh'])
  1. [22]:
  1. cmh.filter(role="leaf").inventory.hosts.keys()
  1. [22]:
  1. dict_keys(['leaf00.cmh', 'leaf01.cmh'])

You can also grab the children of a group:

  1. [23]:
  1. nr.inventory.children_of_group("eu")
  1. [23]:
  1. {Host: host1.bma,
  2. Host: host2.bma,
  3. Host: leaf00.bma,
  4. Host: leaf01.bma,
  5. Host: spine00.bma,
  6. 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.

  1. [24]:
  1. def has_long_name(host):
  2. return len(host.name) == 11
  3. nr.filter(filter_func=has_long_name).inventory.hosts.keys()
  1. [24]:
  1. dict_keys(['spine00.cmh', 'spine01.cmh', 'spine00.bma', 'spine01.bma'])
  1. [25]:
  1. # Or a lambda function
  2. nr.filter(filter_func=lambda h: len(h.name) == 9).inventory.hosts.keys()
  1. [25]:
  1. 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:

  1. [26]:
  1. # first you need to import the F object
  2. from nornir.core.filter import F
  1. [27]:
  1. # hosts in group cmh
  2. cmh = nr.filter(F(groups__contains="cmh"))
  3. print(cmh.inventory.hosts.keys())
  1. dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'spine01.cmh', 'leaf00.cmh', 'leaf01.cmh'])
  1. [28]:
  1. # devices running either linux or eos
  2. linux_or_eos = nr.filter(F(platform="linux") | F(platform="eos"))
  3. print(linux_or_eos.inventory.hosts.keys())
  1. dict_keys(['host1.cmh', 'host2.cmh', 'spine00.cmh', 'leaf00.cmh', 'host1.bma', 'host2.bma', 'spine00.bma', 'leaf00.bma'])
  1. [29]:
  1. # spines in cmh
  2. cmh_and_spine = nr.filter(F(groups__contains="cmh") & F(role="spine"))
  3. print(cmh_and_spine.inventory.hosts.keys())
  1. dict_keys(['spine00.cmh', 'spine01.cmh'])
  1. [30]:
  1. # cmh devices that are not spines
  2. cmh_and_not_spine = nr.filter(F(groups__contains="cmh") & ~F(role="spine"))
  3. print(cmh_and_not_spine.inventory.hosts.keys())
  1. 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:

  1. [31]:
  1. nested_string_asd = nr.filter(F(nested_data__a_string__contains="asd"))
  2. print(nested_string_asd.inventory.hosts.keys())
  1. dict_keys(['host1.cmh'])
  1. [32]:
  1. a_dict_element_equals = nr.filter(F(nested_data__a_dict__c=3))
  2. print(a_dict_element_equals.inventory.hosts.keys())
  1. dict_keys(['host2.cmh'])
  1. [33]:
  1. a_list_contains = nr.filter(F(nested_data__a_list__contains=2))
  2. print(a_list_contains.inventory.hosts.keys())
  1. 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.