解析并格式化 NETCONF 回显内容
在 《Python 使用 NETCONF 管理配置 H3C 网络设备》中,简单介绍了 Python 使用 NETCONF 操作网络设备。
对于配置类的操作,即 edit-config
,NETCONF 的回显内容一般情况下为 ok
或者具体的报错信息;对于查询类的操作,即 get
get-config
等,回显内容为 XML 格式,可读性较差,此时需要对查询到的内容进行格式化。
思路
对于 XML 格式的数据,可以直接使用 XML 模块来进行解析,由于查询信息时,已经传入了一个 XML,那么进行解析时,可以根据这个 XML 来进行操作,使用 lxml 模块来进行实际操作。
针对网络设备的回显信息,先解析为 lxml 支持的格式如 Element
,再使用 lxml 中 find 相关的方法,并添加上命名空间和具体的查询元素,查找到最终想要的信息。
示例
查询接口列表
获取信息
首先构建查询接口信息的 XML。
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 内容下发到设备上,获取信息。
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))
获取到的信息如下,主要看回显信息的类型和支持的方法。
<?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
。
# ...
print(ret.data,type(ret.data))
print(ret.data==ret.data_ele)
返回结果如下:
<Element {urn:ietf:params:xml:ns:netconf:base:1.0}data at 0x7fc9c8ab2180> <class 'lxml.etree._Element'>
True
内容拆解
网络设备接口有很多,因此使用 Element
的 findall() 方法,并加上命名空间和想要查找的元素。
上文查询信息的 XML 中,接口是以 <Interface>...</Interface>
来进行筛选的,返回值也是如此,所以以该元素作为查找值:
print(ret.data.findall('.//{http://www.h3c.com/netconf/data:1.0}Interface'))
回显信息:
[<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 循环将每个接口的每个属性都放到单独的列表里面,接口信息将无法对应。
针对这种情况,可以有四种办法(第一种是当时的头脑风暴,现在仅作记录):
- 针对单个接口的每个元素进行 find,并将单个接口的信息放入一个字典中;
- 使用
Element
的 getchildren() 方法,通过获取 tag 和 text,直接将接口信息转换为字典; - 针对单个接口只使用 find 查找想要的信息,并将单个接口的信息放入一个字典中;
- 不重复造轮子,使用现成的模块 lxmltodict。
获取所有信息并格式化为字典
采用上面说的第二种的方法来获取每个接口的所有信息,最终效果:
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)
结果如下:
[{'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() 方法,我们获取到的单个接口信息转换为字符串,就是:
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 两个属性来获取到具体的内容。
for i in ifaces[0]:
print(i.tag, i.text)
结果如下:
{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
已经是我们想要得到的数据了!之后用字符串替换掉命名空间,写入字典,就得到了下面这个函数:
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
经过上面的步骤,已经得到了接口信息,是一个字典形式:
{'IfIndex': '1', 'Name': 'GigabitEthernet0/0', 'AdminStatus': '1', 'InetAddressIPV4': '192.168.56.20'}
到这一步应该算成功了, XML 内容转换为了清晰可读的字典。
获取指定信息并格式化为字典(优化显示)
首先,要想获取指定信息,前提是要有一个获取信息的列表,这里我采用的类型是字典而不是列表,为的是替换原始的 key,增加可读性,如下:
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)
打印结果如下:
[{'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 获取接口信息时,可以直接获取接口的所有状态。
- 如果是交给程序调用,可以使用 getchildren() 的方法,获取到接口的所有信息并格式化为字典,之后交由其他模块来处理;
- 如果是呈现到使用者,例如前端或者 CLI 展示,可以使用获取指定信息的方法,提高返回值的可读性。
简单方法
有现成的模块可以直接将 xml 格式转换为字典,就是 xmltodict
模块。
pip install xmltodict
这个模块使用了 OrderedDict 类来实现,也解决了上面提到的字典无序导致的无法进行值对应的问题。
Python 中的字典是无序的,因为它是按照 HASH 来存储的,不过 Python 中有个模块 collections,里面自带了一个子类 OrderedDict,实现了对字典对象中元素的排序。
使用方法也很简单,直接传入 XML 内容即可进行解析。
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']))
结果如下:
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 ~
不过不论哪种方法提取数据,适合自己的才是最好的,手写简单的轮子可以更理解的更深入一点 (*  ̄︿ ̄)。