Custom Plugins

netsim-tools support dynamically loadable plugins allowing you to implement custom data model transformations without adding nerd knobs to the core topology transformation.

Warning

This is an experimental feature. A few commonly-used functions are defined in netsim.api; performing operations beyond simple data transformation might require digging through the source code. You might want to open a discussion on netsim-tools GitHub repository before proceeding.

Plugins needed by a topology file are listed in the plugin top-level element, for example:

plugin: [ bgp-anycast ]

module: [ ospf, bgp ]
...

Plugins are either Python files or directories containing Python code plus configuration templates. They are loaded from the current directory or netsim/extra directory.

For simple plugins, the plugin name specifies the file name (without the .py extension). For plugin packages, the plugin name specifies the directory with plugin.py Python module and one or more Jinja2 templates (one per supported ansible_network_os type).

Plugins can define well-known functions that are invoked during the topology transformation process which includes these steps:

  • execute plugin init function

  • check topology top-level elements

  • adjust global parameters (defaults), node list, link list, and address pools

  • execute plugin pre_transform function

  • execute module pre_transform function

  • adjust groups (including setting node data from node_data)

  • execute plugin pre_node_transform function

  • transform node data

  • execute plugin post_node_transform function

  • execute plugin pre_link_transform function

  • transform link data

  • execute plugin post_link_transform function

  • execute module post_transform function

  • execute plugin post_transform function

Every plugin function is called with a single topology argument: the current topology data structure. The node- or link-manipulation functions must iterate over topology.nodes or topology.links lists.

Plugins extending configuration modules might have to define additional module attributes. The module attribute lists have to be extended in the init function before any module validation code is executed.

Example

All anycast servers in a BGP anycast topology should have the same AS number, but do not need IBGP sessions between themselves. A custom plugin deletes IBGP sessions for any node with bgp.anycast attribute.

This is the topology file used in BGP anycast example. It uses node_data attribute on a BGP AS group to set bgp.anycast node attribute on any node in AS 65101

plugin: [ bgp-anycast ]

module: [ ospf, bgp ]

defaults:
  device: iosv

bgp:
  as_list:
    65000:
      members: [ l1, l2, l3, s1 ]
      rr: [ s1 ]
    65101:
      members: [ a1,a2,a3 ]

groups:
  as65101:
    node_data:
      bgp.anycast: 10.42.42.42/32

nodes: 
  [ l1, l2, l3, s1, a1, a2, a3 ]

links: [ s1-l1, s1-l2, s1-l3, l2-a1, l2-a2, l3-a3 ]

The plugin imports common netsim module to create error messages, and api module to get common utility functions.

import sys
from box import Box
from netsim import common
from netsim import api

The initialization function adds anycast attribute to bgp node attributes:

def init(topo: Box) -> None:
  topo.defaults.bgp.attributes.node.append('anycast')

The custom transformation is executed as the last step of the topology transformation – the post_transform function removes IBGP neighbors from all nodes with bgp.anycast attribute.

def post_transform(topo: Box) -> None:
...
  for node in topo.nodes:
    if 'bgp' in node:
      if 'anycast' in node.bgp:
        node.bgp.advertise_loopback = False
        node.bgp.neighbors = [
          n for n in node.bgp.neighbors
            if n.type != 'ibgp' ]
...

The post_transform function should also set the config node parameter to deploy custom configuration template that creates additional loopback interface with the anycast IP address.

def post_transform(topo: Box) -> None:
  config_name = api.get_config_name(globals())
  for node in topo.nodes:
    if 'bgp' in node:
      if 'anycast' in node.bgp:
...
        api.node_config(node,config_name)

Notes:

  • api.get_config_name gets the config_name plugin attribute (set to the plugin directory during the plugin initialization process) and reports an error if the plugin calling it isn’t a part of a plugin package.

  • api.node_config appends the specified custom configuration template to the list of node configuration templates. While equivalent to…

    node.config = node.get('config',[]).append(template)

    … the utility function handles edge cases like missing config attribute or duplicate configuration templates.