LLDP neighbor names (normally truncated output)

Streamlining output of native SR OS commands by overriding them as aliases

General description

This script solves a common problem where the column width of an SR OS show command output is too small, resulting in a truncated output. One command where this truncation might happen is the SR OS show command that displays the LLDP neighbor list.

Elements used in this tutorial

Element Version
CentOS Workstation/Server CentOS Linux release 7.9.2009 (Core)
SR OS device running in Model-Driven mode 22.5R2
Python (on the CentOS machine) 3.6.8

This is a demo quality pySROS script that does not have official support from Nokia. It is verified to work for both local and remote execution on an SR OS box.

Output example

Consider the following SR OS show command for LLDP neighbors:

[/]
A:admin@sros3# show system lldp neighbor
Link Layer Discovery Protocol (LLDP) System Information

===============================================================================
NB = nearest-bridge   NTPMR = nearest-non-tpmr   NC = nearest-customer
===============================================================================
Lcl Port      Scope Remote Chassis ID  Index  Remote Port     Remote Sys Name
-------------------------------------------------------------------------------
1/1/c2/1      NB    52:54:00:05:AE:00  1      1/1/c2/1, 10-G* sros2
1/1/c1/1      NB    52:54:00:CB:87:00  2      1/1/c2/1, 10-G* sros1
1/1/c5/1      NB    52:54:00:B6:C2:00  3      1/1/c1/1, 10-G* sros6
1/1/c4/1      NB    52:54:00:4D:FA:00  4      1/1/c1/1, 10-G* sros5
1/1/c3/1      NB    52:54:00:7D:5F:00  5      1/1/c3/1, 10-G* sros4
===============================================================================
* indicates that the corresponding row element may have been truncated.
Number of neighbors : 5

[/]

In the above example, the "Remote Port" column values are truncated.

The following example shows enhanced output from a pySROS script. The column widths are dynamically set based on data found in the router's state information. The "Remote Port" column is wide enough to present all the data without truncation. As an added benefit, emojis can be woven into the output as shown:

[/]
A:admin@sros3# pyexec cf3:/lldp_neighbor.py

=========================================================================================
NB = nearest-bridge   NTPMR = nearest-non-tpmr   NC = nearest-customer  EMOJIs : 😀 💩 🤠
=========================================================================================
Lcl Port      Scope Remote Chassis ID  Index  Remote Port                Remote Sys Name 
1/1/c1/1      NB    52:54:00:CB:87:00  2      1/1/c2/1, 10-Gig Ethernet  sros1
1/1/c2/1      NB    52:54:00:05:AE:00  1      1/1/c2/1, 10-Gig Ethernet  sros2
1/1/c3/1      NB    52:54:00:7D:5F:00  5      1/1/c3/1, 10-Gig Ethernet  sros4
1/1/c4/1      NB    52:54:00:4D:FA:00  4      1/1/c1/1, 10-Gig Ethernet  sros5
1/1/c5/1      NB    52:54:00:B6:C2:00  3      1/1/c1/1, 10-Gig Ethernet  sros6
=========================================================================================
Number of neighbors : 5

[/]

But what if the "Remote Sys Name" column is also truncated? This situation is shown in the following output:

[/]
A:admin@sros3# show system lldp neighbor
Link Layer Discovery Protocol (LLDP) System Information

===============================================================================
NB = nearest-bridge   NTPMR = nearest-non-tpmr   NC = nearest-customer
===============================================================================
Lcl Port      Scope Remote Chassis ID  Index  Remote Port     Remote Sys Name
-------------------------------------------------------------------------------
1/1/c2/1      NB    52:54:00:05:AE:00  1      1/1/c2/1, 10-G* sros2
1/1/c1/1      NB    52:54:00:CB:87:00  2      1/1/c2/1, 10-G* sros1
1/1/c5/1      NB    52:54:00:B6:C2:00  3      1/1/c1/1, 10-G* sros6
1/1/c4/1      NB    52:54:00:4D:FA:00  4      1/1/c1/1, 10-G* sros5
1/1/c3/1      NB    52:54:00:7D:5F:00  5      1/1/c3/1, 10-G* thisIsAVeryLong*
===============================================================================
* indicates that the corresponding row element may have been truncated.
Number of neighbors : 5

[/]

Because programming generally aims to reduce problems to smaller chunks that are more easily solved and then extrapolate that solution to the larger problem, the pySROS script-based command output dynamically increases the column widths to properly present the data with no truncation (and again, emojis!):

[/]
A:admin@sros3# pyexec cf3:/lldp_neighbor.py

