Librenms API example to automatically generate a network graph from lldp information

Following the discussion in Discord from yesterday where user Skylark asked about the links endpoint from the API I said I can share my code I wrote to automatically generate a network graph based on LLDP information.
This of course is “as good” as the coverage of LLDP in your network is. Meaning you need to enable lldp on all your devices possible, down to servers and installing lldpd there, for example.
To use this script you need to have python3-requests and python3-graphviz - these are at least the packages how they are called in Debian.

"""
This code will return a pdf file that represents the map of our network,
automatically generated from lldp data.
"""

import requests
import graphviz


AUTH_TOKEN = "YOURAPIKEY"
DEVICES_API_URL = 'https://your.domain/api/v0/devices'
PORTS_API_URL = 'https://your.domain/api/v0/ports'
LINKS_API_URL = 'https://your.domain/api/v0/resources/links'

request_headers = {
    "X-Auth-Token": AUTH_TOKEN,
}
# get all devices
resp_devices = requests.get(url=DEVICES_API_URL, headers=request_headers)
data_devices = resp_devices.json()
# flatten response
devices = {subdict['device_id']
    : subdict for subdict in data_devices['devices']}


# get all ports
resp_ports = requests.get(url=PORTS_API_URL, headers=request_headers)
data_ports = resp_ports.json()
# flatten response
ports = {subdict['port_id']: subdict for subdict in data_ports['ports']}


# get all links (lldp)
resp_links = requests.get(url=LINKS_API_URL, headers=request_headers)
data_links = resp_links.json()
# flatten response
links = {subdict['id']: subdict for subdict in data_links['links']}


def search_device_id(_id):
    """ return the actual hostname from a given device id """
    hostname = devices[_id]['hostname']
    return hostname


def search_port_id(_id):
    """ return the port id for a given portname """
    port = ports[_id]['ifName']
    return port

f = graphviz.Digraph('G', filename='lldp', format='pdf')
f.attr(rankdir='LR')
f.attr('node', shape='box')

for key, value in links.items():
    if isinstance(value, dict):
        f.edge(search_device_id(
            value['local_device_id']), value['remote_hostname'])
f.view()
3 Likes

I rewrote this a little to fit own my use:
config.ini

[librenms]
api_host  = http://librenms.company.com
api_token = 35ca6b881c2076234ffc327e7b536c1b7

xdp.py

#!/usr/bin/python

"""
This code will return a pdf file that represents the data of our network,
automatically generated from cdp & lldp data.
"""

import requests
import configparser
import graphviz

config = configparser.ConfigParser()
config.read('config.ini')

lnms_api      = config['librenms']['api_host']
lnms_token    = config['librenms']['api_token']

request_headers = {
    "X-Auth-Token": lnms_token,
}

def get_list_from_API(api_path, api_id, api_data):
    """ retrive a flaten list from an API call """
    get_api_list = requests.get(url=lnms_api+'/api/v0/'+api_path, headers=request_headers).json()
    # flatten response
    reply = {subdict[api_id] : subdict for subdict in get_api_list[api_data]}
    return(reply)

def search_device_id(_id):
    """ return the actual sysname from a given device id """
    sysname = devices[_id]['sysName']
    return(sysname)

# get all devices
devices = get_list_from_API('devices','device_id','devices')

# get all links (xdp)
links = get_list_from_API('resources/links','id','links')

draw = graphviz.Digraph('G', filename='lldp', format='pdf')
draw.attr(rankdir='LR')
draw.attr('node', shape='box')

for key, value in links.items():
    if isinstance(value, dict):
        draw.edge(search_device_id(
            value['local_device_id']), value['remote_hostname'])

draw.render()

Changes done:

  • Removed get ports (was not being used, curious what other thing you where using it for)
  • changed f.view to f.render to skip errors due to browser not being setup up for CLI
  • used sysName instead of Hostname (some of my devices have IPs for hostname)
1 Like

Oh I figure forgot to remove get ports, sorry!
I’m rather strict when it comes to interface descriptions. They need to be right because otherwise debugging can get hard when you don’t find the port of a server someone is complaining about. So I wrote a script that compares the interface descriptions with the lldp remote name and if they don’t match the script will tell you which port on which device doesn’t match.
It runs as a cron job every night and writes an email so someone can look after it in the morning to fix it.

I need to get into your notebooks :slight_smile:

I’m off work now but in January I can post that credit l script here, too, if you want.

Hey @Skylark!
Sorry for the delay but here is the script I use to find me the mismatches between lldp neighbors and the actual interface description:

"""
This code will return an output that will tell where an interface description ("alias")
is not equal to what lldp reports which hints at a missing or wrong interface description
that needs to be fixed.
"""

import requests


AUTH_TOKEN = "YOURAPIKEY"
DEVICES_API_URL = 'https://your.domain/api/v0/devices'
PORTS_API_URL = 'https://your.domain/api/v0/ports?columns=ifName%2Cport_id%2CifAlias'
LINKS_API_URL = 'https://your.domain/api/v0/resources/links'

request_headers = {
    "X-Auth-Token": AUTH_TOKEN,
}
# get all devices
resp_devices = requests.get(url=DEVICES_API_URL, headers=request_headers)
data_devices = resp_devices.json()
# flatten response
devices = {subdict['device_id']
    : subdict for subdict in data_devices['devices']}

# get all ports
resp_ports = requests.get(url=PORTS_API_URL, headers=request_headers)
data_ports = resp_ports.json()

# flatten response
ports = {subdict['port_id']: subdict for subdict in data_ports['ports']}

# get all links (lldp)
resp_links = requests.get(url=LINKS_API_URL, headers=request_headers)
data_links = resp_links.json()
# flatten response
links = {subdict['id']: subdict for subdict in data_links['links']}


def search_device_id(_id):
    """ return the actual hostname from a given device id """
    hostname = devices[_id]['hostname']
    return hostname


def search_port_id(_id):
    """ return the port id for a given portname """
    port = ports[_id]['ifName']
    return port


def search_portifDescription(_id):
    """ return the interface description for a given port id """
    portdesc = ports[_id]['ifAlias']
    return portdesc


COUNTER = 0
mismatches = {}
for key, value in links.items():
    if isinstance(value, dict):
        local_port = search_port_id(value['local_port_id'])
        if_alias = search_portifDescription(value['local_port_id'])
        local_device_name = search_device_id(value['local_device_id'])
        remote_hostname = value['remote_hostname']
        #os10-bug or intel-bug - sometimes it does not properly get the llpd neighbor
        if remote_hostname == "Not Advertised":
            continue
        #this is the *actual* check if the names match
        #also support partial matches (e.g. bad people don't use fqdn...)
        if not remote_hostname in if_alias:
            try:
                mismatches[local_device_name].append((local_port,remote_hostname))
            except KeyError:
                mismatches[local_device_name] = [(local_port,remote_hostname)]
            COUNTER += 1
print("Stats:", COUNTER, "misconfigurations found.")
for key in mismatches:
    #print (key, '->', mismatches[key])
    print ('\nssh', key)
    for data in mismatches[key]:
        print("conf t")
        print("interface", data[0])
        print("description", data[1])
        print("end")

Perhaps it is useful to you.