The Tabular Parser

parsergen is a generic parser for show commands. The goal is to make it simple to create a parser for any given show command one time, and then reuse this parser to create tests for any values found within the output.

The main functions of parsergen are:

  • Find and verify expected values in show command output.

  • Provide an OS agnostic common interface to OS specific show commands.

  • Create once (per command), use many times (throughout all of your pyATS test suites).

  • Maintain a per command cache for fast processing.

  • Only parse the parts of the output that the user is interested in, thus avoiding wasting precious real-time.

Column Based Results

For column based table results one should use the oper_fill_tabular class. To illustrate it’s use we’ll examine some standard table results.

Full show arp example

RP/0/0/CPU0:one#show arp
Fri Jan 22 15:39:49.731 EST

-------------------------------------------------------------------------------
0/0/CPU0
-------------------------------------------------------------------------------
Address         Age        Hardware Addr   State      Type  Interface
11.11.11.1      -          0292.77d4.d5ee  Interface  ARPA  GigabitEthernet0/0/0/0
10.10.10.1      -          02b7.a23a.e076  Interface  ARPA  GigabitEthernet0/0/0/1
11.11.11.2      00:00:35   02e9.4522.5326  Dynamic    ARPA  GigabitEthernet0/0/0/0
10.10.10.2      00:00:35   02db.ebba.ecc4  Dynamic    ARPA  GigabitEthernet0/0/0/1
RP/0/0/CPU0:one#

We need to identify a few things in order to parse this output. First if there are multiple tables we must identify what the title of each table is. In this case the title is a card reference. Next we must identify the headers that define the columns. For show arp the headers are Address, Age, Hardware Addr, State, Type, and Interface. Finally we need to determine how to uniquely identify entries from the table. These are the index fields. For show arp these would be the Address and Interface fields or columns 0 and 5. For this command that is all we need.

def test_log_arp_results_1 (self):
    res = parsergen.oper_fill_tabular(device=device1,
                                      show_command="show arp",
                                      header_fields=
                                          [ "Address",
                                            "Age",
                                            "Hardware Addr",
                                            "State",
                                            "Type",
                                            "Interface" ],
                                      index = [ 0, 5 ],
                                      table_title_pattern = r"^(\d+/\d+/CPU\d+)")

    log.info("Tabular parse result:\n" + pprint.pformat(res.entries))

Now when we execute the test the following output is logged:

Results:
{'0/0/CPU0': {'10.10.10.1': {'GigabitEthernet0/0/0/1': {'Address': '10.10.10.1',
                                                        'Age': '-',
                                                        'Hardware Addr': '02b7.a23a.e076',
                                                        'Interface': 'GigabitEthernet0/0/0/1',
                                                        'State': 'Interface',
                                                        'Type': 'ARPA'}},
              '10.10.10.2': {'GigabitEthernet0/0/0/1': {'Address': '10.10.10.2',
                                                        'Age': '00:00:14',
                                                        'Hardware Addr': '02db.ebba.ecc4',
                                                        'Interface': 'GigabitEthernet0/0/0/1',
                                                        'State': 'Dynamic',
                                                        'Type': 'ARPA'}},
              '11.11.11.1': {'GigabitEthernet0/0/0/0': {'Address': '11.11.11.1',
                                                        'Age': '-',
                                                        'Hardware Addr': '0292.77d4.d5ee',
                                                        'Interface': 'GigabitEthernet0/0/0/0',
                                                        'State': 'Interface',
                                                        'Type': 'ARPA'}},
              '11.11.11.2': {'GigabitEthernet0/0/0/0': {'Address': '11.11.11.2',
                                                        'Age': '00:00:14',
                                                        'Hardware Addr': '02e9.4522.5326',
                                                        'Interface': 'GigabitEthernet0/0/0/0',
                                                        'State': 'Dynamic',
                                                        'Type': 'ARPA'}}}}

For multi-table results the n-level dictionary in self.entries is first indexed by the match groups from the table_title_pattern, in this case that is the card location. The rest of the indices are given by the values from the columns identified in the index arg. Once all these indices are specified the result is the actual entry dictionary. This dictionary is indexed by the header name of the value you want.

So for example if we want to see the ARP entry’s MAC address for the IP 10.10.10.2 on one’s GigabitEthernet0/0/0/1 interface we would take the above results and use the following code:

