格式化输出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 | get_all_iface = """ |
然后将 XML 内容下发到设备上,获取信息。
1 | from ncclient import manager |
获取到的信息如下,主要看回显信息的类型和支持的方法。
1 | <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> |
从上面的内容可以看到,通过 ncclient 模块执行 get 操作后,返回值是一个 GetReply
的类,它支持 [‘data', 'data_ele', 'data_xml', 'error', 'errors', 'ok', 'parse', 'xml']
方法。
可以使用 data
或者 data_ele
,这两个方法最终的结果是相同的,都是返回 Element
。
1 | # ... |
返回结果如下:
1 | <Element {urn:ietf:params:xml:ns:netconf:base:1.0}data at 0x7fc9c8ab2180> <class 'lxml.etree._Element'> |
内容拆解
网络设备接口有很多,因此使用 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 循环将每个接口的每个属性都放到单独的列表里面,接口信息将无法对应。
针对这种情况,可以有四种办法(第一种是当时的头脑风暴,现在仅作记录):
- 针对单个接口的每个元素进行 find,并将单个接口的信息放入一个字典中;
- 使用
Element
的 getchildren() 方法,通过获取 tag 和 text,直接将接口信息转换为字典; - 针对单个接口只使用 find 查找想要的信息,并将单个接口的信息放入一个字典中;
- 不重复造轮子,使用现成的模块 lxmltodict。
获取所有信息并格式化为字典
采用上面说的第二种的方法来获取每个接口的所有信息,最终效果:
1 | H3C_DATA = "http://www.h3c.com/netconf/data:1.0" |
结果如下:
1 | [{'IfIndex': '1', 'Name': 'GigabitEthernet0/0', 'AdminStatus': '1', 'InetAddressIPV4': '192.168.56.20'}, |
可以看到已经以字典方式获取到了所有接口的信息,之后再进行处理时,就更加容易了。
关于 getchildren() 方法,我们获取到的单个接口信息转换为字符串,就是:
1 | from lxml import etree |
从 XML 基础知识的角度来说明上面的结果: <Interface>
是一个根元素,<IfIndex>
<Name>
等四项内容是根的子元素,xmlns
是根的属性,转换到 lxml 中,可以用 tag,text 两个属性来获取到具体的内容。
1 | for i in ifaces[0]: |
结果如下:
1 | {http://www.h3c.com/netconf/data:1.0}IfIndex 1 |
已经是我们想要得到的数据了!之后用字符串替换掉命名空间,写入字典,就得到了下面这个函数:
1 | def elem_to_dict_all(elem, ns): |
经过上面的步骤,已经得到了接口信息,是一个字典形式:
1 | {'IfIndex': '1', 'Name': 'GigabitEthernet0/0', 'AdminStatus': '1', 'InetAddressIPV4': '192.168.56.20'} |
到这一步应该算成功了, XML 内容转换为了清晰可读的字典。
获取指定信息并格式化为字典(优化显示)
首先,要想获取指定信息,前提是要有一个获取信息的列表,这里我采用的类型是字典而不是列表,为的是替换原始的 key,增加可读性,如下:
1 | H3C_DATA = "http://www.h3c.com/netconf/data:1.0" |
打印结果如下:
1 | [{'IP 地址': '192.168.56.20', '接口名称': 'GigabitEthernet0/0'}, |
通过这种方式,不但得到了字典形式的接口信息,而且对 key 进行可可读性处理。
对比
上面内容拆解中,说明了两种方法来实现对设备接口信息的格式化处理,这两种方式有各自应用场景。
个人观点:使用 XML 获取接口信息时,可以直接获取接口的所有状态。
- 如果是交给程序调用,可以使用 getchildren() 的方法,获取到接口的所有信息并格式化为字典,之后交由其他模块来处理;
- 如果是呈现到使用者,例如前端或者 CLI 展示,可以使用获取指定信息的方法,提高返回值的可读性。
简单方法
有现成的模块可以直接将 xml 格式转换为字典,就是 xmltodict
模块。
1 | pip install xmltodict |
这个模块使用了 OrderedDict 类来实现,也解决了上面提到的字典无序导致的无法进行值对应的问题。
Python 中的字典是无序的,因为它是按照 HASH 来存储的,不过 Python 中有个模块 collections,里面自带了一个子类 OrderedDict,实现了对字典对象中元素的排序。
使用方法也很简单,直接传入 XML 内容即可进行解析。
1 | import xmltodict |
结果如下:
1 | OrderedDict([('Interface', OrderedDict([('@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 ~
不过不论哪种方法提取数据,适合自己的才是最好的,手写简单的轮子可以更理解的更深入一点 (*  ̄︿ ̄)。