Netbox Custom Links for Snipe-IT and LibreNMS

Table of contents

  1. Netbox Custom links
  2. Prepare Apache web server to run CGI/Python Scripts
  3. NetBox/Snipe-IT Python script for custom links
  4. NetBox/LibreNMS Python script for custom links

In one of my last blog posts I showed how to create Custom links in NetBox for Snipe-IT Asset Management. In this blog post I’m expanding the Custom Links with some additional functionality. This time, the code is written in Python and executed as CGI on the NetBox server (instead of the Snipe-IT server).

Instead of running a script on each application server, I think a better approach is to run scripts “centrally” on the NetBox server itself. This eases also the development and handling of scripts.

Prepare Apache web server to run CGI/Python Scripts

To prepare the Apache2 web server, the CGI module needs to be activated and configured. For handling CGI executeables, I am adding after the NetBox configuration part in /etc/apache2/sites-enabled/netbox.conf the following lines:


    <VirtualHost *:443>
      ...
      NETBOX-CONFIG
      ...
      Alias /cgi /var/www/cgi-bin
      <Directory /var/www/cgi-bin>
        AllowOverride None
        Options None
        Options +ExecCGI
        AddHandler cgi-script .py
        Require all granted
      </Directory>
      <Location /cgi>
        ProxyPass !
      </Location>
    </VirtualHost>

Please note that a ProxyPass directive is required for /cgi, otherwise the script is executed by Gunicorn WSGI server, which uses the NetBox web application.

To add the CGI module mod_cgid, I’m using the a2enmod command. Afterwards I’m restarting the web server with the CGI module and configuration activated:


    #a2enmod cgid
    #/etc/init.d/apache2 restart

The script in Python works similar like the PHP-script in my other blog post. However, I am checking in this script if a parameter device or location is in the GET variable passed. If device is passed, then Snipe-IT shows the device based on it’s hardware ID. If location is passed in the GET request, then Snipe-IT shows the location and the hardware assigned to the location based on the ID.

If something goes wrong, for example a site exists in NetBox but no corresponding location exists in Snipe-IT, then all locations in Snipe-IT are listed. For the device/hardware, I do a similar “error handling”.

If something really goes wrong, like no parameter as GET variable is passed, then the script just returns the default page of Snipe-IT.


    #!/usr/bin/python3
    # This file is copyright under the latest version of the EUPL.
    # Please see LICENSE file for your rights under this license.

    import requests
    import cgi
    import cgitb
    import json

    #
    # Snipe-IT HOST and API token
    #
    snipeit_host = 'https://SNIPE-IT_HOST'
    token = 'Bearer SNIPE-IT_API_TOKEN'

    #
    # get form data
    #
    cgitb.enable()
    form = cgi.FieldStorage()

    #
    # handle devices
    #
    if "device" in form:
      device = form.getvalue('device')
      url = snipeit_host + '/api/v1/hardware?limit=1&offset=0&search='
      headers = {'authorization': token}
      r = requests.get(url + device, headers = headers)

      #
      # if status code is 200, then redirect to hardware
      #
      if r.status_code == requests.codes.ok:
        #
        # Snipe-IT must return something like an ID
        # If there is no ID, then just list all hardware
        #
        try:
          data = r.json()
          device_id = str(data['rows'][0]['id'])
          print ('Location: ' + snipeit_host + '/hardware/' + device_id + '\n')
        except IndexError:
          print ('Location: ' + snipeit_host + '/hardware/' + '\n')
      else:
        print ('Location: ' + snipeit_host +'\n')
      #
      # handle locations
      #
      elif "location" in form:
        location = form.getvalue('location')
        url = snipeit_host + '/api/v1/locations?limit=1&offset=0&search='
        headers = {'authorization': token}
        r = requests.get(url + location, headers = headers)

        #
        # if status code is 200, then redirect to location
        #
        if r.status_code == requests.codes.ok:
          #
          # Snipe-IT must return something like an ID
          # If there is no ID, then just list all locations
          #
          try:
            data = r.json()
            location_id = str(data['rows'][0]['id'])
            print ('Location: ' + snipeit_host + '/locations/' + location_id + '\n')
          except IndexError:
            print ('Location: ' + snipeit_host + '/locations/' +'\n')
        else:
          print ('Location: ' + snipeit_host +'\n')
    #
    # handle all other errors by just forwarding to the Snipe-IT host
    #
    else:
      print ('Location: ' + snipeit_host +'\n')

