Playing with Arista eAPI – Dynamic VLAN Assignment-ish

A note

Before we get started, let me first say that just because we can, doesn’t mean we should.  I got a request to show an example of how dynamic VLAN assingment based on mac-address could work and this is the results of that request. Before anyone takes this code and runs with it, let me first say that you really need to think about whether or not this is a good idea for you and your organization.

If you want dynamic VLAN assignment, there are other ways to accomplish this and I would encourage you to look into them before going down this path.

That being said – It was a fun little project and I wanted to share it with you all.

The Project

For this project, the goal is to write a simple script to help us automatically assign the right VLAN to a host based on the MAC address of that host.

Now we all know there are different ways to do this, like pure mac-authentication using a RADIUS server, or even using a mac-based VLAN configuration, but that wouldn’t be as fun as trying to get this work with Arista’s eAPI now would it?

The Plan

So our basic plan is the following

  • Query the Arista eAPI to request all of the mac addresses.
  • Look through the eAPI response for the target mac address while ignoring the uplink ports that connects the switches
  • Once the interface where the host is connected has been identified, send another Arista eAPI call to assign the appropriate VLAN to the target interface.

*Note: This is a simple project and should NOT be implemented into a production environment without REALLY thinking about the various failure modes.

First we’ll do the imports. For this project, we’ll import the standard json library as well as the Arista pyeapi library.

note: This assumes you’ve actually pip install pyeapi’d this library into your environment. You did do that, right?

In [30]:
import json
import pyeapi

Create the Switches Variable

Now that we’ve imported the libraries we’ll need, let’s start setting up the variables

First we’ll create a variable called switches that will be a python list of strings which in IPv4 address format. One for each switch which we want to search through.

In [23]:
switches = ['10.101.0.249', '10.101.0.250']

As with most things with automation, you have two choices, standardize or write a LOT of code to keep up with the variable conditions. For my purposes, I want to keep this straightforward so I will be deciding that uplinks, or connections/trunks, between the two switches will be using interface Ethernet1 only.

This may sound a bit strange, but remember that in normal operations, an ARP will show up on the link that interconnects two switches as it travels through the network. I’m assuming you don’t want to be changing the VLAN on the interfaces that connect your switches, right?

We will now create a variable called uplinks that will be a python string object with the name of the ethernet interface as it would be displayed in the eAPI response. Basically we’ll be saying if the name is what’s contained in the uplink variable, then don’t do anything.

In [24]:
uplink = 'Ethernet1'

Create the target variable

Our last variable will be to set up the actual target host that we want to find and assign to a specific VLAN. You will need to make sure that the format of the mac-address must be consistent with what’s in the Arista eAPI response.

As we all know, mac-address can be displayed in various different formats including

  • xx:xx:xx:xx:xx:xx
  • xxxx.xxxx.xxxx
  • xxxx:xxxx:xxxx
  • xxxxxxxxxxxx
  • note: If we were doing this right, we would probably want to include some code that would normalize the mac-addresses into a consistent format regardless of the input format. But that’s code for another day. *

The other thing we’ll need to do is to include the dot1q vlan tag.

We will now create a variable called target that will be a python dict object with two key-value pairs. One for the mac-address, and one for the dot1q VLAN ID. This should be very obvious

In [25]:
target = {"mac" : '00:0d:6f:40:1b:e0', "vlan" : "3"}

Gathering the MAC addresses

Now that we’ve staged all variables, we can get down to writing some code.

The first function we’re going to create a python function to gather the mac-addresses from our list of switches and return a list of the mac-addresses.

In [26]:
def gather_macs(devices):
    """
    Function which takes a list of devices as input and runs the show mac address-table command against them
    using the Arista pyeapi library
    :param devices:
    :return: list of mac-address output
    """
    found_macs = []
    for switch in devices:
        node = pyeapi.connect(host=switch, username='admin', password='password')
        output = node.send('show mac address-table')
        output['switch'] = switch
        found_macs.append(output)
    print (len(found_macs))
    return found_macs

