Working with NetDevices

NetDevices is the core of Trigger’s device interaction. Anything that communicates with devices relies on the metadata stored within NetDevice objects.

Your Source Data

Before you can work with device metadata, you must tell Trigger how and from where to read it. You may either modify the values for these options within or you may specify the values as environment variables of the same name as the configuration options.

Please see Configuration and defaults for more information on how to do this. There are two configuration options that facilitate this:

The location of the file containing the metadata. Default: /etc/trigger/netdevices.xml
The format of the metadata file. Default: xml

When you instantiate NetDevices the specified file is read and parsed using the specified format. The currently accepted formats are:

  • JSON
  • Sqlite
  • XML

Except when using RANCID as a data source, the contents of your source data should be a dump of relevant metadata fields from your CMDB.

If you don’t have a CMDB, then you’re going to have to populate this file manually. But you’re a Python programmer, right? So you can come up with something spiffy!

Importing from RANCID

New in version 1.2.

Experimental support for using a RANCID repository to populate your metadata is now working. We say it’s experimental because it is not yet complete. Currently all it does for you is populates the bare minimum set of fields required for basic functionality.

To learn more please visit the section on working with the RANCID format.

Supported Formats


XML is the slowest method supported by Trigger, but it is currently the default for legacy reasons. At some point we will switch JSON to the default.

Here is a sample what the netdevices.xml file bundled with the Trigger source code looks like:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Dummy version of netdevices.xml, with just one real entry modeled from the real file -->
    <device nodeName="">
        <budgetName>Data Center</budgetName>
        <lastUpdate>2010-07-19 19:56:32.0</lastUpdate>
        <make>M40 INTERNET BACKBONE ROUTER</make>
        <onCallName>Data Center</onCallName>
        <owningTeam>Data Center</owningTeam>
        <owner>12345678 - Network Engineering</owner>
        <projectName>Test Lab</projectName>

Please see conf/netdevices.xml within the Trigger source distribution for a full example.


JSON is the fastest method supported by Trigger. This is especially the case if you utilize the optional C extension of simplejson. The file can be minified and does not need to be indented.

Here is a sample of what the netdevices.json file bundled with the Trigger source code looks like (pretty-printed for readabilty):

        "adminStatus": "PRODUCTION",
        "enablePW": "boguspassword",
        "OOBTerminalServerTCPPort": "5005",
        "assetID": "0000012345",
        "OOBTerminalServerNodeName": "ts1",
        "onCallEmail": "",
        "onCallID": "17",
        "OOBTerminalServerFQDN": "",
        "owner": "12345678 - Network Engineering",
        "OOBTerminalServerPort": "5",
        "onCallName": "Data Center",
        "nodeName": "",
        "make": "M40 INTERNET BACKBONE ROUTER",
        "budgetCode": "1234578",
        "budgetName": "Data Center",
        "operationStatus": "MONITORED",
        "deviceType": "ROUTER",
        "lastUpdate": "2010-07-19 19:56:32.0",
        "authMethod": "tacacs",
        "projectName": "Test Lab",
        "barcode": "0101010101",
        "site": "LAB",
        "loginPW": null,
        "lifecycleStatus": "INSTALLED",
        "manufacturer": "JUNIPER",
        "layer3": "1",
        "layer2": "1",
        "room": "CR10",
        "layer4": "1",
        "serialNumber": "987654321",
        "owningTeam": "Data Center",
        "coordinate": "16ZZ",
        "model": "M40-B-AC",
        "OOBTerminalServerConnector": "C"

To use JSON, create your NETDEVICES_FILE full of objects that look like the one above and set NETDEVICES_FORMAT to 'json'.

Please see conf/netdevices.json within the Trigger source distribution for a full example.


This is the easiest method to get running assuming you’ve already got a RANCID instance to leverage. At this time, however, the metadata available from RANCID is very limited and populates only the following fields for each Netdevice object:

nodeName:The device hostname.
manufacturer:The representative name of the hardware manufacturer. This is also used to dynamically populate the vendor attribute on the device object
vendor:The canonical vendor name used internally by Trigger. This will always be a single, lowercased word, and is automatically set when a device object is created.
deviceType:One of (‘SWITCH’, ‘ROUTER’, ‘FIREWALL’). This is currently a hard-coded value for each manufacturer.
adminStatus:If RANCID says the device is 'up', then this is set to 'PRODUCTION'; otherwise it’s set to 'NON-PRODUCTION'.

The support for RANCID comes in two forms: single or multiple instance.

Single instance is the default and expects to find the router.db file and the configs directory in the root directory you specify.

Multiple instance will instead walk the root directory and expect to find router.db and configs in each subdirectory it finds. Multiple instance can be toggled by seting the value of RANCID_RECURSE_SUBDIRS to True to your

To use RANCID as a data source, set the value of NETDEVICES_FILE in to the absolute path of location of of the root directory where your RANCID data is stored and set the value NETDEVICES_FORMAT to 'rancid'.