To use the Custom Link in NetBox, I’m setting those up in the NetBox Administration/Extras/Custom Links with the following configuration:


    Content type:   dcim > device
    Name:           Snipe-IT Asset Management: Device
    Group name:     Internal links
    Weight:         150
    Button class:   Success (green)
    New window:     ticked
    Template text:  Snipe-IT: {{ obj }}
    Template URL:   https://NETBOX_SERVER/cgi/netbox-snipeit.py?device={{ obj }}


    Content type:   dcim > site
    Name:           Snipe-IT Asset Management: Site
    Group name:     Internal links
    Weight:         150
    Button class:   Success (green)
    New window:     ticked
    Template text:  Snipe-IT: {{ obj }}
    Template URL:   https://NETBOX_SERVER/cgi/netbox-snipeit.py?location={{ obj }}

The code for the Custom Links script is also available at: netbox-snipeit.py

Because my devices are organized by a location identifier as device groups in LibreNMS, I’m able to “link” with a Python script from the NetBox Sites to the device groups in LibreNMS.

In this case I’m passing a devicegroup parameter as GET variable to the script, which looks up the corresponding group ID in LibreNMS.


    #!/usr/bin/python3
    # This file is copyright under the latest version of the EUPL.
    # Please see LICENSE file for your rights under this license.
    import requests
    import cgi
    import cgitb
    import json

    #
    # LibreNMS HOST and API token
    #
    librenms_host = 'https://LIBRENMS_HOST'
    token = 'LIBRENMS_TOKEN'

    #
    # get form data
    #
    cgitb.enable()
    form = cgi.FieldStorage()

    #
    # handle devices
    #
    if "devicegroup" in form:
      devicegroup = form.getvalue('devicegroup')
      url = librenms_host + '/api/v0/devicegroups'
      headers = {'X-Auth-Token': token}
      r = requests.get(url, headers = headers)

      #
      # if status code is 200, then redirect to device group
      #
      if r.status_code == requests.codes.ok:
        #
        # LibreNMS must return something like an ID
        # If there is no ID, then just list all devices
        #
        try:
          data = r.json()
          #
          # LibreNMS API returns a count of device groups
          #
          rows = data['count']

            #
            # Look up ID by group name
            #
            for i in range(0, rows):
              group_name = str(data['groups'][i]['name'])

              #
              # if group_name matches the passed devicegroup,
              # then get the corresponding ID
              #
              if group_name == devicegroup:
                group_id = str(data['groups'][i]['id'])
                print ('Location: ' + librenms_host + '/devices/group=' + group_id + '\n')
              else:
                print ('Location: ' + librenms_host + '/devices/' + '\n')
        except IndexError:
          print ('Location: ' + librenms_host + '/devices/' + '\n')
      else:
        print ('Location: ' + librenms_host +'\n')

    #
    # handle all other errors by just forwarding to the Snipe-IT host
    #
    else:
      print ('Location: ' + librenms_host +'\n')

In NetBox, I’m creating for the Custom Link the following configuration:


    Content type:   dcim > site
    Name:           LibreNMS: Device groups
    Group name:     Internal links
    Weight:         150
    Button class:   Success (green)
    New window:     ticked
    Template text:  LibreNMS: {{ obj }}
    Template URL:   https://NETBOX_SERVER/cgi/netbox-librenms.py?devicegroup={{ obj }}

The code for the Custom Links script is also available at: netbox-librenms.py