主机清单
主机清单(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]:
# %load files/inventory/hosts.yaml
---
host01.bj:
hostname: 127.0.0.1
port: 2201
username: netdevops
password: netdevops
platform: linux
groups:
- bj
data:
site: bj
role: host
type: host
nested_data:
a_dict:
a: 1
b: 2
a_list: [1, 2]
a_string: "this is a web server"
spine00.bj:
hostname: 127.0.0.1
username: netdevops
password: netdevops
port: 12444
platform: ios
groups:
- bj
data:
site: bj
role: spine
type: network_device
spine01.bj:
hostname: 127.0.0.1
username: netdevops
password: ""
platform: junos
port: 12204
groups:
- bj
data:
site: bj
role: spine
type: network_device
leaf00.bj:
hostname: 127.0.0.1
username: netdevops
password: netdevops
port: 12443
platform: hp_comware
groups:
- bj
data:
site: bj
role: leaf
type: network_device
asn: 65100
leaf01.bj:
hostname: 127.0.0.1
username: netdevops
password: ""
port: 12203
platform: huawei
groups:
- bj
data:
site: bj
role: leaf
type: network_device
asn: 65101
host01.gz:
groups:
- gz
platform: linux
data:
site: gz
role: host
type: host
spine01.gz:
hostname: 127.0.0.1
username: netdevops
password: netdevops
port: 12444
platform: eos
groups:
- gz
data:
site: gz
role: spine
type: network_device
leaf01.gz:
hostname: 127.0.0.1
username: netdevops
password: netdevops
port: 12443
platform: eos
groups:
- gz
data:
site: gz
role: leaf
type: network_device
host00:
groups:
- gz
- bj
host01:
groups:
- bj
- gz
主机文件是由键值对组成的映射表,其中最外层的是主机名,第二层是主机的一些基本信息,第三层、第四层是主机的其他相关信息。可以通过以下代码来查看一个主机对象的数据模型:
[2]:
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"
}
通过这段代码可以看到一个主机对象可以包含的所有信息。
如果需要登录设备,那么 connection_options
里面的 5 个参数 hostname、port、username、password、platform 是必须包含的(注:默认情况下,connection_options
会从第二层进行取值,如果设备的登录地址和资产管理地址不一样,可以在该选项里面单独指定),如果有额外的连接参数需要传递(如 enable password 、指定连接方式等),则需要在 extras
里面进行添加;其他字段都是可以选的,其中用户可以将所需的任意信息定义到 data
字段中。
当然,如果主机信息只做资产管理的作用,没有登录设备的需求,除了最外层的主机名以外,其他字段都是可选的。
groups 文件和 hosts 文件一样,也是由键值对映射组成,来看一个示例:
[3]:
# %load files/inventory/groups.yaml
---
global:
data:
domain: global.local
asn: 1
north:
data:
asn: 65100
bj:
groups:
- north
- global
gz:
data:
asn: 65000
vlans:
100: wired
200: wireless
最后,defaults 文件与之前描述的 Host 对象架构一样,但是它只有 data
字段,没有其他外层键值对。
[4]:
# %load files/inventory/defaults.yaml
---
data:
domain: netdevops.local
访问主机清单
可以通过 nornir 对象的 inventory
属性来访问主机清单。
[5]:
from nornir import InitNornir
nr = InitNornir(config_file="files/config.yaml")
主机清单有两个类字典(dict-like)的属性:hosts
和 groups
,通过访问该属性,可以获取到当前有哪些主机和组。
查看加载的配置文件中包含哪些主机:
[6]:
nr.inventory.hosts
[6]:
{'host01.bj': Host: host01.bj,
'spine00.bj': Host: spine00.bj,
'spine01.bj': Host: spine01.bj,
'leaf00.bj': Host: leaf00.bj,
'leaf01.bj': Host: leaf01.bj,
'host01.gz': Host: host01.gz,
'spine01.gz': Host: spine01.gz,
'leaf01.gz': Host: leaf01.gz,
'host00': Host: host00,
'host01': Host: host01}
查看加载的配置文件中包含哪些组:
[7]:
nr.inventory.groups
[7]:
{'global': Group: global,
'north': Group: north,
'bj': Group: bj,
'gz': Group: gz}
主机和组都是类字典(dict-like)形式的对象,可以通过 [$values]
来访问它们的属性,以主机 host01.bj
为例,来查看一下这个主包含哪些属性:
[8]:
host = nr.inventory.hosts["host01.bj"]
host.keys()
[8]:
dict_keys(['site', 'role', 'type', 'nested_data', 'asn', 'domain'])
查看这个主机位于哪个站点:
[9]:
host["site"]
[9]:
'bj'
继承模型
Nornir 中,hosts、groups、defaults 数据之间有继承关系,下面来看一下继承是如何工作的。
[10]:
# %load files/inventory/groups.yaml
---
global:
data:
domain: global.local
asn: 1
north:
data:
asn: 65100
bj:
groups:
- north
- global
gz:
data:
asn: 65000
vlans:
100: wired
200: wireless
在 hosts.yaml
中,可以看到 host01.bj
属于 bj
组,bj
组又属于 north
和 global
组;主机 host01.gz
属于 gz
组。
在这里,nornir 的数据解析方式是:递归遍历所属的父组,并查看任意父组中是否包含相应的数据。
[11]:
host01_bj = nr.inventory.hosts["host01.bj"]
host01_bj["domain"] # 继承自 `global` 组
[11]:
'global.local'
[12]:
host01_bj["asn"] # 继承自 `north` 组
[12]:
65100
如果主机有数据,那么优先使用主机具有的数据,而不是从父组继承:
[13]:
leaf01_bj = nr.inventory.hosts["leaf01.bj"]
leaf01_bj["asn"] # 主机的 asn 为 65101,父组 `bj` 的 asn 为 65100
[13]:
65101
如果主机、父组都没有数据,那么会从 defaults
中继承:
[14]:
host01_gz = nr.inventory.hosts["host01.gz"]
host01_gz["domain"] # 从 `defaults` 中继承数据
[14]:
'netdevops.local'
如果 nornir 遍历了所有的父组,而且 defaults
中也没有数据,则会返回 KeyError
:
[15]:
try:
host01_gz["non_existent"]
except KeyError as e:
print(f"无法找到数据:{e}")
无法找到数据:'non_existent'
如果不想遍历父组的话,可以直接使用主机的 data
属性来访问。例如从上面的示例中 host01_bj
的 asn 是继承自父组 north
,直接通过 data
来访问这个属性的话,不会遍历父组,而是返回 KeyError
的错误。
父组之间数据的优先级关系
Nornir 通过遍历所有父组来查找数据,那么如果多个父组里面有相同的数据,会如何取值?通过一个不恰当的例子来看一下,host00
和 host01
都属于 bj
和 gz
组,但是配置文件中的顺序有所差异:
[16]:
host00 = nr.inventory.hosts["host00"]
print(host00.groups) # `gz` 的 asn 为 65000
host00["asn"]
[Group: gz, Group: bj]
[16]:
65000
[17]:
host01 = nr.inventory.hosts["host01"]
print(host01.groups) # `bj` 的 asn 为 65100,继承自 `north`
host01["asn"]
[Group: bj, Group: gz]
[17]:
65100
可以看到如果主机属于多个组,数据解析是按照列表的先后顺序进行迭代,源码实现中是对数据的 key
做了判断,如果遍历已经找到了对应的 key
,之后不会再更新数据。
主机清单的过滤方法
到目前已经看到 nr.inventory.hosts
和 nr.inventory.groups
是类字典(dict-like)的对象,可以使用它们来遍历所有主机和组或直接访问任何特定的主机和组。现在来看看如何进行一些更高级的过滤:根据主机的属性对来对一组主机进行操作。
过滤主机最简单的方法是通过 filter
传入键值对()参数,例如筛选站点是 bj
的机器:
[18]:
nr.filter(site='bj').inventory.hosts
[18]:
{'host01.bj': Host: host01.bj,
'spine00.bj': Host: spine00.bj,
'spine01.bj': Host: spine01.bj,
'leaf00.bj': Host: leaf00.bj,
'leaf01.bj': Host: leaf01.bj}
也可以使用多个键值对来进行过滤,例如筛选站点是 bj
而且角色为 spine
的设备:
[19]:
nr.filter(site='bj', role='spine').inventory.hosts
[19]:
{'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}
filter
方法也可以进行叠加使用:
[20]:
nr.filter(site='bj').filter(role='spine').inventory.hosts
[20]:
{'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}
或者赋值给对象,进行再次过滤:
[21]:
bj = nr.filter(site='bj')
[22]:
bj.filter(role='spine').inventory.hosts
[22]:
{'spine00.bj': Host: spine00.bj, 'spine01.bj': Host: spine01.bj}
[23]:
bj.filter(role='leaf').inventory.hosts
[23]:
{'leaf00.bj': Host: leaf00.bj, 'leaf01.bj': Host: leaf01.bj}
还可以根据组进行过滤,例如查找所有属于 bj
组的主机:
[24]:
nr.inventory.children_of_group('bj')
[24]:
{Host: host00,
Host: host01,
Host: host01.bj,
Host: leaf00.bj,
Host: leaf01.bj,
Host: spine00.bj,
Host: spine01.bj}
高级过滤方法
有时候使用键值对无法满足过滤需求,还可以使用更高级的过滤方式:
过滤函数(filter function)
过滤对象(filter object)
过滤函数(filter functions)
Filter 方法里面的 filter_func
参数可以通过传入自定义代码来进行主机过滤。过滤函数的格式应该是 my_func(host)
,其中参数是一个主机对象(Host)并且返回值必须是 True
或 False
来确定过滤结果是否是需要的主机。
[25]:
# 过滤名字主机名长度为 10 的主机
def has_long_name(host):
return len(host.name) == 10
nr.filter(filter_func=has_long_name).inventory.hosts
[25]:
{'spine00.bj': Host: spine00.bj,
'spine01.bj': Host: spine01.bj,
'spine01.gz': Host: spine01.gz}
[26]:
# 或者使用 lambda 函数
nr.filter(filter_func=lambda h: len(h.name)==6).inventory.hosts
[26]:
{'host00': Host: host00, 'host01': Host: host01}
过滤对象(filter object)
使用过滤对象 F
来叠加创建复杂查询对象。
F
对象作为 filter
方法的参数,也接受键值对传参,可以使用叠加的双下划线来访问到任意数据(类似于字典的 []
取值),也可以使用 __contains
来检查一个元素中是否包含指定字符。同时还支持将多个 F
对象进行位运算(&
、|
、~
)来返回查询对象。
注:
__contains__
一般情况下是 Python 容器对象的方法,在 nornir 中,groups 是一个列表,所以对组进行过滤时,应该使用__contains
。
来看几个例子:
[27]:
# 首先引入 F 对象
from nornir.core.filter import F
[28]:
# 查看属于 `bj` 组的设备
bj = nr.filter(F(groups__contains='bj'))
bj.inventory.hosts
[28]:
{'host01.bj': Host: host01.bj,
'spine00.bj': Host: spine00.bj,
'spine01.bj': Host: spine01.bj,
'leaf00.bj': Host: leaf00.bj,
'leaf01.bj': Host: leaf01.bj,
'host00': Host: host00,
'host01': Host: host01}
[29]:
# 查看 `bj` 组中,系统是 `linux` 的设备
bj_linux = nr.filter(F(groups__contains='bj') & F(platform='linux'))
bj_linux.inventory.hosts
[29]:
{'host01.bj': Host: host01.bj}
[30]:
# 查看系统是 `ios` 或者 `eos` 的设备
ios_or_eos = nr.filter(F(platform='ios') | F(platform='eos'))
ios_or_eos.inventory.hosts
[30]:
{'spine00.bj': Host: spine00.bj,
'spine01.gz': Host: spine01.gz,
'leaf01.gz': Host: leaf01.gz}
[31]:
# 查看 `gz` 组中,角色不是 `spine` 的设备
gz_not_spine = nr.filter(F(groups__contains='gz') & ~F(role='spine'))
[32]:
gz_not_spine.inventory.hosts
[32]:
{'host01.gz': Host: host01.gz,
'leaf01.gz': Host: leaf01.gz,
'host00': Host: host00,
'host01': Host: host01}
[33]:
# 使用 `__` 来查看用户自定义的数据,并检查 dicts/lists/strings 是否包含元素
nested_dict = nr.filter(F(nested_data__a_dict__a=1))
nested_dict.inventory.hosts
[33]:
{'host01.bj': Host: host01.bj}
[34]:
nested_list = nr.filter(F(nested_data__a_list__contains=1))
nested_list.inventory.hosts
[34]:
{'host01.bj': Host: host01.bj}
[35]:
nested_string = nr.filter(F(nested_data__a_string__contains='web'))
nested_string.inventory.hosts
[35]:
{'host01.bj': Host: host01.bj}
[36]:
# 也可以对键值对的数据进行 `__contains` 查找
host_os = nr.filter(F(platform__contains='os'))
host_os.inventory.hosts
[36]:
{'spine00.bj': Host: spine00.bj,
'spine01.bj': Host: spine01.bj,
'spine01.gz': Host: spine01.gz,
'leaf01.gz': Host: leaf01.gz}