addr = '10.10.10.2'
intf = 'GigabitEthernet0/0/0/1'
card = '0/0/CPU0'
macaddr = res.entries[card][addr][intf]['Hardware Addr']
log.info( addr + " on 0/0/CPU0 " + intf + " has MAC " + macaddr)

The following output is produced:

10.10.10.2 on GigabitEthernet0/0/0/1 has MAC 02db.ebba.ecc4

A Simpler show arp Example

It’s worth recognizing that in most cases the IP address alone is enough to uniquely identify an entry. Let’s use that knowledge and additionally specify a card location in our show command, as that will greatly simply the returned results.

RP/0/0/CPU0:one#show arp location 0/0/CPU0
Thu May 13 11:59:18.909 EDT

Address         Age        Hardware Addr   State      Type  Interface
10.10.10.1      -          02b7.a23a.e076  Interface  ARPA  GigabitEthernet0/0/0/1
11.11.11.1      -          0292.77d4.d5ee  Interface  ARPA  GigabitEthernet0/0/0/0
10.10.10.2      00:00:31   02db.ebba.ecc4  Dynamic    ARPA  GigabitEthernet0/0/0/1
11.11.11.2      00:00:31   02e9.4522.5326  Dynamic    ARPA  GigabitEthernet0/0/0/0
RP/0/0/CPU0:one#

Now for the test code.:

def test_log_arp_results_2 (self):
    res = parsergen.oper_fill_tabular(device=device1,
                                  show_command="show arp location 0/0/CPU0",
                                  header_fields =
                                    [ "Address",
                                      "Age",
                                      "Hardware Addr",
                                      "State",
                                      "Type",
                                      "Interface" ])
    log.info("Results:\n" + pprint.pformat(res.entries))

Notice we eliminate the index arg (by default column zero is considered the index column), as well as the table_title_pattern argument. Below we show the logged output from the above code.

Results:
{'12.12.12.1': {'Address': '12.12.12.1',
                'Age': '-',
                'Hardware Addr': '0292.77d4.d5ee',
                'Interface': 'GigabitEthernet0/0/0/0',
                'State': 'Interface',
                'Type': 'ARPA'},
 '12.12.12.2': {'Address': '12.12.12.2',
                'Age': '00:47:27',
                'Hardware Addr': '02e9.4522.5326',
                'Interface': 'GigabitEthernet0/0/0/0',
                'State': 'Dynamic',
                'Type': 'ARPA'},
 '13.13.13.1': {'Address': '13.13.13.1',
                'Age': '-',
                'Hardware Addr': '02b7.a23a.e076',
                'Interface': 'GigabitEthernet0/0/0/1',
                'State': 'Interface',
                'Type': 'ARPA'},
 '13.13.13.2': {'Address': '13.13.13.2',
                'Age': '00:47:27',
                'Hardware Addr': '02db.ebba.ecc4',
                'Interface': 'GigabitEthernet0/0/0/1',
                'State': 'Dynamic',
                'Type': 'ARPA'}}
TEST 2010-01-22 16:26:41,113: PASS: example_suite_t.test_log_arp_results_2

As you can see now we simply have to index using the IP to get at the actual entry dictionary. So to view the MAC address and interface for IP 10.10.10.2 we use the following code.

addr = '10.10.10.2'
macaddr = res.entries[addr]['Hardware Addr']
intf = res.entries[addr]['Interface']
exec_logger.info(twoaddr + " on " + intf + " has MAC addr " + macaddr)

Resulting in the following output:

10.10.10.2 on GigabitEthernet0/0/0/1 has MAC addr 02db.ebba.ecc4

Tabular parsing support for delimited Multi-line header tables

Paresergen supports parisng delimited tables with multi-line headers as illustrated in the below example.

sysadmin-vm:0_RP0# show controller sfe driver rack 0
Sun Apr  23 21:24:51.773 UTC-07:00

=========================================================================
SFE Driver information
=========================================================================

Driver Version: 1   (1.1)

Functional role: Active,   ISSU role: NA
Rack: 0/RP0, Type: lcc, Number: 0, IP Address: 192.1.0.1
Startup time       : 2017 Apr 15 04:04:32.117
Availability Masks :
     Card: 0x1       Asic: 0xF       Exp Asic: 0xF
