主机清单

主机清单(Inventory) 是 nornir 最重要的部分,它由 hosts、groups、defaults 三部分组成。它还支持多种插件,默认情况下使用 SimpleInventory 插件。在之前的版本中,nornir 还支持 Ansible、Netbox 等主机格式的插件,3.0 版本之后,除了最核心的功能外,其他的功能都需要手动导入插件来使用。

在本教程中使用 SimpleInventory 插件来了解主机清单相关的内容。

可以在 nornir.tech 中获取当前已经公开发布的插件。

SimpleInventory 插件中,需要 hosts、groups、defaults 三个文件来存储信息,其中 groups、defaults 文件不是必需的。

主机相关的文件都使用 YAML 格式来保存数据,YAML 是一种可读性较好的标记语言,有关 YAML 的内容,可以查看 YAML 入门教程或者 YAML 官方手册

现在来看一个 hosts 的示例文件:

  1. [1]:
  1. # %load files/inventory/hosts.yaml
  2. ---
  3. host01.bj:
  4. hostname: 127.0.0.1
  5. port: 2201
  6. username: netdevops
  7. password: netdevops
  8. platform: linux
  9. groups:
  10. - bj
  11. data:
  12. site: bj
  13. role: host
  14. type: host
  15. nested_data:
  16. a_dict:
  17. a: 1
  18. b: 2
  19. a_list: [1, 2]
  20. a_string: "this is a web server"
  21. spine00.bj:
  22. hostname: 127.0.0.1
  23. username: netdevops
  24. password: netdevops
  25. port: 12444
  26. platform: ios
  27. groups:
  28. - bj
  29. data:
  30. site: bj
  31. role: spine
  32. type: network_device
  33. spine01.bj:
  34. hostname: 127.0.0.1
  35. username: netdevops
  36. password: ""
  37. platform: junos
  38. port: 12204
  39. groups:
  40. - bj
  41. data:
  42. site: bj
  43. role: spine
  44. type: network_device
  45. leaf00.bj:
  46. hostname: 127.0.0.1
  47. username: netdevops
  48. password: netdevops
  49. port: 12443
  50. platform: hp_comware
  51. groups:
  52. - bj
  53. data:
  54. site: bj
  55. role: leaf
  56. type: network_device
  57. asn: 65100
  58. leaf01.bj:
  59. hostname: 127.0.0.1
  60. username: netdevops
  61. password: ""
  62. port: 12203
  63. platform: huawei
  64. groups:
  65. - bj
  66. data:
  67. site: bj
  68. role: leaf
  69. type: network_device
  70. asn: 65101
  71. host01.gz:
  72. groups:
  73. - gz
  74. platform: linux
  75. data:
  76. site: gz
  77. role: host
  78. type: host
  79. spine01.gz:
  80. hostname: 127.0.0.1
  81. username: netdevops
  82. password: netdevops
  83. port: 12444
  84. platform: eos
  85. groups:
  86. - gz
  87. data:
  88. site: gz
  89. role: spine
  90. type: network_device
  91. leaf01.gz:
  92. hostname: 127.0.0.1
  93. username: netdevops
  94. password: netdevops
  95. port: 12443
  96. platform: eos
  97. groups:
  98. - gz
  99. data:
  100. site: gz
  101. role: leaf
  102. type: network_device
  103. host00:
  104. groups:
  105. - gz
  106. - bj
  107. host01:
  108. groups:
  109. - bj
  110. - gz

主机文件是由键值对组成的映射表,其中最外层的是主机名,第二层是主机的一些基本信息,第三层、第四层是主机的其他相关信息。可以通过以下代码来查看一个主机对象的数据模型:

  1. [2]:
  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. }

通过这段代码可以看到一个主机对象可以包含的所有信息。

如果需要登录设备,那么 connection_options 里面的 5 个参数 hostname、port、username、password、platform 是必须包含的(注:默认情况下,connection_options 会从第二层进行取值,如果设备的登录地址和资产管理地址不一样,可以在该选项里面单独指定),如果有额外的连接参数需要传递(如 enable password 、指定连接方式等),则需要在 extras 里面进行添加;其他字段都是可以选的,其中用户可以将所需的任意信息定义到 data 字段中。

