主机清单

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

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

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

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

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

    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. "name": "str",
    2. "connection_options": {
    3. "$connection_type": {
    4. "extras": {
    5. "$key": "$value"
    6. },
    7. "hostname": "str",
    8. "port": "int",
    9. "username": "str",
    10. "password": "str",
    11. "platform": "str"
    12. }
    13. },
    14. "groups": [
    15. "$group_name"
    16. ],
    17. "data": {
    18. "$key": "$value"
    19. },
    20. "port": "int",
    21. "username": "str",
    22. "password": "str",
    23. "platform": "str"
    24. }

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

    如果需要登录设备,那么 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. [14]:
    1. 'netdevops.local'

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

    1. [15]:
    1. try:
    2. host01_gz["non_existent"]
    3. except KeyError as 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. [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,之后不会再更新数据。

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

    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 对象进行位运算(&|~)来返回查询对象。

    来看几个例子:

    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. {'spine00.bj': Host: spine00.bj,
    2. 'spine01.bj': Host: spine01.bj,