===========================================================================================================
NB = nearest-bridge   NTPMR = nearest-non-tpmr   NC = nearest-customer  EMOJIs : 😀 💩 🤠
===========================================================================================================
Lcl Port      Scope Remote Chassis ID  Index  Remote Port                Remote Sys Name                   
1/1/c1/1      NB    52:54:00:CB:87:00  2      1/1/c2/1, 10-Gig Ethernet  sros1                  
1/1/c2/1      NB    52:54:00:05:AE:00  1      1/1/c2/1, 10-Gig Ethernet  sros2                  
1/1/c3/1      NB    52:54:00:7D:5F:00  5      1/1/c3/1, 10-Gig Ethernet  thisIsAVeryLongSystemNameForSros4
1/1/c4/1      NB    52:54:00:4D:FA:00  4      1/1/c1/1, 10-Gig Ethernet  sros5                  
1/1/c5/1      NB    52:54:00:B6:C2:00  3      1/1/c1/1, 10-Gig Ethernet  sros6                  
===========================================================================================================

[/]

Actual code: lldp_neighbor.py

In this section, you'll find the complete working code. It isn't as complex as some of the other use cases shown in this repository and is therefore described only in this section. A pySROS script must set up a connection to a device before it can perform any interactions with the device. The generally accepted way of establishing a connection uses a get_connection function, so this script implements one.

A translating dictionary is added to include abbreviations of LLDP types in their equivalent "Scope" column notation. A print_table function is implemented that uses the data provided to it to determine how wide each of the columns should be, respecting the minimum width required to still properly show the column headers. Some code is included here that shows how to change the color of text in the output and how to include emojis.

The last function implemented is named lldp_check with a helper function find_lldp_ports. The helper function checks the router's configuration to find those ports that might have LLDP neighbors. Connector ports (1/1/c1) would not be expected to have LLDP information associated with them, for example. Any port that has LLDP configured is added to a dictionary to be checked; this dictionary is returned by the helper.

Based on information available in the state model and entries in this dictionary, lldp_check finds the data that feeds into the function call to print_table. In the main function, this is all tied together to produce the final result in the CLI output shown above.

#!/usr/bin/env python3

# ## lldp_neighbor.py
#   Copyright 2022 Nokia
# ##

# pylint: disable=consider-using-f-string

"""Re-implementation of "show system lldp neighbor" intended to improve
    upon existing SR OS command and remove some of the known limitations
Tested on: SR OS 22.5.R2
"""


import sys
from pysros.management import connect
from pysros.exceptions import ModelProcessingError


translate_lldp_type = {
    "nearest-bridge": "NB",
}


def get_connection():
    """Function definition to obtain a Connection object to a specific
    SR OS device and access the model-driven information.
    :parameter host: The hostname or IP address of the SR OS node.
    :type host: str
    :paramater credentials: The username and password to connect
                            to the SR OS node.
    :type credentials: dict
    :parameter port: The TCP port for the connection to the SR OS node.
    :type port: int
    :returns: Connection object for the SR OS node.
    :rtype: :py:class:`pysros.management.Connection`
    """
    try:
        connection_object = connect(
            host="10.0.1.13", username="admin", password="admin"
        )
    except RuntimeError as error1:
        print("Failed to connect.  Error:", error1)
        sys.exit(-1)
    except ModelProcessingError as error2:
        print("Failed to create model-driven schema.  Error:", error2)
        sys.exit(-2)
    return connection_object