当然,如果主机信息只做资产管理的作用,没有登录设备的需求,除了最外层的主机名以外,其他字段都是可选的。

groups 文件和 hosts 文件一样,也是由键值对映射组成,来看一个示例:

  1. [3]:
  1. # %load files/inventory/groups.yaml
  2. ---
  3. global:
  4. data:
  5. domain: global.local
  6. asn: 1
  7. north:
  8. data:
  9. asn: 65100
  10. bj:
  11. groups:
  12. - north
  13. - global
  14. gz:
  15. data:
  16. asn: 65000
  17. vlans:
  18. 100: wired
  19. 200: wireless

最后,defaults 文件与之前描述的 Host 对象架构一样,但是它只有 data 字段,没有其他外层键值对。

  1. [4]:
  1. # %load files/inventory/defaults.yaml
  2. ---
  3. data:
  4. domain: netdevops.local

访问主机清单

可以通过 nornir 对象的 inventory 属性来访问主机清单。

  1. [5]:
  1. from nornir import InitNornir
  2. nr = InitNornir(config_file="files/config.yaml")

主机清单有两个类字典(dict-like)的属性:hostsgroups,通过访问该属性,可以获取到当前有哪些主机和组。

查看加载的配置文件中包含哪些主机:

  1. [6]:
  1. nr.inventory.hosts
  1. [6]:
  1. {'host01.bj': Host: host01.bj,
  2. 'spine00.bj': Host: spine00.bj,
  3. 'spine01.bj': Host: spine01.bj,
  4. 'leaf00.bj': Host: leaf00.bj,
  5. 'leaf01.bj': Host: leaf01.bj,
  6. 'host01.gz': Host: host01.gz,
  7. 'spine01.gz': Host: spine01.gz,
  8. 'leaf01.gz': Host: leaf01.gz,
  9. 'host00': Host: host00,
  10. 'host01': Host: host01}

查看加载的配置文件中包含哪些组:

  1. [7]:
  1. nr.inventory.groups
  1. [7]:
  1. {'global': Group: global,
  2. 'north': Group: north,
  3. 'bj': Group: bj,
  4. 'gz': Group: gz}

主机和组都是类字典(dict-like)形式的对象,可以通过 [$values] 来访问它们的属性,以主机 host01.bj 为例,来查看一下这个主包含哪些属性:

  1. [8]:
  1. host = nr.inventory.hosts["host01.bj"]
  2. host.keys()
  1. [8]:
  1. dict_keys(['site', 'role', 'type', 'nested_data', 'asn', 'domain'])

查看这个主机位于哪个站点:

  1. [9]:
  1. host["site"]
  1. [9]:
  1. 'bj'

继承模型

Nornir 中,hosts、groups、defaults 数据之间有继承关系,下面来看一下继承是如何工作的。

  1. [10]:
  1. # %load files/inventory/groups.yaml
  2. ---
  3. global:
  4. data:
  5. domain: global.local
  6. asn: 1
  7. north:
  8. data:
  9. asn: 65100
  10. bj:
  11. groups:
  12. - north
  13. - global
  14. gz:
  15. data:
  16. asn: 65000
  17. vlans:
  18. 100: wired
  19. 200: wireless

hosts.yaml 中,可以看到 host01.bj 属于 bj 组,bj 组又属于 northglobal 组;主机 host01.gz 属于 gz 组。

在这里,nornir 的数据解析方式是:递归遍历所属的父组,并查看任意父组中是否包含相应的数据。

  1. [11]:
  1. host01_bj = nr.inventory.hosts["host01.bj"]
  2. host01_bj["domain"] # 继承自 `global` 组
  1. [11]:
  1. 'global.local'
  1. [12]:
  1. host01_bj["asn"] # 继承自 `north` 组
  1. [12]:
  1. 65100

如果主机有数据,那么优先使用主机具有的数据,而不是从父组继承:

  1. [13]:
  1. leaf01_bj = nr.inventory.hosts["leaf01.bj"]
  2. leaf01_bj["asn"] # 主机的 asn 为 65101,父组 `bj` 的 asn 为 65100
  1. [13]:
  1. 65101

如果主机、父组都没有数据,那么会从 defaults 中继承:

  1. [14]:
  1. host01_gz = nr.inventory.hosts["host01.gz"]
  2. host01_gz["domain"] # 从 `defaults` 中继承数据
  1. [14]:
  1. 'netdevops.local'

