格式化输出NETCONF回显内容

在 《Python 使用 NETCONF 管理配置 H3C 网络设备》中,简单介绍了 Python 使用 NETCONF 操作网络设备。

对于配置类的操作,即 edit-config,NETCONF 的回显内容一般情况下为 ok 或者具体的报错信息;对于查询类的操作,即 get get-config 等,回显内容为 XML 格式,可读性较差,此时需要对查询到的内容进行格式化。

思路

对于 XML 格式的数据,可以直接使用 XML 模块来进行解析,由于查询信息时,已经传入了一个 XML,那么进行解析时,可以根据这个 XML 来进行操作,使用 lxml 模块来进行实际操作。

针对网络设备的回显信息,先解析为 lxml 支持的格式如 Element ,再使用 lxml 中 find 相关的方法,并添加上命名空间和具体的查询元素,查找到最终想要的信息。

示例

查询接口列表

获取信息

首先构建查询接口信息的 XML。

1
2
3
4
5
6
7
8
9
10
11
12
13
get_all_iface = """
<top xmlns="http://www.h3c.com/netconf/data:1.0">
<Ifmgr>
<Interfaces>
<Interface>
<Name></Name>
<InetAddressIPV4></InetAddressIPV4>
<AdminStatus></AdminStatus>
</Interface>
</Interfaces>
</Ifmgr>
</top>
"""

然后将 XML 内容下发到设备上,获取信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from ncclient import manager

host = {
'host': '192.168.56.20',
'username': 'netdevops',
'password': 'netdevops',
'port': 830,
'device_params': {'name': 'h3c'},
}

# 对于 ssh 协议,连接设备时会先保存对端的 key,并从本机查找并验证,使用以下两个 False 的参数来跳过检查
conn = manager.connect(**host, hostkey_verify=False, look_for_keys=False)
# 获取设备所有接口的名称、IP地址、状态
ret = conn.get(('subtree', top))
print(ret,type(ret),dir(ret))

获取到的信息如下,主要看回显信息的类型和支持的方法。