Now we can run this and assign the output to a python variable.

In [34]:
mac_list = gather_macs(switches)
mac_list
2
Out[34]:
[{'id': None,
  'jsonrpc': '2.0',
  'result': [{'multicastTable': {'tableEntries': []},
    'unicastTable': {'tableEntries': [{'entryType': 'dynamic',
       'interface': 'Ethernet48',
       'lastMove': 1521485142.203959,
       'macAddress': '00:0d:6f:40:1b:e0',
       'moves': 1,
       'vlanId': 1},
      {'entryType': 'dynamic',
       'interface': 'Ethernet17',
       'lastMove': 1516790007.97397,
       'macAddress': '00:1c:73:8c:92:75',
       'moves': 1,
       'vlanId': 1}]}}],
  'switch': '10.101.0.249'},
 {'id': None,
  'jsonrpc': '2.0',
  'result': [{'multicastTable': {'tableEntries': []},
    'unicastTable': {'tableEntries': [{'entryType': 'dynamic',
       'interface': 'Ethernet1',
       'lastMove': 1521485429.933993,
       'macAddress': '00:0d:6f:40:1b:e0',
       'moves': 1,
       'vlanId': 1},
      {'entryType': 'dynamic',
       'interface': 'Ethernet1',
       'lastMove': 1521421510.48407,
       'macAddress': '00:1c:73:8c:92:75',
       'moves': 1,
       'vlanId': 1}]}}],
  'switch': '10.101.0.250'}]

Now that we’ve got the list of mac-address from the switches that we defined in the switches variable. To do that, we need to write some more python code which will look through the list we created in the last step and look through that mac-list to see if the mac-address is present

In [21]:
def find_mac(target, list_of_macs):
    mac_location = []
    for switch in list_of_macs:
        for entry in switch['result'][0]['unicastTable']['tableEntries']:
            if entry['macAddress'] == target['mac']:
                print ("I found the mac_address " + target['mac'] + " on " + switch['switch'] + " on interface " + entry['interface'])
                mac_location.append({'mac': target['mac'], 'switch': switch['switch'], 'interface' : entry['interface']})
    return mac_location

Now that we’ve created the function, we can test this out using the following bit of code. Remember we created the variable target up above? That’s what we’re using here as the input.

In [28]:
host = find_mac(target, mac_list)
host
I found the mac_address 00:0d:6f:40:1b:e0 on 10.101.0.249 on interface Ethernet48
I found the mac_address 00:0d:6f:40:1b:e0 on 10.101.0.250 on interface Ethernet1
Out[28]:
[{'interface': 'Ethernet48',
  'mac': '00:0d:6f:40:1b:e0',
  'switch': '10.101.0.249'},
 {'interface': 'Ethernet1',
  'mac': '00:0d:6f:40:1b:e0',
  'switch': '10.101.0.250'}]

As you can see, we found two occurrences of the mac-address, the bottom one of two items in this list has the interface of Ethernet1 which means this is the uplink port between the two switches, remember?

So now we have to write some code to exclude the uplink interfaces which you can see below in the if entry[‘interface’] == uplink
phrase below.

If it’s not in the uplink list, then we need to use issue a command to the specific Arista switch to the specific interface where we found that mac-address.

In [35]:
def set_vlan(mac_location, target):
    for entry in mac_location:
        if entry['interface'] == uplink:
            print ("Skip the uplink")
            continue
        else:
            node = pyeapi.connect(host=entry['switch'], username='admin', password='password')
            data = {
  "jsonrpc": "2.0",
  "method": "runCmds",
  "params": {
    "format": "json",
    "timestamps": False,
    "autoComplete": False,
    "expandAliases": False,
    "cmds": [
      {
        "cmd": "enable",
        "input": "my_enable_passw0rd"
      },
      "configure",
      "interface " + entry['interface'],
      "switchport access vlan " + target['vlan']
    ],
    "version": 1
  },
  "id": "EapiExplorer-1"
}
            print (entry['switch'])
            print (data)
            output = node.send(json.dumps(data))
            print (output)