如果 nornir 遍历了所有的父组,而且 defaults 中也没有数据,则会返回 KeyError:

  1. [15]:
  1. try:
  2. host01_gz["non_existent"]
  3. except KeyError as e:
  4. print(f"无法找到数据:{e}")
  1. 无法找到数据:'non_existent'

如果不想遍历父组的话,可以直接使用主机的 data 属性来访问。例如从上面的示例中 host01_bj 的 asn 是继承自父组 north,直接通过 data 来访问这个属性的话,不会遍历父组,而是返回 KeyError 的错误。

父组之间数据的优先级关系

Nornir 通过遍历所有父组来查找数据,那么如果多个父组里面有相同的数据,会如何取值?通过一个不恰当的例子来看一下,host00host01 都属于 bjgz 组,但是配置文件中的顺序有所差异:

  1. [16]:
  1. host00 = nr.inventory.hosts["host00"]
  2. print(host00.groups) # `gz` 的 asn 为 65000
  3. host00["asn"]
  1. [Group: gz, Group: bj]
  1. [16]:
  1. 65000
  1. [17]:
  1. host01 = nr.inventory.hosts["host01"]
  2. print(host01.groups) # `bj` 的 asn 为 65100,继承自 `north`
  3. host01["asn"]
  1. [Group: bj, Group: gz]
  1. [17]:
  1. 65100

可以看到如果主机属于多个组,数据解析是按照列表的先后顺序进行迭代,源码实现中是对数据的 key 做了判断,如果遍历已经找到了对应的 key,之后不会再更新数据。

主机清单的过滤方法

到目前已经看到 nr.inventory.hostsnr.inventory.groups 是类字典(dict-like)的对象,可以使用它们来遍历所有主机和组或直接访问任何特定的主机和组。现在来看看如何进行一些更高级的过滤:根据主机的属性对来对一组主机进行操作。

过滤主机最简单的方法是通过 filter 传入键值对()参数,例如筛选站点是 bj 的机器:

  1. [18]:
  1. nr.filter(site='bj').inventory.hosts
  1. [18]:
  1. {'host01.bj': Host: host01.bj,
  2. 'spine00.bj': Host: spine00.bj,
  3. 'spine01.bj': Host: spine01.bj,
  4. 'leaf00.bj': Host: leaf00.bj,
  5. 'leaf01.bj': Host: leaf01.bj}