Make sure that the value of RANCID_RECURSE_SUBDIRS matches the RANCID method you are using. This setting defaults to False, so if you only have a single RANCID instance, there is no need to add it to your

Lastly, to illustrate what a NetDevice object that has been populated by RANCID looks like, here is the output of .dump():

Owning Org.:       None
Owning Team:       None
OnCall Team:       None

Vendor:            Juniper (juniper)
Make:              None
Model:             None
Type:              ROUTER
Location:          None None None

Project:           None
Serial:            None
Asset Tag:         None
Budget Code:       None (None)

Admin Status:      PRODUCTION
Lifecycle Status:  None
Operation Status:  None
Last Updated:      None

Compare that to what a device dump looks like when fully populated from CMDB metadata in What’s in a NetDevice?. It’s important to keep this in mind, because if you want to do device associations using any of the unpopulated fields, you’re gonna have a bad time. This is subject to change as RANCID support evolves, but this is the way it is for now.


SQLite is somewhere between JSON and XML as far as performance, but also comes with the added benefit that support is built into Python, and you get a real database file you can leverage in other ways outside of Trigger.

-- Table structure for table `netdevices`
-- This is for 'netdevices.sql' SQLite support within
-- trigger.netdevices.NetDevices for storing and tracking network device
-- metadata.
-- This is based on the current set of existing attributes in use and is by no
-- means exclusive. Feel free to add your own fields to suit your environment.

