Automate switch port configuration with EosSdk

Switch migrations are part of life in any datacenter, whether to add capacity with a larger system or new capabilities with a later product generation. There are two parts to this task – loading a configuration and the “rack and stack” of physical installation and cabling.

Configuring the new new leaf switch is greatly simplified by tools such as ZTPServer. You can even use LLDP to verify that you’ve cabled the switch to its neighbors correctly. However, when it comes to plugging in servers, you still depend on a very manual process. This can be straight forward when you’ve a single VLAN that all servers use, but often switches have multiple VLANs defined and servers have multiple NICs each in a different VLAN. This can lead to error prone directions such as “connect the second NIC in the fourth server in the rack to the sixth port in the second module of the 7304”. In the same way that we can just connect a switch to the network and have it download its configuration, shouldn’t we be able to plug a server into a switch and based on the MAC address have the switch configure the switch port automatically? With EosSdk, we can.

EosSdk

First, we’ll need the SDK. The EOS Software Development Kit (EOS SDK) lets you program native apps or “agents” that run on your Arista switch. Agents can use the same event-driven asynchronous behavior as any other process in EOS. In particular, an agent can react to changes in the mac address table.

The Wiki covers how to install the SDK.  Installation can also be automated as part of the ZTP process. The wiki also goes into all the details on the lifecycle of an agent and how it subscribes and publishes events. Rather than rehash all the content, we’ll assume you’ve made yourself familiar with that documentation and installed the SDK.

You can verify installation was successful and can be used by python.

Arista#show extensions
Name Version/Release Status extension
------------------------------------------ ------------------------- ------ ----
EosSdk-1.7.0-4.15.2F.i686.rpm 1.7.0/2692966.gaevanseoss A, I 1

A: available | NA: not available | I: installed | NI: not installed | F: forced
Arista#python-shell
Welcome to the Python shell. Press Ctrl-D to exit.
>>> import eossdk
>>> print eossdk.version
1.7.0
>>>

Designing our agent

The goal of our script is simple: when we learn a new MAC address on a server port, look up that address in a list that tells us what VLAN that MAC address is in, and then set the VLAN on the interface we learned the MAC address on.

So, where do we get that list? If we’re migrating from an existing switch, we can just copy the address table. In EOS, we can get a JSON formatted version of the table that will be easier for our agent to parse.

Arista#show mac address-table | json
{
    "multicastTable": {
        "tableEntries": []
    },
    "unicastTable": {
        "tableEntries": [
            {
                "macAddress": "08:00:27:aa:81:45",
                "lastMove": 1451323018.77205,
                "interface": "Ethernet1",
                "moves": 1,
                "entryType": "dynamic",
                "vlanId": 10
            },
            {
                "macAddress": "08:00:27:28:88:56",
                "lastMove": 1451323027.288416,
                "interface": "Ethernet2",
                "moves": 1,
                "entryType": "dynamic",
                "vlanId": 20
            }
        ]
    }
}

Pulling this from the original switch and pushing it to the new one can again be automated as part of the ZTP process.

The ZTP Server can also provide standard interface settings such as spanning tree portfast. It can also set the default VLAN. We can use this to ensure our agent only configures interfaces that connect to end devices.

Writing our agent

There are a lot of examples on the EosSdk page, many in C++ but also plenty of python that we will use as a guide. HelloWorld.py is a good starting point.

After looking through those examples and reading the agent lifecycle document, you’ll know the first thing to do is to grab the right event handlers and start the event loop. We’ll need the agent_mgr and the mac_table_mgr handlers. We’ll call our agent MacVlanAgent.

import cjson
import eossdk
import sys

if __name__ == "__main__":
    sdk_  = eossdk.Sdk()
    _ = MacVlanAgent(sdk_.get_agent_mgr(), sdk_.get_mac_table_mgr())
    sdk_.main_loop(sys.argv)

Now we need to build the MacVlanAgent class. We’ll first initialize various elements:

  • Our agent and MacTable handlers
  • A tracer to output debug messages
  • An ethIntfMgr handler. We use this to set the vlan on the interface.
  • A quarantine VLAN. This is initally set to the default VLAN for access ports.