也可以使用多个键值对来进行过滤,例如筛选站点是 bj 而且角色为 spine 的设备:

  1. [19]:
  1. nr.filter(site='bj', role='spine').inventory.hosts
  1. [19]:
  1. {'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}

filter 方法也可以进行叠加使用:

  1. [20]:
  1. nr.filter(site='bj').filter(role='spine').inventory.hosts
  1. [20]:
  1. {'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}

或者赋值给对象,进行再次过滤:

  1. [21]:
  1. bj = nr.filter(site='bj')
  1. [22]:
  1. bj.filter(role='spine').inventory.hosts
  1. [22]:
  1. {'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}
  1. [23]:
  1. bj.filter(role='leaf').inventory.hosts
  1. [23]:
  1. {'leaf00.bj': Host: leaf00.bj, 'leaf01.bj': Host: leaf01.bj}

还可以根据组进行过滤,例如查找所有属于 bj 组的主机:

  1. [24]:
  1. nr.inventory.children_of_group('bj')
  1. [24]:
  1. {Host: host00,
  2. Host: host01,
  3. Host: host01.bj,
  4. Host: leaf00.bj,
  5. Host: leaf01.bj,
  6. Host: spine00.bj,
  7. Host: spine01.bj}

高级过滤方法

有时候使用键值对无法满足过滤需求,还可以使用更高级的过滤方式:

  1. 过滤函数(filter function)

  2. 过滤对象(filter object)

过滤函数(filter functions)

Filter 方法里面的 filter_func 参数可以通过传入自定义代码来进行主机过滤。过滤函数的格式应该是 my_func(host),其中参数是一个主机对象(Host)并且返回值必须是 TrueFalse 来确定过滤结果是否是需要的主机。

  1. [25]:
  1. # 过滤名字主机名长度为 10 的主机
  2. def has_long_name(host):
  3. return len(host.name) == 10
  4. nr.filter(filter_func=has_long_name).inventory.hosts
  1. [25]:
  1. {'spine00.bj': Host: spine00.bj,
  2. 'spine01.bj': Host: spine01.bj,
  3. 'spine01.gz': Host: spine01.gz}
  1. [26]:
  1. # 或者使用 lambda 函数
  2. nr.filter(filter_func=lambda h: len(h.name)==6).inventory.hosts
  1. [26]:
  1. {'host00': Host: host00, 'host01': Host: host01}

过滤对象(filter object)

使用过滤对象 F 来叠加创建复杂查询对象。

F 对象作为 filter 方法的参数,也接受键值对传参,可以使用叠加的双下划线来访问到任意数据(类似于字典的 [] 取值),也可以使用 __contains 来检查一个元素中是否包含指定字符。同时还支持将多个 F 对象进行位运算(&|~)来返回查询对象。

注:__contains__ 一般情况下是 Python 容器对象的方法,在 nornir 中,groups 是一个列表,所以对组进行过滤时,应该使用 __contains

来看几个例子:

  1. [27]:
  1. # 首先引入 F 对象
  2. from nornir.core.filter import F
  1. [28]:
  1. # 查看属于 `bj` 组的设备
  2. bj = nr.filter(F(groups__contains='bj'))
  3. bj.inventory.hosts
  1. [28]:
  1. {'host01.bj': Host: host01.bj,
  2. 'spine00.bj': Host: spine00.bj,
  3. 'spine01.bj': Host: spine01.bj,
  4. 'leaf00.bj': Host: leaf00.bj,
  5. 'leaf01.bj': Host: leaf01.bj,
  6. 'host00': Host: host00,
  7. 'host01': Host: host01}
  1. [29]:
  1. # 查看 `bj` 组中,系统是 `linux` 的设备
  2. bj_linux = nr.filter(F(groups__contains='bj') & F(platform='linux'))
  3. bj_linux.inventory.hosts
  1. [29]:
  1. {'host01.bj': Host: host01.bj}
  1. [30]:
  1. # 查看系统是 `ios` 或者 `eos` 的设备
  2. ios_or_eos = nr.filter(F(platform='ios') | F(platform='eos'))
  3. ios_or_eos.inventory.hosts
  1. [30]:
  1. {'spine00.bj': Host: spine00.bj,
  2. 'spine01.gz': Host: spine01.gz,
  3. 'leaf01.gz': Host: leaf01.gz}
  1. [31]:
  1. # 查看 `gz` 组中,角色不是 `spine` 的设备
  2. gz_not_spine = nr.filter(F(groups__contains='gz') & ~F(role='spine'))
  1. [32]:
  1. gz_not_spine.inventory.hosts
  1. [32]:
  1. {'host01.gz': Host: host01.gz,
  2. 'leaf01.gz': Host: leaf01.gz,
  3. 'host00': Host: host00,
  4. 'host01': Host: host01}
  1. [33]:
  1. # 使用 `__` 来查看用户自定义的数据,并检查 dicts/lists/strings 是否包含元素
  2. nested_dict = nr.filter(F(nested_data__a_dict__a=1))
  3. nested_dict.inventory.hosts
  1. [33]:
  1. {'host01.bj': Host: host01.bj}
  1. [34]:
  1. nested_list = nr.filter(F(nested_data__a_list__contains=1))
  2. nested_list.inventory.hosts
  1. [34]:
  1. {'host01.bj': Host: host01.bj}
  1. [35]:
  1. nested_string = nr.filter(F(nested_data__a_string__contains='web'))
  2. nested_string.inventory.hosts
  1. [35]:
  1. {'host01.bj': Host: host01.bj}
  1. [36]:
  1. # 也可以对键值对的数据进行 `__contains` 查找
  2. host_os = nr.filter(F(platform__contains='os'))
  3. host_os.inventory.hosts
  1. [36]:
  1. {'spine00.bj': Host: spine00.bj,
  2. 'spine01.bj': Host: spine01.bj,
  3. 'spine01.gz': Host: spine01.gz,
  4. 'leaf01.gz': Host: leaf01.gz}