1
2
3
<?xml version="1.0" encoding="UTF-8"?><rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="urn:uuid:e667b24e-14c9-4126-9581-7b25b7835b45"><data><top xmlns="http://www.h3c.com/netconf/data:1.0"><Ifmgr><Interfaces><Interface><IfIndex>1</IfIndex><Name>GigabitEthernet0/0</Name><AdminStatus>1</AdminStatus><InetAddressIPV4>192.168.56.20</InetAddressIPV4></Interface><Interface><IfIndex>2</IfIndex><Name>GigabitEthernet0/1</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>3</IfIndex><Name>GigabitEthernet0/2</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>4</IfIndex><Name>Serial1/0</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>5</IfIndex><Name>Serial2/0</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>6</IfIndex><Name>Serial3/0</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>7</IfIndex><Name>Serial4/0</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>8</IfIndex><Name>GigabitEthernet5/0</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>9</IfIndex><Name>GigabitEthernet5/1</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>10</IfIndex><Name>GigabitEthernet6/0</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>11</IfIndex><Name>GigabitEthernet6/1</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>129</IfIndex><Name>NULL0</Name><AdminStatus>1</AdminStatus></Interface><Interface><IfIndex>130</IfIndex><Name>InLoopBack0</Name><AdminStatus>1</AdminStatus><InetAddressIPV4>127.0.0.1</InetAddressIPV4></Interface><Interface><IfIndex>131</IfIndex><Name>Register-Tunnel0</Name><AdminStatus>1</AdminStatus></Interface></Interfaces></Ifmgr></top></data></rpc-reply> 
<class 'ncclient.operations.retrieve.GetReply'>
['ERROR_CLS', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_data', '_errors', '_huge_tree', '_parsed', '_parsing_hook', '_raw', '_root', 'data', 'data_ele', 'data_xml', 'error', 'errors', 'ok', 'parse', 'xml']

从上面的内容可以看到,通过 ncclient 模块执行 get 操作后,返回值是一个 GetReply 的类,它支持 [‘data', 'data_ele', 'data_xml', 'error', 'errors', 'ok', 'parse', 'xml']方法。

可以使用 data 或者 data_ele ,这两个方法最终的结果是相同的,都是返回 Element

1
2
3
# ...
print(ret.data,type(ret.data))
print(ret.data==ret.data_ele)

返回结果如下:

1
2
<Element {urn:ietf:params:xml:ns:netconf:base:1.0}data at 0x7fc9c8ab2180> <class 'lxml.etree._Element'>
True

内容拆解

网络设备接口有很多,因此使用 Element 的 findall() 方法,并加上命名空间和想要查找的元素。

上文查询信息的 XML 中,接口是以 <Interface>...</Interface> 来进行筛选的,返回值也是如此,所以以该元素作为查找值:

1
print(ret.data.findall('.//{http://www.h3c.com/netconf/data:1.0}Interface'))

回显信息:

1
[<Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5400>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5480>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5380>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5580>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5600>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5680>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5700>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5780>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5800>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5880>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5900>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5980>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5a00>, <Element {http://www.h3c.com/netconf/data:1.0}Interface at 0x7f681f2e5a80>]

回显信息为列表格式,且每个接口又有自己独立的 Element,又需要进行二次解析。

二次解析时,如果使用 findall 方法,只能按照每个接口的属性返回相应的列表;如接口的状态为一个列表,接口的名称为一个列表,接口的 IP 地址又是另一个列表;这时会有一个问题,使用 NETCONF 查询信息时,如果接口没有该属性,返回的 XML 内容中就不会有这个元素,上文的返回值中便是一个例子:如果接口下没有 IP 地址如 GigabitEthernet0/2,返回的 XML 中就没有 InetAddressIPV4 这个元素, 如果使用 for 循环将每个接口的每个属性都放到单独的列表里面,接口信息将无法对应。

针对这种情况,可以有四种办法(第一种是当时的头脑风暴,现在仅作记录):

  1. 针对单个接口的每个元素进行 find,并将单个接口的信息放入一个字典中;
  2. 使用 Element 的 getchildren() 方法,通过获取 tag 和 text,直接将接口信息转换为字典;
  3. 针对单个接口只使用 find 查找想要的信息,并将单个接口的信息放入一个字典中;
  4. 不重复造轮子,使用现成的模块 lxmltodict。
获取所有信息并格式化为字典

采用上面说的第二种的方法来获取每个接口的所有信息,最终效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
H3C_DATA = "http://www.h3c.com/netconf/data:1.0"
H3C_DATA_C = '{' + H3C_DATA_1_0 + '}'

# 将 findall 封装成一个函数,便于复用
def find_all_in_data(elem, value):
return _findall(elem, c, value)

def _findall(elem, ns, value):
return elem.findall('.//{}{}'.format(ns, value))

def elem_to_dict_all(elem, ns):
to_dict = {}
for e in elem.getchildren():
to_dict[e.tag.replace(ns,'')] = e.text
return to_dict

# ret 为上面直接从设备上获取到的返回值
# 根据上面的返回值,先获取到包含所有接口信息元素的列表,即上一个代码框中的回显信息
ifaces = find_all_in_data(ret.data,'Interface')
# 使用 Element 的 getchildren() 方法将所有的内容获取为字典,并将所有结果放入到一个列表里面
result = [elem_to_dict_all(iface, H3C_DATA_C) for iface in ifaces]
# 打印结果
print(result)

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[{'IfIndex': '1', 'Name': 'GigabitEthernet0/0', 'AdminStatus': '1', 'InetAddressIPV4': '192.168.56.20'}, 
{'IfIndex': '2', 'Name': 'GigabitEthernet0/1', 'AdminStatus': '1'},
{'IfIndex': '3', 'Name': 'GigabitEthernet0/2', 'AdminStatus': '1'},
{'IfIndex': '4', 'Name': 'Serial1/0', 'AdminStatus': '1'},
{'IfIndex': '5', 'Name': 'Serial2/0', 'AdminStatus': '1'},
{'IfIndex': '6', 'Name': 'Serial3/0', 'AdminStatus': '1'},
{'IfIndex': '7', 'Name': 'Serial4/0', 'AdminStatus': '1'},
{'IfIndex': '8', 'Name': 'GigabitEthernet5/0', 'AdminStatus': '1'},
{'IfIndex': '9', 'Name': 'GigabitEthernet5/1', 'AdminStatus': '1'},
{'IfIndex': '10', 'Name': 'GigabitEthernet6/0', 'AdminStatus': '1'},
{'IfIndex': '11', 'Name': 'GigabitEthernet6/1', 'AdminStatus': '1'},
{'IfIndex': '129', 'Name': 'NULL0', 'AdminStatus': '1'},
{'IfIndex': '130', 'Name': 'InLoopBack0', 'AdminStatus': '1', 'InetAddressIPV4': '127.0.0.1'}, {'IfIndex': '131', 'Name': 'Register-Tunnel0', 'AdminStatus': '1'}]

可以看到已经以字典方式获取到了所有接口的信息,之后再进行处理时,就更加容易了。

关于 getchildren() 方法,我们获取到的单个接口信息转换为字符串,就是:

1
2
3
4
5
from lxml import etree
ifaces = find_all_in_data(ret.data,'Interface')
print(etree.tostring(ifaces[0]))
# 得到结果如下:
b'<Interface xmlns="http://www.h3c.com/netconf/data:1.0"><IfIndex>1</IfIndex><Name>GigabitEthernet0/0</Name><AdminStatus>1</AdminStatus><InetAddressIPV4>192.168.56.20</InetAddressIPV4></Interface>'

从 XML 基础知识的角度来说明上面的结果: <Interface> 是一个根元素,<IfIndex> <Name> 等四项内容是根的子元素,xmlns 是根的属性,转换到 lxml 中,可以用 tag,text 两个属性来获取到具体的内容。

1
2
for i in ifaces[0]:
print(i.tag, i.text)

结果如下:

1
2
3
4
{http://www.h3c.com/netconf/data:1.0}IfIndex 1
{http://www.h3c.com/netconf/data:1.0}Name GigabitEthernet0/0
{http://www.h3c.com/netconf/data:1.0}AdminStatus 1
{http://www.h3c.com/netconf/data:1.0}InetAddressIPV4 192.168.56.20

已经是我们想要得到的数据了!之后用字符串替换掉命名空间,写入字典,就得到了下面这个函数:

1
2
3
4
5
def elem_to_dict_all(elem, ns):
to_dict = {}
for e in elem.getchildren():
to_dict[e.tag.replace(ns,'')] = e.text
return to_dict

经过上面的步骤,已经得到了接口信息,是一个字典形式:

1
{'IfIndex': '1', 'Name': 'GigabitEthernet0/0', 'AdminStatus': '1', 'InetAddressIPV4': '192.168.56.20'}

到这一步应该算成功了, XML 内容转换为了清晰可读的字典。

获取指定信息并格式化为字典(优化显示)

首先,要想获取指定信息,前提是要有一个获取信息的列表,这里我采用的类型是字典而不是列表,为的是替换原始的 key,增加可读性,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
H3C_DATA = "http://www.h3c.com/netconf/data:1.0"
H3C_DATA_C = '{' + H3C_DATA_1_0 + '}'

iface_key_map = {
'接口名称': 'Name',
'MTU': 'ActualMTU',
'IP 地址': 'InetAddressIPV4',
'掩码': 'InetAddressIPV4Mask',
'配置状态''AdminStatus',
}

def elem_to_dict(elem, ns, key_map):
to_dict = {}
for k,v in key_map.items():
# 传递的 elem 是具体接口的信息,属性不会重复,所以这里使用 find 方法
field = elem.find('.//{}{}'.format(ns, v))
if field is not None:
text = field.text
to_dict[k] = text
return to_dict

def data_elem_to_dict(elem, key_map):
return elem_to_dict(elem, H3C_DATA_C, key_map)

# ret 为上面直接从设备上获取到的返回值
ifaces = find_all_in_data(ret.data,'Interface')
# 获取字典里面指定的信息
result = [data_elem_to_dict(iface,iface_key_map) for iface in ifaces]
print(result)

打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[{'IP 地址': '192.168.56.20', '接口名称': 'GigabitEthernet0/0'},
{'接口名称': 'GigabitEthernet0/1'},
{'接口名称': 'GigabitEthernet0/2'},
{'接口名称': 'Serial1/0'},
{'接口名称': 'Serial2/0'},
{'接口名称': 'Serial3/0'},
{'接口名称': 'Serial4/0'},
{'接口名称': 'GigabitEthernet5/0'},
{'接口名称': 'GigabitEthernet5/1'},
{'接口名称': 'GigabitEthernet6/0'},
{'接口名称': 'GigabitEthernet6/1'},
{'接口名称': 'NULL0'},
{'IP 地址': '127.0.0.1', '接口名称': 'InLoopBack0'},
{'接口名称': 'Register-Tunnel0'}]

通过这种方式,不但得到了字典形式的接口信息,而且对 key 进行可可读性处理。

对比

上面内容拆解中,说明了两种方法来实现对设备接口信息的格式化处理,这两种方式有各自应用场景。

个人观点:使用 XML 获取接口信息时,可以直接获取接口的所有状态。

  1. 如果是交给程序调用,可以使用 getchildren() 的方法,获取到接口的所有信息并格式化为字典,之后交由其他模块来处理;
  2. 如果是呈现到使用者,例如前端或者 CLI 展示,可以使用获取指定信息的方法,提高返回值的可读性。

简单方法

有现成的模块可以直接将 xml 格式转换为字典,就是 xmltodict 模块。

1
pip install xmltodict

这个模块使用了 OrderedDict 类来实现,也解决了上面提到的字典无序导致的无法进行值对应的问题。

Python 中的字典是无序的,因为它是按照 HASH 来存储的,不过 Python 中有个模块 collections,里面自带了一个子类 OrderedDict,实现了对字典对象中元素的排序。

使用方法也很简单,直接传入 XML 内容即可进行解析。

1
2
3
4
5
6
7
8
import xmltodict
from lxml import etree

# ret 为上面直接从设备上获取到的返回值
ifaces = find_all_in_data(ret.data,'Interface')
result = xmltodict.parse(etree.tostring(ifaces[0]))
print(result)
print(dict(result['Interface']))

结果如下:

1
2
OrderedDict([('Interface', OrderedDict([('@xmlns', 'http://www.h3c.com/netconf/data:1.0'), ('IfIndex', '1'), ('Name', 'GigabitEthernet0/0'), ('AdminStatus', '1'), ('InetAddressIPV4', '192.168.56.20')]))])
{'@xmlns': 'http://www.h3c.com/netconf/data:1.0', 'IfIndex': '1', 'Name': 'GigabitEthernet0/0', 'AdminStatus': '1', 'InetAddressIPV4': '192.168.56.20'}

使用 xmltodict 可以一步到位,直接将结果转换为字典!可以将 xmltodict 进行二次封装,进行可读处理等。

现成的轮子还是很方便 T_T ~

不过不论哪种方法提取数据,适合自己的才是最好的,手写简单的轮子可以更理解的更深入一点 (*  ̄︿ ̄)。