class MacVlanAgent(eossdk.AgentHandler, eossdk.MacTableHandler):
    def __init__(self, agentMgr, macTableMgr):
        eossdk.AgentHandler.__init__(self, agentMgr)
        eossdk.MacTableHandler.__init__(self, macTableMgr)
        self.tracer = eossdk.Tracer("MacVlanAgent")
        self.agentMgr_ = agentMgr
        self.macTableMgr_ = macTableMgr
        self.tracer.trace0("MacVlanAgent constructed")
        self.ethIntfMgr_ = sdk_.get_eth_intf_mgr()
        self.quarantine = 1

We’ll then handle agent events. We expose two options – a filename that contains our list of mac addresses, and an alternative VLAN to use as the quarantine. If we can’t open the file or have issues parsing it, we’ll shut down the agent. If we can parse it, we start watching mac addre

 
    def on_initialized(self):
        filename = self.agentMgr_.agent_option("filename")
        q = self.agentMgr_.agent_option("quarantine")
        if filename:
            self.on_agent_option("filename", filename)
        if q:
            self.on_agent_option("quarantine", q)

    def on_agent_option(self, optionName, value):
        if optionName == "filename":
            if value:
                load=self.load_table(value)
                if load:
                    # If we have a good config file
                    # start watching mac tables
                    self.watch_all_mac_entries(True)
                    self.tracer.trace1("vlanMacList is %s" % self.vlanMacList)
                else:
                    self.tracer.trace1("Could not parse config file")
                    self.on_agent_enabled(False)
     
        if optionName == "quarantine":
            self.quarantine=int(value)

    # Handle agent shutdown
    def on_agent_enabled(self, enabled):
        if not enabled:
            self.tracer.trace0("Shutting down");
            self.agentMgr_.agent_shutdown_complete_is(True)

When the filename option is set, we call a function that parses that file and produces a list of MACs and VLANs. In this case, we’re parsing the show mac address-table JSON output above.

    def load_table(self,filename):
        try:
            with open(filename) as f:
                try:
                    result = cjson.decode(f.read())
                    self.vlanMacList={}
                    try:
                        for i in result['unicastTable']['tableEntries']:
                            try:
                                m=i['macAddress']
                                self.vlanMacList[m]=i['vlanId']
                            except KeyError:
                                sys.stderr.write("Ignoring host %r\n" % i)
                        return True
                    except KeyError:
                        self.stderr.write("Cannot parse file %r\n" % filename)
                        return False
                except (TypeError, ValueError, cjson.Error) as e:
                    sys.stderr.write('error reading config: %s\n' % e)
                    return False
        except:
            return False

So far we’ve initialized our agent and parsed our MAC address list. Now we need to handle MAC table changes.

A mac_entry_set event provides us the MAC address and the interface it was learned on.  We check that the interface is set to the quarantine VLAN, then look up the mac address for the VLAN it should be in. If we find it we use ethIntfMgr_.default_vlan_is() to set the VLAN.

For now we’ll just log mac_entry_delete events.

    def on_mac_entry_set(self, entry):
        self.tracer.trace1("new mac entry %s" % entry.to_string())
        intf = entry.intf()
        ethAddrString=entry.mac_key().eth_addr().to_string()
        if(entry.mac_key().vlan_id() == self.quarantine):
            try:
                self.tracer.trace1("Setting vlan of %s to  %s" % (intf.to_string(), 
                                    self.vlanMacList[ethAddrString]))
                self.ethIntfMgr_.default_vlan_is(intf, 
                                                 self.vlanMacList[ethAddrString])
            except KeyError:
                self.tracer.trace9("No match found")

   def on_mac_entry_del(self, key):
        self.tracer.trace1("mac entry deleted is %s" % key.to_string())

That’s the end of the agent programming. A full listing is at the end of the page.

Configuring the switch

In the switch configuration we set up a daemon to call the script and set the options we want.
Switch interfaces are set to the quarantine vlan.

daemon MacVlan
   exec /mnt/flash/MacVlan.py
   option filename value /mnt/flash/sw1-address-table
   option quarantine value 999
!
vlan 999
   trunk group notrunk
!
interface Ethernet1
   switchport access vlan 999
   spanning-tree portfast
!
interface Ethernet2
   switchport access vlan 999
   spanning-tree portfast

Running the agent

We can start the agent by configuring “no shutdown” under the daemon configuration.


