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

Using JSONSchema to Validate input

There are a lot of REST APIs out there. Quite a few of them use JSON as the data structure which allows us to get data in and out of these devices. There are a lot of network focused blogs that detail how to send and receive data in and out of these devices, but I wasn’t able to find anything that specifically talked about validating the input and output of the data to make sure we’re sending and receiving the expected information.

Testing is a crucial, and IMO too often overlooked, part of the Infrastructure as Code movement. Hopefully this post will help others start to think more about validating input and output of these APIs, or at the very least, spend just a little more time thinking about testing your API interactions before you decide to automate the massive explosion of your infrastructure with a poorly tested script. 🙂

What is JSONSchema

I’m assuming that you already know what JSON is, so let’s skip directly to talking about JsonSchema. This is a pythonlibrary which allows you to take your input/output  and verify it against a known schema which defined the data types you’re expecting to see.

For example, consider this snippet of a schema which defines what a valid VLAN object looks like

"vlan_id":
{
    "description": "The unique ID of the VLAN. IEEE 802.1Q VLAN identifier (VID)", 
    "minimum": 1, 
    "maximum": 4094, 
    "type": "integer", 
    "sql.not_null": true
}

You can see that this is a small set of rules that defines what is a valid entry for the vlan_id property of a VLAN.  As a network professional, it’s obvious to us that a valid VLAN ID  must be between 1 and 4094. We know this because we deal with it every day. But our software doesn’t know this. We have to teach it what a valid vlan_id property looks like and that’s what the schema does.

our software doesn’t know this. We have to teach it

Why do we care?

Testing is SUPER important. By being able to test the input/output of what you’re feeding into your automation/orchestration framework, it can help you to avoid, at worst, a total meltdown, or, at best, a couple of hours trying to figure out why your code doesn’t work.

Using JSONSchema

So the two things you’re going to need to use JSONSchema are

  • The JSON Schema for a specific API endpoint
  • The input/output that you want to validate.

In this case, we’ll use a VLAN object that is coming out of an ArubaOS-Switch REST API.

You did know the ArubaOS-Switches have a REST API, right?

Step1 – Loading the VLAN object

We’re going to gather the output from the VLANS API. Instead of writing custom code, we’ll just use the pyarubaoss library. I’ll leave you to check out the GitHub repo and just paste the contents of the output of a single VLAN JSON output here. I’m also going to create a second VLAN with a VLAN_ID of 5000. Just to show how this works. 5000 of course, is not valid and we’d like to prove that. Right?

Step 2 – Loading the JSON Schema Definition

Now we have the output, we want to make sure that the output here complies with the JSON Schema definition that we’ve been provided.

Loading the JSON schema

Here’s a sub-set of the JSON schema that defines what a valid VLAN looks like

Step 3 – Importing the JSON Schema Library and Validating

Now we’re going to load the JSON Schema library into our python session and use it to validate the VLAN object using the Schema we defined above.

First we’ll look at the vlan_good object and run it through the validate function

As you can see, there’s nothing to see here. Basically this means that the vlan_good object is conforming properly to the provided JSON Schema. The VLAN ID is valid as it’s a integer value between 1 and 4094

Now let’s take a look at the vlan_bad object and run it through the same validate function

We can see that the validate function now raises an exception because and let’s us know very specifically that the VLAN ID 5000 is not valid

jsonschema.exceptions.ValidationError: 5000is greater than the maximum of 4094

Pretty cool right? We can still definitely shoot ourselves in the foot, but at least we know the input/output data that we’re using for our API is valid. To me this is important for two reasons

  • I can validate that the data I’m trying to send to a given API conforms to what that API is expecting
  • I can validate that the vendor didn’t suddenly change their API which is going to break my code

Wrap Up

There are a lot of networking folk who have started to take on the new set of skills required to automate their infrastructure. One of the most crucial parts of this is testing and validating the code to ensure that you’re not just blowing up your network more efficiently. JSON Schema is one of the tools in your tool box that can help you do that.

Comments, questions? Let me know

@netmanchris

Auto Network Diagram with Graphviz

One of the most useful and least updated pieces of network documentation is the network diagram. We all know this, and yet we still don’t have/make time to update this until something catastrophic happens and then we says to ourselves