Now we use the output of the find_mac() location that we got above combined with the VLAN information that we stored in the target variable.

In [36]:
output = set_vlan(host, target)
print (output)
10.101.0.249
{'method': 'runCmds', 'params': {'expandAliases': False, 'autoComplete': False, 'timestamps': False, 'version': 1, 'format': 'json', 'cmds': [{'cmd': 'enable', 'input': 'my_enable_passw0rd'}, 'configure', 'interface Ethernet48', 'switchport access vlan 3']}, 'jsonrpc': '2.0', 'id': 'EapiExplorer-1'}
{'result': [{}, {}, {}, {}], 'jsonrpc': '2.0', 'id': 'EapiExplorer-1'}
Skip the uplink
None
Now for the last part, we will write a quick bit of code to check the vlan of the interface to make sure that it’s actually been set in the right VLAN.
In [56]:
def check_int_vlan(switch, interface):
    node = pyeapi.connect(host=switch, username='admin', password='password')
    output = node.send('show mac address-table')
    return output
vlans = check_int_vlan('10.101.0.249', 'Ethernet48')['result'][0]['unicastTable']
for i in vlans['tableEntries']:
    print ("Mac address " + i['macAddress'] + " is located on interface " + i['interface'] + " in VLAN " + str(i['vlanId']))
Mac address 00:1c:73:8c:92:75 is located on interface Ethernet17 in VLAN 1
Mac address 00:0d:6f:40:1b:e0 is located on interface Ethernet48 in VLAN 3

Closing Thoughts

Hopefully you follow the logical process of how you would go about fulfilling the requirements. One thing I do want to leave you thinking about though is whether the goal made sense in the first place? As well, what additional assumptions and operational process needs to be in place to make this a reality. Questions like: Is the mac-address right right identifier? Where did you get the mac-address? Is this the physical mac-address or a virtual mac-address?

Feel free to post in the comments below if you feel inspired.

@netmanchris

Advertisements

Hey Alexa, Turn my lab on!

TL/DR Put together a custom Alexa Skill so I can turn switches and routers off in my lab as shown in the video here. Feels pretty great.

 

As most of my twitter followers have noticed, I’ve been doing a lot of Home Automation, mostly with Apple #homeKit. But I also picked up an Amazon Dot because… well why not?

One of the great things abut the digital voice assistance from Amazon, is that they have created an extensible framework that enables those with a little bit of coding skills to add to Mrs. A’s already already impressive impressive array of abilities.

The Amazon Alexa developer page is pretty impressive. There’s a ton of information and tutorials there, as well as an SDK and code examples in Node.js. I’m almost exclusively a python coder at this point, so I decided to look for something a little more familiar and came upon this.

Flask-Ask

Flask-Ask is a Flask extension that makes building Alexa skills for the Amazon Echo easier and much more fun.

Essentially, John Wheeler took the flask WSGI ( web) framework and made it super easy to be able to create Amazon Alexa skills using this familiar library. I’ve used Flask in the past for a few projects, so this was a no-brainer for me.

John also put together a set of tutorials here which can be used to jumpstart the Alexa skills development process. There’s also a flask-ask Quickstart on the amazing developers blog which pointed me towards ngrok which came in really handy!

*Ngrok allows you to create secure tunnels to a local host. You run ngrok with the port number you want to expose and it automatically exposes the host as a resource on the grok website. It’s really really really cool. 

The Project

Like many of us, I have a physical lab in my house from my CCIE studies. As well, specializing in network management over the years requires access to physical gear in a lot of instances. Powering on that gear full time is out of the question because of the cost and power drain. As I’m sure you can imagine, going back and forth to turn things on and off gets old real quick.