Unicast/Multicast (ratio) : 0
+----------------------------------------------------------------+
|Process  | Connection | Registration| Connection | DLL          |
|/Lib     | status     | status      | requests   | registration |
+----------------------------------------------------------------+
| PM      |  Active    |  n/a        |           1|  n/a         |
| PL-LOCAL|  Active    |  Active     |           1|  n/a         |
| FSDB    |  Active    |  Active     |           1|  n/a         |
| FGID    |  Active    |  Active     |           1|  n/a         |
| CM      |  Active    |  Active     |           1|  n/a         |
| CCC     |  Active    |  n/a        |           1|  n/a         |
| GASPP   |  n/a       |  n/a        |         n/a|  n/a         |
| CIH     |  n/a       |  n/a        |         n/a|  Yes         |
+----------------------------------------------------------------+

Asics :
HP - HotPlug event,  PON - Power ON reset,     WB - Warm Boot,  A - All
HR - Hard Reset,     DC  - Disconnect signal,  DL - DownLoad
+-----------------------------------------------------------------------------------+
| Asic inst.|card|HP|Asic| Asic  | Admin|plane| Fgid| Asic State |DC| Last  |PON|HR |
|  (R/S/A)  |pwrd|  |type| class | /Oper|/grp | DL  |            |  | init  |(#)|(#)|
+-----------------------------------------------------------------------------------+
| 0/FC0/0   | UP | 1| s13|  FE1600| UP/UP| 0/0 | DONE| NRML       | 0| PON   |  1|  0|
| 0/FC0/1   | UP | 1| s13|  FE1600| UP/UP| 0/1 | DONE| NRML       | 0| PON   |  1|  0|
| 0/FC0/2   | UP | 1| s13|  FE1600| UP/UP| 0/2 | DONE| NRML       | 0| PON   |  1|  0|
| 0/FC0/3   | UP | 1| s13|  FE1600| UP/UP| 0/3 | DONE| NRML       | 0| PON   |  1|  0|
+-----------------------------------------------------------------------------------+

We will parse the second table. We only need to pass the delimiter so parsergen will be able to identify the table columns borders.

Now for the test code.:

def test_tabular_parser_12(self):
    result = oper_fill_tabular(device=device1,
                                show_command="show controller sfe driver rack 0",
                                refresh_cache=True,
                                header_fields=
                                [['Asic inst\.',
                                  'card',
                                  'HP',
                                  'Asic',
                                  'Asic',
                                  'Admin',
                                  'plane',
                                  'Fgid',
                                  'Asic State',
                                  'DC',
                                  'Last',
                                  'PON',
                                  'HR'],
                                ['\(R/S/A\)',
                                 'pwrd',
                                 '',
                                 'type',
                                 'class',
                                 '/Oper',
                                 '/grp',
                                 'DL',
                                 '',
                                 '',
                                 'init',
                                 '\(#\)',
                                 '\(#\)']],
                                label_fields=
                                [ "Asic inst. (R/S/A) ",
                                  "card pwrd",
                                  "HP",
                                  "Asic type",
                                  "Asic class",
                                  "Admin /Oper",
                                  "plane /grp",
                                  "Fgid DL",
                                  "Asic State",
                                  "DC",
                                  "Last init",
                                  "PON State (#)",
                                  "HR (#)" ],
                                index = [ 0, 12 ],
                                delimiter = '|')

    log.info("Results:\n" + pprint.pformat(res.entries))

Below we show the parsed output from the above code.

Results:
{'0/FC0/0': {'Admin /Oper': 'UP/UP',
             'Asic State': 'NRML',
             'Asic class': 'FE1600',
             'Asic inst. (R/S/A) ': '0/FC0/0',
             'Asic type': 's13',
             'DC': '0',
             'Fgid DL': 'DONE',
             'HP': '1',
             'HR (#)': '0',
             'Last init': 'PON',
             'PON State (#)': '1',
             'card pwrd': 'UP',
             'plane /grp': '0/0'},
 '0/FC0/1': {'Admin /Oper': 'UP/UP',
             'Asic State': 'NRML',
             'Asic class': 'FE1600',
             'Asic inst. (R/S/A) ': '0/FC0/1',
             'Asic type': 's13',
             'DC': '0',
             'Fgid DL': 'DONE',
             'HP': '1',
             'HR (#)': '0',
             'Last init': 'PON',
             'PON State (#)': '1',
             'card pwrd': 'UP',
             'plane /grp': '0/1'},
 '0/FC0/2': {'Admin /Oper': 'UP/UP',
             'Asic State': 'NRML',
             'Asic class': 'FE1600',
             'Asic inst. (R/S/A) ': '0/FC0/2',
             'Asic type': 's13',
             'DC': '0',
             'Fgid DL': 'DONE',
             'HP': '1',
             'HR (#)': '0',
             'Last init': 'PON',
             'PON State (#)': '1',
             'card pwrd': 'UP',
             'plane /grp': '0/2'},
 '0/FC0/3': {'Admin /Oper': 'UP/UP',
             'Asic State': 'NRML',
             'Asic class': 'FE1600',
             'Asic inst. (R/S/A) ': '0/FC0/3',
             'Asic type': 's13',
             'DC': '0',
             'Fgid DL': 'DONE',
             'HP': '1',
             'HR (#)': '0',
             'Last init': 'PON',
             'PON State (#)': '1',
             'card pwrd': 'UP',
             'plane /grp': '0/3'}}

Parse using device output

Parsergen now supports working without passing the device as an argument, user can pass the device output (as a string) along with the OS (for abstraction purpose) instead.

def test_non_tabular_parser(self):

    """
        Test non tabular parser when passing a device output
        and device os only and compare against selected tags.
    """

    pure_cli = dedent(self.showCommandOutput1)

    attrValPairsToCheck = [
        ('show.intf.if_name',                       'MgmtEth0/0/CPU0/0'),
        ('show.intf.line_protocol',                 'up'),
        ('show.intf.ip_address',                    '10.30.108.132'),
        ('show.intf.mtu',                           1514),
        ('show.intf.admin_state',                   'up'),
    ]

    device_os = 'iosxr'

    pgcheck = oper_check (
                attrvalpairs = attrValPairsToCheck,
                show_command = \
                    ('show_interface_<WORD>', [], {'ifname':'MgmtEth0/0/CPU0/0'}),
                refresh_cache=True,
                device_output = pure_cli,
                device_os = device_os)

    result = pgcheck.parse()
    self.assertTrue(result)
    self.assertEqual(parsergen.ext_dictio['device_name'], self.outputDict2)
def test_tabular_parser(self):

    """
        Test tabular parser when passing a device output
        and device os only.
    """

    pure_cli='''
        Interface              IP-Address      OK? Method Status                Protocol
        GigabitEthernet0/0     10.1.10.20      YES NVRAM  up                    up
        GigabitEthernet1/0/1   unassigned      YES unset  up                    up
        GigabitEthernet1/0/10  unassigned      YES unset  down                  down
    '''
    device_os = 'iosxe'

    res = parsergen.oper_fill_tabular(header_fields=
                                        [ "Interface",
                                          "IP-Address",
                                          "OK\?",
                                          "Method",
                                          "Status",
                                          "Protocol" ],
                                      label_fields=
                                        [ "Interface",
                                          "IP-Address",
                                          "OK?",
                                          "Method",
                                          "Status",
                                          "Protocol" ],
                                      index=[ 0, 5 ],
                                      device_output = pure_cli,
                                      device_os = device_os)

    self.assertEqual(res.entries, outputDict4)

    outputDict4 = {
        'GigabitEthernet0/0':
            {'up':
                {'IP-Address': '10.1.10.20',
                 'Interface': 'GigabitEthernet0/0',
                 'Method': 'NVRAM',
                 'OK?': 'YES',
                 'Protocol': 'up',
                 'Status': 'up'}},
        'GigabitEthernet1/0/1':
            {'up':
                {'IP-Address': 'unassigned',
                 'Interface': 'GigabitEthernet1/0/1',
                 'Method': 'unset',
                 'OK?': 'YES',
                 'Protocol': 'up',
                 'Status': 'up'}},
        'GigabitEthernet1/0/10':
            {'down':
                {'IP-Address': 'unassigned',
                 'Interface': 'GigabitEthernet1/0/10',
                 'Method': 'unset',
                 'OK?': 'YES',
                 'Protocol': 'down',
                 'Status': 'down'}}}

Note

Here is the list of the supported OSes: [ ‘ios’, ‘iosxr’, ‘iosxe’, ‘nxos’, ‘calvados’, ‘iox’,

‘pix’, ‘asa’, ‘sanos’, ‘dcos’, ‘aireos’, ‘linux’ ]