Wow. I wish I had updated this sooner…

Graphviz

According to the website 

Graphviz is open source graph visualization software. Graph visualization is a way of representing structural information as diagrams of abstract graphs and networks. It has important applications in networking, bioinformatics,  software engineering, database and web design, machine learning, and in visual interfaces for other technical domains.

note: Lots of great examples and docs there BTW.  Definitely check it out.

Getting started

So you’re going to have to first install graphviz from their website. Go ahead… I’l wait here.

Install the graphviz python binding

This should be easy assuming you’ve already got python and pip installed. I’m assuming that you do.

>>> pip install graphviz

Getting LLDP Neighbors from Arista Devices

You can use the Arista pyeapi library, also installable through pip as well.  There’s a blog which introduces you to the basics here which you can check out. Essentially I followed that blog and then substituted the “show lldp neighbors” command to get the output I was looking for.

Creating a Simple Network Diagram

The code for this is available here

Essentially, I’m just parsing the JSON output from the Arista eAPI and creating a DOTfile which is used to generate the diagram.

Pros: It’s automated

Cons: It’s not very pretty at all.

SimpleTopo.png

 

Prettying it up a Bit

Code for this is available here

So with a little bit of work using the .attr methods we can pretty this up a bit.  For example the

dot.attr('node', shape='box')

method turns the node shape from an ellipse into a box shape. The other transformations are pretty obvious as well.

Notice that we changed the shape of the shape, the style of the arrows a bit and also shaded in the box.  There are a lots of other modifications we can make, but I’ll leave you to check out the docs for that. 

SimplePrettierTopo.png

 

 

Adding your own graphics

Code for this is available here

Getting a bit closer to what I want, but still I think we can do a bit better. For this example, I used mspaint to create a simple PNG file with a switch-ish image on it. From what I can tell, there’s no reason you couldn’t just use the vendor icons for whatever devices you’re using, but for now, just playing with something quick and dirty.

Once the file is created and placed somewhere in the path, you can use this method

dot.attr('node', image="./images/switch1.png")

to get the right image.  You’ll also notice I used

dot.attr('edge', arrowhead='none')

to remove the arrow heads. ( I actually removed another command, can you spot it? )

SimplePrettierGraphicTopo.png

 

Straighter Lines

Code for this is available here

So looking at this image, one thing I don’t like is the curved lines. This is where Graphviz beat me for the day. I did find that I was able to apply the

dot.graph_attr['splines'] = "ortho"

attribute to the dot object to get me the straight lines I wanted, but when I did that, I got a great message that told me that I would need to use xlables instead of standard labels.

SimplePrettierGraphicOrthoTopo.png

Next Steps

Code for this is available here

For this next step, it was time to get the info live from the device, and also to attempt to stitch multiple devices together into a single topology. Some things I noticed is that the name of the node MUST match the hostname of the device, otherwise you end up with multiple nodes.  You can see there’s still a lot of work to do to clean this up, but I think it’s worth sharing. Hopefully you do too.

MultiTopo.png

 

Thoughts

Pros: Graphviz is definitely cool. I can see a lot of time spent in drawing network diagrams here. The fact that you could automatically run this every X period to ensure you have a up to date network diagram at all times is pretty awesome. It’s customizable which is nice, and multi-vendor would be pretty easy to implement. Worse case scenario, you could just poll the LLDP MIB with SNMP and dump the data into the appropriate bucket. Not elegant, but definitely functional.

Cons:  The link labels are a pain. In the short time I was playing with it, I wasn’t able to google or documentation my way into what I want, which is a label on each end of the link which would tell me what interface on which device. Not the glob of data in the middle that makes me wonder which end is which.

The other thing I don’t like is the curvy lines. I want straight lines. Whether that’s an issue with the graphviz python library that I’m using or it’s actually a problem with the whole graphviz framework isn’t clear to me yet. Considering the time saved, I could probably live with this as is, but I’d also like to do better.

If anyone has figured out how to get past these minor issues, please drop me a line!  @netmanchris on twitter or comment on the blog.

As always, comments and fixes are always appreciated!

@netmanchris