CREATE TABLE netdevices (
    OOBTerminalServerConnector VARCHAR(1024),
    OOBTerminalServerFQDN VARCHAR(1024),
    OOBTerminalServerNodeName VARCHAR(1024),
    OOBTerminalServerPort VARCHAR(1024),
    OOBTerminalServerTCPPort VARCHAR(1024),
    acls VARCHAR(1024),
    adminStatus VARCHAR(1024),
    assetID VARCHAR(1024),
    authMethod VARCHAR(1024),
    barcode VARCHAR(1024),
    budgetCode VARCHAR(1024),
    budgetName VARCHAR(1024),
    bulk_acls VARCHAR(1024),
    connectProtocol VARCHAR(1024),
    coordinate VARCHAR(1024),
    deviceType VARCHAR(1024),
    enablePW VARCHAR(1024),
    explicit_acls VARCHAR(1024),
    gslb_master VARCHAR(1024),
    implicit_acls VARCHAR(1024),
    lastUpdate VARCHAR(1024),
    layer2 VARCHAR(1024),
    layer3 VARCHAR(1024),
    layer4 VARCHAR(1024),
    lifecycleStatus VARCHAR(1024),
    loginPW VARCHAR(1024),
    make VARCHAR(1024),
    manufacturer VARCHAR(1024),
    model VARCHAR(1024),
    nodeName VARCHAR(1024),
    onCallEmail VARCHAR(1024),
    onCallID VARCHAR(1024),
    onCallName VARCHAR(1024),
    operationStatus VARCHAR(1024),
    owner VARCHAR(1024),
    owningTeam VARCHAR(1024),
    projectID VARCHAR(1024),
    projectName VARCHAR(1024),
    room VARCHAR(1024),
    serialNumber VARCHAR(1024),
    site VARCHAR(1024)

To use SQLite, create a database using the schema provided within Trigger source distribution at conf/netdevices.sql. You will need to populate the database full of rows with the columns above and set NETDEVICES_FORMAT to 'sqlite'.

Getting Started

First things first, you must instantiate NetDevices. It has three things it requires before you can properly do this:

  1. The NETDEVICES_FILE file must be readable and must properly parse using the format specified by NETDEVICES_FORMAT (see above);
  2. An instance of Redis.
  3. The path to must be valid, and must properly parse.

How it works

The NetDevices object itself is an immutable, dictionary-like Singleton object. If you don’t know what a Singleton is, it means that there can only be one instance of this object in any program. The actual instance object itself an instance of the inner _actual class which is stored in the module object as NetDevices._Singleton. This is done as a performance boost because many Trigger components require a NetDevices instance, and if we had to keep creating new ones, we’d be waiting each time one had to parse NETDEVICES_FILE all over again.

Upon startup, each device object/element/row found within NETDEVICES_FILE is used to create a NetDevice object. This object pulls in ACL associations from AclsDB.

The Singleton Pattern

The NetDevices module object has a _Singleton attribute that defaults to None. Upon creating an instance, this is populated with the _actual instance:

>>> nd = NetDevices()
>>> nd._Singleton
<trigger.netdevices._actual object at 0x2ae3dcf48710>
>>> NetDevices._Singleton
<trigger.netdevices._actual object at 0x2ae3dcf48710>

This is how new instances are prevented. Whenever you create a new reference by instantiating NetDevices again, what you are really doing is creating a reference to NetDevices._Singleton:

>>> other_nd = NetDevices()
>>> other_nd._Singleton
<trigger.netdevices._actual object at 0x2ae3dcf48710>
>>> nd._Singleton is other_nd._Singleton

The only time this would be an issue is if you needed to change the actual contents of your object (such as when debugging or passing production_only=False). If you need to do this, set the value to None:

>>> NetDevices._Singleton = None

Then the next call to NetDevices() will start from scratch. Keep in mind because of this pattern it is not easy to have more than one instance (there are ways but we’re not going to list them here!). All existing instances will inherit the value of NetDevices._Singleton:

>>> third_nd = NetDevices(production_only=False)
>>> third_nd._Singleton
<trigger.netdevices._actual object at 0x2ae3dcf506d0>
>>> nd._Singleton
<trigger.netdevices._actual object at 0x2ae3dcf506d0>
>>> third_nd._Singleton is nd._Singleton

Instantiating NetDevices

Throughout the Trigger code, the convention when instantiating and referencing a NetDevices instance, is to assign it to the variable nd. All examples will use this, so keep that in mind:

>>> from trigger.netdevices import NetDevices
>>> nd = NetDevices()
>>> len(nd)

By default, this only includes any devices for which adminStatus (aka administrative status) is PRODUCTION. This means that the device is used in your production environment. If you would like you get all devices regardless of adminStatus, you must pass production_only=False to the constructor:

>>> from trigger.netdevices import NetDevices
>>> nd = NetDevices(production_only=False)
>>> len(nd)

The included sample metadata files contains one device that is marked as NON-PRODUCTION.

What’s in a NetDevice?

A NetDevice object has a number of attributes you can use creatively to correlate or identify them:

>>> dev = nd.find('test1-abc')
>>> dev

Printing it displays the hostname:

>>> print dev

You can dump the values:

>>> dev.dump()

        Owning Org.:       12345678 - Network Engineering
        Owning Team:       Data Center
        OnCall Team:       Data Center

        Vendor:            Juniper (JUNIPER)
        Make:              M40 INTERNET BACKBONE ROUTER
        Model:             M40-B-AC
        Type:              ROUTER
        Location:          LAB CR10 16ZZ

        Project:           Test Lab
        Serial:            987654321
        Asset Tag:         0000012345
        Budget Code:       1234578 (Data Center)

        Admin Status:      PRODUCTION
        Lifecycle Status:  INSTALLED
        Operation Status:  MONITORED
        Last Updated:      2010-07-19 19:56:32.0

You can reference them as attributes:

>>> dev.nodeName, dev.vendor, dev.deviceType
('', <Vendor: Juniper>, 'ROUTER')

There are some special methods to perform identity tests:

>>> dev.is_router(), dev.is_switch(), dev.is_firewall()
(True, False, False)

You can view the ACLs assigned to the device:

>>> dev.explicit_acls
>>> dev.implicit_acls
set(['juniper-router.policer', 'juniper-router-protect'])
>>> dev.acls
set(['juniper-router.policer', 'juniper-router-protect', 'abc123'])

Or get the next time it’s ok to make changes to this device (more on this later):

>>> dev.bounce.next_ok('green')
datetime.datetime(2011, 7, 13, 9, 0, tzinfo=<UTC>)
>>> print dev.bounce.status()

Searching for devices

Like a dictionary

Since the object is like a dictionary, you may reference devices as keys by their hostnames:

>>> nd
{'': <NetDevice:>,
 '': <NetDevice:>,
 '': <NetDevice:>,
 '': <NetDevice:>}
>>> nd['']

You may also perform any other operations to iterate devices as you would with a dictionary (.keys(), .itervalues(), etc.).

Special methods

There are a number of ways you can search for devices. In all cases, you are returned a list.

The simplest usage is just to list all devices:

>>> nd.all()
[<NetDevice:>, <NetDevice:>,
 <NetDevice:>, <NetDevice:>]

Using all() is going to be very rare, as you’re more likely to work with a subset of your devices.

Find a device by its shortname (minus the domain):

>>> nd.find('test1-abc')

List devices by type (switches, routers, or firewalls):

>>> nd.list_routers()
[<NetDevice:>, <NetDevice:>]
>>> nd.list_switches()
>>> nd.list_firewalls()

Perform a case-sensitive search on any field (it defaults to nodeName):

[<NetDevice:>, <NetDevice:>]
>>>'NON-PRODUCTION', 'adminStatus')

Or you could just roll your own list comprehension to do the same thing:

>>> [d for d in nd.all() if d.adminStatus == 'NON-PRODUCTION']

Perform a case-INsenstive search on any number of fields as keyword arguments:

>>> nd.match(oncallname='data center', adminstatus='non')
>>> nd.match(vendor='netscreen')

Helper function

Another nifty tool within the module is device_match, which returns a NetDevice object:

>>> from trigger.netdevices import device_match
>>> device_match('test')
2 possible matches found for 'test':
 [ 1]
 [ 2]
 [ 0] Exit

Enter a device number: 2

If there are multiple matches, it presents a prompt and lets you choose, otherwise it chooses for you:

>>> device_match('fw')
Matched ''.