To address that problem, I picked up a couple of intelligent PDU’s on eBay. There are many “smart” PDUs out there and I happen to have a set of Server Technologies that allows me to control each socket on a 16 port power bar. Pretty cool, right?  No more walking to the garage, which is a good thing when you’re trying to focus on a problem.

So things are heading in the right direction;  I can pop over to the local web interface of my PDU and turn my devices on and off. That’s nice…. But all the home automation stuff I’ve been doing lead me to wonder…

Why can’t I just ask for the device to be turned on?

I can ask Siri or Alexa to turn on the lights or adjust the temperature of my house. I can ask the about the weather or to check my calendar. There’s no reason why I shouldn’t be able to do the same with my lab gear.

So I decided to make that a reality.

What’s not covered in this blog

The one step which is not covered in this blog is writing the pyservertech library which I built on top of the pysnmp library. Essentially I walked the MIBs until I found how to gather the info I needed and figured out which specific MIB I needed to set to turn an individual power socket on or off.  I might do a blog on that specific piece too, but for now, I’m trying to focus on the Alexa piece.

If there’s interest, please let me know in comments or on twitter and I’ll prioritize the SNMP set blog. 🙂 

Building the Alexa Skill

Alexa skills are a combination of three components

  • Ask-Flask – This is the actual code and includes the templates file shown below
  • Intent_Schema – Kinda obvious, but this includes the various intents that you’re going to use in your skill
  • Sample Utterances – Are a collection of the various verbal phrases and how they are connected to the intents.

I’ll do my best to connect these in the code below, but I’d really recommend going through a couple of the tutorials above and play around with the examples to built some intuition on how these components connect.

The code below let’s a user do the following using Amazon Alexa’s voice assistant.

  1. Ask Alexa to open the Lab skill ( Lab is what I called it )
  2. Alexa asks the user “Welcome to the lab. I’m going to ask you which plug you want me to turn on. Ready?”
  3. User responds with “Yes”or “Sure”
  4. Alexa asks the user “Please tell me which power socket you would like to turn on?”
  5. User responds with a number which is the power socket they would like to turn on
  6. Alexa decodes the response and returns the number in a JSON array to the local Flask server
  7. Python code takes the number from the JSON array and uses that as input into the power_on() function.
  8. Power_On() function sends an SNMP SET command to the appropriate input.
  9. Device powers on.Alexa says “I’ve turned the power socket on.”
  10. I don’t walk to the garage.

Now that we understand how the code is supposed to work, let’s take a look at the individual pieces and how they fit together.

Alexa Skill

This is the python code that you’ll run on your local machine. This contains only a portion of the logic of the “program” as Amazon is really doing the majority of the lifting on their side as far as the speech recognition and returning the appropriate data in a JSON array.

Templates file

This file contains the various phrases that Alexa is going to speak on behalf of your application. You can see we’ve only got a few different.

Intent_Schema

This file gets loaded on the Amazon website.  Using the developer interface, you load the JSON which defines the Intent Schema directly into the intent schema location on the Interaction Model page.

NewImage

Sample Utterences File

Just like the Intent Schema, the Sample Utterances is also loaded directly into the Amazon developer portal into the Interaction model for this specific skill

NewImage

What’s next

This is just the start of this skill. All it does right now is turn things on, which is cool, but I want more. Just off the top of my head here are some of the things I’d like to do

  • Turn individual devices on or off
  • Turn individual devices on or off by name “Alexa ask the lab to turn on the HPE 2920 switch!”
  • Turn groups of devices on or off “Alexa ask the lab to turn on the Juniper branch!”
  • Request data from the PDUs “Alexa ask the lab How many devices are currently turned on?”  Or “ask the lab how much power is currently being used”

As you can imagine, this would require a lot more code and logic to accomplish all these goals. Definitely something I’m going to pursue, but I’m hoping that the simple example above helps to inspire someone else in their journey down this path.

Questions? Comments? You know what to do…

@netmanchris