Home > other >  influence data structure for python xml parser
influence data structure for python xml parser

Time:01-14

I'm pulling what is left of my hair out today.

I'm working with NETCONF and Juniper Junos devices and struggling to understand how to achieve something.

The problem is the XML config output is formatting annotations in a way that the parsers are not associating them with its node.

Here is some example xml from the device using the command show configuration snmp | display xml with the junk removed to make it easy to understand.

<rpc-reply xmlns:junos="http://xml.juniper.net/junos/21.2R0/junos">
    <configuration>
        <snmp>
            <client-list>
                <name>SNMP-POLLER-LIST</name>
                <junos:comment>/* snmp-poller-1 */</junos:comment>
                <client-address-list>
                    <name>1.1.1.1/32</name>
                </client-address-list>
                <client-address-list>
                    <name>1.1.1.2/32</name>
                </client-address-list>
                <junos:comment>/* snmp-poller-2 */</junos:comment>
                <client-address-list>
                    <name>2.2.2.2/32</name>
                </client-address-list>
            </client-list>
        </snmp>
    </configuration>
</rpc-reply>

This is basically an access list for SNMP access, not all of them have annotations, this is what the config on the Juniper looks like

client-list CF-SNMP-POLLER-LIST {
    /* snmp-poller-1 */
    1.1.1.1/32;
    1.1.1.2/32;
    /* snmp-poller-2 */
    2.2.2.2/32;
    }

When I parse the XML in Python 3.8 using lxml or xmltodict it produces a dictionary like below, it adds the comments to a separate list with no association with the client list hosts.

{
    "name": "SNMP-POLLER-LIST",
    "comment": [
        "/* snmp-poller-1 */",
        "/* snmp-poller-2 */"
    ],
    "client-address-list": [
        {
            "name": "1.1.1.1/32"
        },
        {
            "name": "3.3.3.3/32"
        },
        {
            "name": "2.2.2.2/32"
        }
    ]
}

My question is this, is there a way I can influence the parser to join the comment to the client-address-list items? Or a simple way to extend the parser?

eg:

{
    "name": "SNMP-POLLER-LIST",
    "client-address-list": [
        {
            "name": "1.1.1.1/32",
            "comment": "/* snmp-poller-1 */"
        },
        ...
    ]
}

I hope this makes sense

edit:

Here is a sample of lxml code I found in my python repl console This could be the start of something now I've stepped away and come back to it.

from lxml import etree
with open("test.xml", "rb") as fh:
    tree = etree.parse(fh)

root = tree.getroot()
rootchildren = root.iter()
for i in rootchildren:
    print(f"tag: {i.tag} text: {i.text}")

All my other code was veriants on load xml from file then send xml string to xmltodict

xmltodict could be my problem!

CodePudding user response:

Here you go. (no need for lxml - just core python)

import xml.etree.ElementTree as ET
from dataclasses import dataclass
from typing import List, Optional

xml = '''<rpc-reply xmlns:junos="http://xml.juniper.net/junos/21.2R0/junos">
    <configuration>
        <snmp>
            <client-list>
                <name>SNMP-POLLER-LIST</name>
                <junos:comment>/* snmp-poller-1 */</junos:comment>
                <client-address-list>
                    <name>1.1.1.1/32</name>
                </client-address-list>
                <client-address-list>
                    <name>1.1.1.2/32</name>
                    <name>12.1.145.2/64</name>
                </client-address-list>
                <junos:comment>/* snmp-poller-2 */</junos:comment>
                <client-address-list>
                    <name>2.2.2.2/32</name>
                </client-address-list>
            </client-list>
        </snmp>
    </configuration>
</rpc-reply>'''


@dataclass
class Entry:
    address_list: List[str]
    comment: Optional[str]


@dataclass
class Config:
    name: str
    entries: List[Entry]


root = ET.fromstring(xml)
client_list = root.find('.//client-list')
name = client_list.find('name').text

temp = []
for entry in client_list:
    if entry.tag not in ['client-address-list', '{http://xml.juniper.net/junos/21.2R0/junos}comment']:
        continue
    else:
        create_new_entry = False
        if entry.tag == '{http://xml.juniper.net/junos/21.2R0/junos}comment':
            comment = entry.text
        else:
            address_list = [a.text for a in entry.findall('name')]
            create_new_entry = True
        if create_new_entry:
            temp.append(Entry(address_list, comment))
            create_new_entry = False
            comment = None

config: Config = Config(name, temp)
print(config)

output

Config(name='SNMP-POLLER-LIST', entries=[Entry(address_list=['1.1.1.1/32'], comment='/* snmp-poller-1 */'), Entry(address_list=['1.1.1.2/32', '12.1.145.2/64'], comment=None), Entry(address_list=['2.2.2.2/32'], comment='/* snmp-poller-2 */')])
  • Related