Arista(config)#daemon MacVlan
Arista(config-daemon-MacVlan)#no shut
Arista(config-daemon-MacVlan)#end
Arista#
Arista#show daemon MacVlan
Agent: MacVlan (running)
Configuration:
Option           Value
---------------- ----------------------------
filename         /mnt/flash/sw1-address-table
quarantine       999

No status data stored.

Arista#
 

For debugging purposes, we’ll run it from the shell with tracing enabled. The agent starts up and parses the mac address table.

bash-4.1# TRACE="MacVlanAgent/*" /mnt/flash/MacVlan.py
[Warning] AGENT_PROCESS_NAME is not set; using agent name 'MacVlan'
2015-09-27 13:41:04.638010 21368 MacVlanAgent 0 MacVlanAgent constructed
2015-09-27 13:41:05.135032 21368 MacVlanAgent 1 vlanMacList is {'08:00:27:aa:81:45': 10, '08:00:27:28:88:56': 20} 

Now we connect the servers. We see the mac_entry_set events. The MAC addresses are found in the list, so we set the vlan of the interface.

2015-09-27 13:43:17.061782 21368 MacVlanAgent 1 new mac entry mac_entry_t(mac_key=mac_key_t(vlan_id=999, eth_addr=08:00:27:aa:81:45), intfs='Ethernet2', persistent=1)
2015-09-27 13:43:17.061893 21368 MacVlanAgent 1 Setting vlan of Ethernet2 to 10
2015-09-27 13:43:39.522325 21368 MacVlanAgent 1 new mac entry mac_entry_t(mac_key=mac_key_t(vlan_id=999, eth_addr=08:00:27:28:88:56), intfs='Ethernet1', persistent=1)
2015-09-27 13:43:39.522441 21368 MacVlanAgent 1 Setting vlan of Ethernet1 to 20

If we go back to EOS, we can see the VLAN settings under the interface configuration.

Arista#show running-config interfaces ethernet 1-2
interface Ethernet1
   switchport access vlan 20
   spanning-tree portfast
interface Ethernet2
   switchport access vlan 10
   spanning-tree portfast
Arista#

Ta-da!

Final Comments

  • Event Driven vs Polling. Tracking MAC addresses can be done via SNMP or running CLI commands on a scheduled basis. This is useful for host tracking and inventory reports. However, in this script we want to react as soon as possible to a new MAC address. Rather than aggressive polling the switch, event driven notification allows us to react immediately to a change.
  • SDK vs API. Configuring a VLAN on an interface using the API would look very similar to the CLI commands:
    response = switch.runCmds( 1, ["configure", "interface %s" % interface, "switchport access vlan %s" %newvlan, "end" ] )

    The SDK version does not look like the CLI at all, but the end result is the same

    self.ethIntfMgr_.default_vlan_is(intf, self.vlanMacList[ethAddrString])
  • Agent integration into EOS – our daemon looks to the process manager like any other EOS agent. We can monitor like any other. It is also managed by ProcManager and will be automatically restarted if it dies.
    Arista#show agent MacVlan logs
    ===> /var/log/agents/MacVlan-9936 Sun Sep 27 15:47:52 2015 <===
    ===== Output from /mnt/flash/MacVlan.py [] (PID=9936) started Sep 27 15:47:52 ===
    Arista#show agent MacVlan ping
    Agent Name Last Ping Max Ping Max Ping Response Seen Last Ping Response Seen
    ---------------------- ------------- ------------ ----------------------- ------------------------
    MacVlan 0.564 ms 136.737 ms 2015-09-27 13:55:18 2015-09-27 15:54:45
    Arista#
    Arista#agent MacVlan terminate
    MacVlan was terminated
    Arista#
    Arista#show log last 5 minutes
    Sep 27 15:52:23 Arista ProcMgr-worker: %PROCMGR-6-PROCESS_TERMINATED: 'MacVlan' (PID=9936) has terminated.
    Sep 27 15:52:23 Arista ProcMgr-worker: %PROCMGR-6-PROCESS_RESTART: Restarting 'MacVlan' immediately (it had PID=9936)
    Sep 27 15:52:23 Arista ProcMgr-worker: %PROCMGR-6-PROCESS_STARTED: 'MacVlan' starting with PID=10648 (PPID=1717) -- execing '/mnt/flash/MacVlan.py'
    Arista#

 

Full listing

MacVlan