Introducing netlab Plugins

Remember the BGP anycast lab I described in December 2021? In that blog post I briefly mentioned a problem of extraneous IBGP sessions and promised to address it at a later date. Let’s see how we can fix that with a netlab plugin.

We always knew that it’s impossible to implement every nerd knob someone would like to have when building their labs, and extending the tool with Python plugins seemed like the only sane way to go. We added custom plugins to netlab in late 2021, but I didn’t want to write about them because we had to optimize the internal data structures first.

Even though you don’t need to know anything about the internal netlab functions to write plugins, your code has to modify the lab topology data model, and we had to get that stabilized, which we hopefully managed to do over the New Year break.

Back to the original challenge. In the BGP anycast lab I wanted to have BGP sessions set up like this:

Desired BGP sessions

Desired BGP sessions

However, netlab tries to do the right thing and creates IBGP full mesh within an AS1. The configured BGP sessions within my lab looked like this:

Actual BGP sessions

Actual BGP sessions

The IBGP sessions within AS 65101 were never established as the loopback addresses of A1…A3 were not advertised into BGP and thus the anycast routers had no way to reach each other (there are no direct links between them). The lab worked as expected, but I still didn’t like the results.

BGP sessions on one of the anycast routers
a1#show ip bgp summary | begin Neighbor
Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd        4        65101       0       0        1    0    0 never    Idle        4        65101       0       0        1    0    0 never    Idle       4        65000      30      25        7    0    0 00:20:05        4

I could see three solutions to that conundrum:

  • Remain unhappy with the way the lab works and move on. Not an option.
  • Implement a bgp.peering.ibgp (or something similar) nerd knob to enable/disable IBGP sessions. Doable, but we’d have to change all device configuration templates and forever keep track of which devices support this nerd knob. Sounds like a lot of technical debt to solve an edge case.
  • Write a plugin that removes unneeded IBGP sessions once the lab topology transformation is complete.

The plugin turned out to be remarkably simple. I imported Python Box2 and netlab API modules3.

from box import Box
from netsim import api

Next, I added anycast attribute to the list of allowed BGP node attributes in the plugin initialization code. I could do that in the topology file, but I wanted to end with a self-contained module (more about that in a week or so).

Add a custom BGP attribute to topology defaults
def init(topo: Box) -> None:
  topo.defaults.bgp.attributes.node.anycast = { 'type': 'ipv4', 'use': 'prefix' }
  • Python Box module makes deeply nested dictionaries a pleasure to work with – imagine having to do the same with traditional hodgepodge of square brackets and quotes.
  • BGP anycast attribute is explained in the Building a BGP Anycast Lab blog post.

Finally, I wanted to modify the lists of BGP neighbors once the topology transformation has been complete. post_transform seemed the perfect hook to use, and all I had to do was to:

  • Find the nodes with bgp.anycast attribute
  • Set bgp.advertise_loopback to False (so I can further simplify the topology file)
  • Keep only those BGP neighbors where the BGP session type is not ibgp
Adjust the BGP attributes and BGP neighbors for nodes with bgp.anycast attribute
def post_transform(topo: Box) -> None:
  for name,node in topo.nodes.items():
    if 'anycast' in node.get('bgp',{}):
      node.bgp.advertise_loopback = False
      node.bgp.neighbors = [ n for n in node.bgp.neighbors if n.type != 'ibgp' ]

Let’s unpack that code:

  • All plugin hooks are called with a single parameter – current lab topology. When the init hook is called, you’ll be working with the original topology definition, at the post_transform stage the data model has been extensively modified.
  • Lab devices are described in the nodes dictionary with the lab topology.
  • Each node is a dictionary that contains numerous parameters, including configuration module settings (another dictionary).
  • Python Box module is a wonderful tool when you need to traverse deep hierarchies, but it does have a few side effects. With the default netlab settings, Python Box automatically creates empty dictionaries when needed. That’s awesome in 90% of the cases, but sometimes I don’t want to get extra dictionaries, so I have to be a bit careful – instead of if node.bgp.anycast: (which would work even if the node had no BGP parameters), I decided to do a check that would never auto-create empty data structures.
  • If the node.bgp.anycast attribute is set, it’s safe to work with BGP parameters, so we can assume that we can set advertise_loopback and that the node has a list of BGP neighbors in node.bgp.neighbors (even if that list happens to be empty).
  • Every BGP session described in node.bgp.neighbors includes session type in type attribute, and the value of that parameter could be ibgp or ebgp. The list comprehension I used selects all list elements that are not IBGP sessions.

Want to do something similar? It’s not too hard once understand the netlab data structures. Here’s how you can get there:

You can also open a discussion in netlab GitHub repository or ask a question in netlab channel in networktocode Slack team and we’ll do our best to help you.

Revision History

Updated the plugin code to work with netlab release 1.5.

  1. Or a hub-and-spoke topology if you define route reflector(s) ↩︎

  2. I had to import the Box module to keep mypy happy. If you don’t care about static type checks (but you should), you could skip this step. ↩︎

  3. The Python modules used by netlab are within netsim namespace for historical reasons. ↩︎

Add comment