Use Python and SNMP (LLDP) to find the neighbors of the switch

introduce

About SNMP

SNMP stands for simple network management protocol. This is a way for servers to share information about their current status and a channel for administrators to modify predefined values. SNMP is a protocol implemented on the application layer of the network stack (click here to learn about the network layer). The protocol was created to collect information from very different systems in a consistent manner.

You can read more about SNMP, OID, and SNMP methods in the links above. To summarize, the script uses:

  • snmp version 2c
  • snmp walk is the main method to obtain data from snmp
  • The data obtained from the device is obtained from a specific OID (more lldp OIDs can be found in here (found)

About LLDP

Link Layer Discovery Protocol(   LLDP  ) It is a vendor neutral layer 2 protocol. Sites connected to a specific LAN segment can use this protocol to announce their identity and functions, and receive the same information from physically adjacent layer 2 peers.

Using python to combine SNMP and LLDP

The purpose of my program is to return the neighbor data (local and remote ports + neighbor name) of all switches in the file by using Python 3.6 and providing a switch data file (community string, snmp port and switch ip).

Sample profile:

community_string1, snmp_port1, ip1
community_string2, snmp_port2, ip2
community_string3, snmp_port3, ip3

Sample output:

[
    {
        "name1": {
            "ip": "ip1",
            "neighbours": [
                {
                    "neighbour_name1": "neighbour_name1",
                    "local_port1": "local_port1",
                    "remote_port1": "remote_port1"
                },
                {
                    "neighbour_name2": "neighbour_name2",
                    "local_port2": "local_port2",
                    "remote_port2": "remote_port2"
                },
                {
                    "neighbour_name3": "neighbour_name3",
                    "local_port3": "local_port3",
                    "remote_port3": "remote_port3"
                },
            ]
        },
        "name2":  {data here},
        "name3":  {data here},
    }
]

Interpretation output

  • name1 represents the switch name in the first line of the configuration file (retrieve PARENT_NAME_OID by executing snmp walk for)
  • ip1   The first line from the configuration file represents the IP of the switch (this is obtained from the configuration file)
  • Neighbors are retrieved through snmp using a specific OID (see the following code).

I think this JSON output format is the most relevant, but if you have a better idea, I'd like to hear it.

code

Now, the code is a little messy, but it uses pysnmp You can use pip   It receives the configuration file as a CLI parameter, parses it, and processes the information in it.

import argparse
import itertools
import os
import pprint

from pysnmp.hlapi import *

NEIGHBOUR_PORT_OID = '1.0.8802.1.1.2.1.4.1.1.8.0'
NEIGHBOUR_NAME_OID = '1.0.8802.1.1.2.1.4.1.1.9'
PARENT_NAME_OID = '1.0.8802.1.1.2.1.3.3'


class MissingOidParameter(Exception):
    """
    Custom exception used when the OID is missing.
    """
    pass


def is_file_valid(filepath):
    """
    Check if a file exists or not.

    Args:
        filepath (str): Path to the switches file
    Returns:
        filepath or raise exception if invalid
    """

    if not os.path.exists(filepath):
        raise ValueError('Invalid filepath')
    return filepath


def get_cli_arguments():
    """
    Simple command line parser function.
    """

    parser = argparse.ArgumentParser(description="")
    parser.add_argument(
        '-f',
        '--file',
        type=is_file_valid,
        help='Path to the switches file'
    )
    return parser


def get_switches_from_file():
    """Return data as a list from a file.

    The file format is the following:

    community_string1, snmp_port1, ip1
    community_string2, snmp_port2, ip2
    community_string3, snmp_port3, ip3

    The output:

    [
        {"community": "community_string1", "snmp_port": "snmp_port1", "ip": "ip1"},
        {"community": "community_string2", "snmp_port": "snmp_port2", "ip": "ip2"},
        {"community": "community_string3", "snmp_port": "snmp_port3", "ip": "ip3"},
    ]
    """

    args = get_cli_arguments().parse_args()
    switches_info = []
    with open(args.file) as switches_info_fp:
        for line in switches_info_fp:
            line = line.rstrip().split(',')
            switches_info.append({
                'community': line[0].strip(),
                'snmp_port': line[1].strip(),
                'ip': line[2].strip(),
            })
    return switches_info


def parse_neighbours_ports_result(result):
    """
    One line of result looks like this:

    result = iso.0.8802.1.1.2.1.4.1.1.8.0.2.3 = 2

    Where the last "2" from the OID is the local port and the value
    after '=' is the remote port (2)
    """
    if not result:
        raise MissingOidParameter('No OID provided.')

    value = result.split(' = ')
    if not value:
        return 'Missing entire value for OID={}'.format(result)
    else:
        oid, port = value
        local_port = re.search(r'{}\.(\d+)'.format(NEIGHBOUR_PORT_OID[2:]), oid).group(1)

        if port:
            remote_port = re.search(r'(\d+)', port).group(1)
        else:
            remote_port = 'Unknown'

    return 'local_port', local_port, 'remote_port', remote_port


def parse_parent_name(result):
    """
    One line of result looks like this:

    result = iso.0.8802.1.1.2.1.3.3.0 = Switch01

    The name of the parent is "Switch01"
    """

    if not result:
        raise MissingOidParameter('No OID provided.')

    value = result.split(' = ')
    if not value:
        return 'Missing entire value for OID={}'.format(result)
    else:
        return 'Unknown' if not value[-1] else value[-1]


def parse_neighbour_names_results(result):
    """
    One line of result looks like this:

    result = iso.0.8802.1.1.2.1.4.1.1.9.0.2.3 = HP-2920-24G

    The name of the parent is "Switch01"
    """

    if not result:
        raise MissingOidParameter('No OID provided.')

    value = result.split(' = ')
    if not value:
        return 'Missing entire value for OID={}'.format(result)
    else:
        return ('name', 'Unknown') if not value[-1] else ('name', value[-1])


def main():
    data = {}
    switches_filedata = get_switches_from_file()

    for switch in switches_filedata:
        community = switch.get('community')
        snmp_port = switch.get('snmp_port')
        ip = switch.get('ip')

        name = ''
        for (error_indication, error_status, error_index, var_binds) in nextCmd(
                SnmpEngine(),
                CommunityData(community),
                UdpTransportTarget((ip, snmp_port)),
                ContextData(),
                ObjectType(ObjectIdentity(PARENT_NAME_OID)),
                lexicographicMode=False
        ):
            # this should always return one result
            name = parse_parent_name(str(var_binds[0]))

        if not name:
            print('Could not retrieve name of switch. Moving to the next one...')
            continue

        neighbour_names = []
        neighbour_local_remote_ports = []

        for (error_indication, error_status, error_index, var_binds) in nextCmd(
                SnmpEngine(),
                CommunityData(community),
                UdpTransportTarget((ip, snmp_port)),
                ContextData(),
                ObjectType(ObjectIdentity(NEIGHBOUR_NAME_OID)),
                lexicographicMode=False
        ):
            for var_bind in var_binds:
                neighbour_names.append(
                    parse_neighbour_names_results(str(var_bind))
                )

        for (error_indication, error_status, error_index, var_binds) in nextCmd(
                SnmpEngine(),
                CommunityData(community),
                UdpTransportTarget((ip, snmp_port)),
                ContextData(),
                ObjectType(ObjectIdentity(NEIGHBOUR_PORT_OID)),
                lexicographicMode=False
        ):
            for var_bind in var_binds:
                neighbour_local_remote_ports.append(
                    parse_neighbours_ports_result(str(var_bind))
                )

        neighbours = []
        for a, b in itertools.zip_longest(
                neighbour_names,
                neighbour_local_remote_ports,
                fillvalue='unknown'
        ):
            neighbours.append({
                a[0]: a[1],
                b[0]: b[1],
                b[2]: b[3]
            })

        data[name] = {
            'ip': ip,
            'neighbors': neighbours
        }

    return data


if __name__ == '__main__':
    all_data = main()
    pprint.pprint(all_data, indent=4)

snmp-lldp.py -f snmp-lldp.txt

Tags: PHP Back-end

Posted on Wed, 17 Nov 2021 00:12:21 -0500 by [n00b]