def print_table(rows, col_min_widths):
    """Function definition to print a table in the form of a list
    of rows with information regarding minimal column width to
    the text console of an SR OS device or a remote operator's
    terminal.
    :parameter rows: the data formatted as rows
    :type rows: list
    :paramater col_min_widths:  a list containing integer data
                                corresponding to a minimum width
                                for each column.
    :type col_min_widths: list
    """

    red = "\u001b[31;1m"
    green = "\u001b[32;1m"
    yellow = "\u001b[33;1m"
    brown = "\u001b[34;1m"
    cyan = "\u001b[35;1m"
    reset_color = "\u001b[0m"
    extra_text = (
        green
        + "EMOJIs : "
        + reset_color
        + yellow
        + "\U0001f600"
        + reset_color
        + " "
        + brown
        + "\U0001f4a9"
        + reset_color
        + " "
        + red
        + "\U0001F920"
        + reset_color
    )

    cols = [
        (
            6
            + (
                col_min_widths[0]
                if col_min_widths[0] > len("Lcl Port")
                else len("Lcl Port")
            ),
            "Lcl Port",
        ),
        (
            1
            + (
                col_min_widths[1]
                if col_min_widths[1] > len("Scope")
                else len("Scope")
            ),
            "Scope",
        ),
        (
            2
            + (
                col_min_widths[2]
                if col_min_widths[2] > len("Remote Chassis ID")
                else len("Remote Chassis ID")
            ),
            "Remote Chassis ID",
        ),
        (
            2
            + (
                col_min_widths[3]
                if col_min_widths[3] > len("Index")
                else len("Index")
            ),
            "Index",
        ),
        (
            2
            + (
                col_min_widths[4]
                if col_min_widths[4] > len("Remote Port")
                else len("Remote Port")
            ),
            "Remote Port",
        ),
        (
            1
            + (
                col_min_widths[5]
                if col_min_widths[5] > len("Remote Sys Name")
                else len("Remote Sys Name")
            ),
            "Remote Sys Name",
        ),
    ]

    # Initalize the Table object with the heading and columns.
    # table = Table("Interfaces modified by script (up -> down) ", cols)
    print(sum((x[0] for x in cols)) * "=")
    print(
        "NB = nearest-bridge   NTPMR = nearest-non-tpmr   "
        "NC = nearest-customer  " + extra_text
    )
    print(sum((x[0] for x in cols)) * "=")
    print(
        "%-*s%-*s%-*s%-*s%-*s%-*s"
        % (
            cols[0][0],
            "Lcl Port",
            cols[1][0],
            "Scope",
            cols[2][0],
            "Remote Chassis ID",
            cols[3][0],
            "Index",
            cols[4][0],
            "Remote Port",
            cols[5][0],
            "Remote Sys Name",
        )
    )
    for row in rows:
        print(
            "%-*s%-*s%-*s%-*s%-*s%-*s"
            % (
                cols[0][0],
                row[0],
                cols[1][0],
                row[1],
                cols[2][0],
                row[2],
                cols[3][0],
                row[3],
                cols[4][0],
                row[4],
                cols[5][0],
                cyan + row[5] + reset_color,
            )
        )
    print(sum((x[0] for x in cols)) * "=")
    print("Number of neighbors : %s" % len(rows))


def find_lldp_ports(conn):
    """Function that collects information about ports that are
    or are not configured with LLDP and, if they are, which types.

    These ports are returned along with the configured LLDP types found.

    :parameter conn: connection to an SR OS device
    :type conn: .Connection
    :returns: Dictionary mapping port to configured LLDP types
    :rtype: :py:class:`dict`
    """
    configured_ports = conn.running.get(
        "/nokia-conf:configure/port", defaults=True
    )
    lldp_config_types_per_port = {}
    for port_key, config in configured_ports.items():
        card, mda, port_id = port_key.split("/", 2)
        if "c" not in port_id or "/" in port_id:
            if "ethernet" not in config or "lldp" not in config["ethernet"]:
                # means it's all defaults
                # or there is no LLDP set on the port
                continue
            lldp_config_types_per_port[
                card + "/" + mda + "/" + port_id
            ] = config["ethernet"]["lldp"]["dest-mac"].keys()
        else:
            # host port case
            pass
    return lldp_config_types_per_port


def check_lldp(connection):
    """Function uses find_lldp_ports to see which parts of the state model
    should be consulted for the necessary information to re-build the table
    created by the native "show system lldp neighbor" command.

    This information is returned as the first element of the tuple,
    the second element is a list containing minimal widths to be used
    to be able to show all the values properly, based on the values
    intended to be within those cells.

    :parameter connection: connection to an SR OS device
    :type connection: .Connection
    :returns: tuple of result and min_len
    :rtype: :py:class:`tuple`
    """
    # ordered list of tuples?
    result = []
    min_len = [0] * 6

    for port, lldp_types in find_lldp_ports(connection).items():
        for lldp_type in lldp_types:
            lldp_state_info = connection.running.get(
                '/nokia-state:state/port[port-id="%s"]/ethernet/lldp/dest-mac[mac-type="%s"]'
                % (port, lldp_type)
            )
            if "remote-system" not in lldp_state_info:
                continue
            remote_systems = lldp_state_info["remote-system"]
            for index, remote_system in remote_systems.items():
                new_tuple = (
                    port,
                    translate_lldp_type[lldp_type],
                    remote_system["chassis-id"].data,
                    str(index[1]),
                    remote_system["port-description"].data,
                    remote_system["system-name"].data,
                )
                result.append(new_tuple)
                for i in enumerate(new_tuple):
                    if len(i[1]) > min_len[i[0]]:
                        min_len[i[0]] = len(i[1])
    return result, min_len


if __name__ == "__main__":
    sr = get_connection()
    results, col_widths = check_lldp(sr)
    print_table(results, col_widths)

On this page