Pytest in the Networking World – Part 3

Blog Detail

This blog post will be the final entry in my “Pytest in the Networking World” blog series. Part 1 can be found here and Part 2 can be found here. I recommend reading through both so you have the basic knowledge of how pytest works and some insight into parameterization. In this post, I will be going over various aspects of mocking.

Why Do We Need Mocking?

Keeping in the same vein as our last blog, let’s say we wrote a function using netmiko that queries our Cisco 3650 for its MAC address table so that we can use that information and pass it to our normalize_mac_address function. Sometimes, this call completes in under a second. Other times, it may take up to a few seconds. When we write tests for our code, do we want to possibly wait a few seconds for our call to the 3650 to complete? We definitely don’t. Unit tests should be fast so that developers are more likely to utilize them. Is there a way we could possibly fake the API call? There is, and it’s called mocking! When we mock a function, we make a “dummy” copy of that function where we can control the logic in the function and what that function returns. Let’s walk through an example of how we’d take advantage of mocking.

Updating Our Files

At the end of the last blog, our Python file contained the following:

>>> NON_NORMALIZED_MACS = ["aabb.ccdd.eeff", "0011.2233.4455", "aa11.bb22.cc33"]
>>> NORMALIZED_MACS = ["aa:bb:cc:dd:ee:ff", "00:11:22:33:44:55", "aa:11:bb:22:cc:33"]
>>>
>>> @pytest.mark.parametrize("param_non_normalized_mac, param_normalized_mac", list(zip(NON_NORMALIZED_MACS, NORMALIZED_MACS)))
>>> def test_normalize_mac_address_lists(param_non_normalized_mac, param_normalized_mac):
>>>     assert normalize_mac_address(param_non_normalized_mac) == param_normalized_mac

Using parameterization, we were able to test our normalize_mac_address function with multiple MAC addresses while ensuring each set of data was treated as its own test. Now, let’s add our function that will connect to a Cisco 3650 and get its MAC address table to the same file that the normalize_mac_address function is in. We’ll also add a function that will get the MAC address table and normalize it all at once. Our file should now look like this:

>>> from netmiko import ConnectHandler
>>> from datetime import datetime
>>>
>>> def normalize_mac_address(mac):
>>>     if mac.count(".") == 2:
>>>         mac = f"{mac[0:2]}:{mac[2:4]}:{mac[5:7]}:{mac[7:9]}:{mac[10:12]}:{mac[12:14]}"
>>>     return mac
>>>
>>> def get_mac_address_table():
>>>     start_time = datetime.now()
>>>
>>>     mydevice = {
>>>         "device_type": "cisco_ios",
>>>         "host": "YOUR DEVICE IP",
>>>         "username": "YOUR DEVICE USERNAME",
>>>         "password": "YOUR PASSWORD FOR THE CONNECTING USER"
>>>     }
>>>     command = "show mac address-table"
>>>
>>>     net_connect = ConnectHandler(**mydevice)
>>>     output = net_connect.send_command(command, use_textfsm=True)
>>>     net_connect.disconnect()
>>>
>>>     print(f"\nTime to run: {datetime.now() - start_time}")
>>>     return output
>>>
>>> print(get_mac_address_table())

For information on Netmiko and TextFSM, check here.

I included some code so we could time how long it takes to run this function. Let’s run it now.

[{'destination_address': '0100.0ccc.cccc', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0100.0ccc.cccd', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0000', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0001', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0002', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0003', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0004', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0005', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0006', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0007', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0008', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0009', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.000a', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.000b', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.000c', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.000d', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.000e', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.000f', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0010', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0180.c200.0021', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': 'ffff.ffff.ffff', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, {'destination_address': '0014.1c57.a488', 'type': 'DYNAMIC', 'vlan': '300', 'destination_port': 'Gi1/0/1'}, {'destination_address': '00c8.8bca.55c7', 'type': 'STATIC', 'vlan': '300', 'destination_port': 'Vl300'}, {'destination_address': '0014.1c57.a488', 'type': 'DYNAMIC', 'vlan': '400', 'destination_port': 'Gi1/0/1'}, {'destination_address': '00c8.8bca.55d2', 'type': 'STATIC', 'vlan': '400', 'destination_port': 'Vl400'}, {'destination_address': '0014.1c57.a488', 'type': 'DYNAMIC', 'vlan': '450', 'destination_port': 'Gi1/0/1'}, {'destination_address': '00c8.8bca.55fd', 'type': 'STATIC', 'vlan': '450', 'destination_port': 'Vl450'}, {'destination_address': '0014.1c57.a488', 'type': 'DYNAMIC', 'vlan': '500', 'destination_port': 'Gi1/0/1'}, {'destination_address': '00c8.8bca.55d0', 'type': 'STATIC', 'vlan': '500', 'destination_port': 'Vl500'}]

Time to run: 0:00:04.369203

This gets us a list of dictionaries each containing the destination address, type, vlan, and destination port for each entry in the table. On the last line, you can see it took 4.4 seconds to connect to the device, run the command, parse the output, and present it back to us. Next we’ll need to update our normalize_mac_address function to account for the new data structure. Let’s also add a function to combine getting the MAC address table and normalizing it all in one function.

>>> from netmiko import ConnectHandler
>>> from datetime import datetime
>>>
>>> def normalize_mac_address(macs):
>>>     for entry in macs:
>>>         if entry["destination_address"].count(".") == 2:
>>>             new_mac = f"{entry['destination_address'][0:2]}:{entry['destination_address'][2:4]}:{entry['destination_address'][5:7]}:{entry['destination_address'][7:9]}:{entry['destination_address'][10:12]}:{entry['destination_address'][12:14]}"
>>>             entry['destination_address'] = new_mac
>>>     return macs
>>>
>>> def get_mac_address_table():
>>>     start_time = datetime.now()
>>>
>>>     mydevice = {
>>>         "device_type": "cisco_ios",
>>>         "host": "YOUR DEVICE IP",
>>>         "username": "YOUR DEVICE USERNAME",
>>>         "password": "YOUR PASSWORD FOR THE CONNECTING USER"
>>>     }
>>>     command = "show mac address-table"
>>>
>>>     net_connect = ConnectHandler(**mydevice)
>>>     output = net_connect.send_command(command, use_textfsm=True)
>>>     net_connect.disconnect()
>>>
>>>     print(f"\nTime to run: {datetime.now() - start_time}\n")
>>>     return output
>>>
>>>
>>> def get_mac_table_and_normalize():
>>>     macs = get_mac_address_table()
>>>     normalized_macs = normalize_mac_address(macs)
>>>     return normalized_macs
>>>
>>> print(get_mac_table_and_normalize())

If you run this now, you’ll see that the MAC addresses in the dictionary have been normalized.

[{'destination_address': '0100.0ccc.cccc', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, ...,
 {'destination_address': '00c8.8bca.55d0', 'type': 'STATIC', 'vlan': '500', 'destination_port': 'Vl500'}]

Time to run: 0:00:04.849528

[{'destination_address': '01:00:0c:cc:cc:cc', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}, ...,
{'destination_address': '00:c8:8b:ca:55:d0', 'type': 'STATIC', 'vlan': '500', 'destination_port': 'Vl500'}]

Middle entries have been taken out for brevity.

Great! Our new function works as expected. Now we’ll turn toward updating our tests file. Currently it looks like:

NON_NORMALIZED_MACS = ["aabb.ccdd.eeff", "0011.2233.4455", "aa11.bb22.cc33"]
NORMALIZED_MACS = ["aa:bb:cc:dd:ee:ff", "00:11:22:33:44:55", "aa:11:bb:22:cc:33"]

@pytest.mark.parametrize("param_non_normalized_mac, param_normalized_mac", list(zip(NON_NORMALIZED_MACS, NORMALIZED_MACS)), ids=[x for x in NON_NORMALIZED_MACS])
def test_normalize_mac_address_lists(param_non_normalized_mac, param_normalized_mac):
    assert normalize_mac_address(param_non_normalized_mac) == param_normalized_mac

This test will currently fail, as we have changed the normalize_mac_address function. For the sake of staying on topic, we’ll comment out that test and focus on testing out get_mac_table_and_normalize. Here is our file with a basic test written for our new function and our old test commented out.

>>> import pytest
>>> from mac_address import normalize_mac_address, get_mac_table_and_normalize
>>>
>>> #NON_NORMALIZED_MACS = ["aabb.ccdd.eeff", "0011.2233.4455", "aa11.bb22.cc33"]
>>> #NORMALIZED_MACS = ["aa:bb:cc:dd:ee:ff", "00:11:22:33:44:55", "aa:11:bb:22:cc:33"]
>>> #
>>> #@pytest.mark.parametrize("param_non_normalized_mac, param_normalized_mac", list(zip(NON_NORMALIZED_MACS, NORMALIZED_MACS)), ids=[x for x in NON_NORMALIZED_MACS])
>>> #def test_normalize_mac_address_lists(param_non_normalized_mac, param_normalized_mac):
>>> #    assert normalize_mac_address(param_non_normalized_mac) == param_normalized_mac
>>>
>>> def test_get_mac_table_and_normalize():
>>>     macs = get_mac_table_and_normalize()
>>>     assert True

Let’s run our test and look at the output.

============================================================= test session starts ==============================================================================
platform linux -- Python 3.8.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/adam/repo/Sandbox/blog_pytest
collected 1 item

test_mac_address.py .                                                                                                                                      [100%]

============================================================== 1 passed in 8.95s ================================================================================

The test passes (because we are currently just asserting True), but you’ll notice that on the last line it states that our test passed in 8.95 seconds. We know that connecting to the device and retrieving the MAC address table is taking roughly 4 seconds, so let’s jump into how we’d mock this function.

Mocking

Let me update our function to leverage mocking, and then I’ll go over it.

>>> import pytest
>>> import re
>>> from mac_address import normalize_mac_address, get_mac_table_and_normalize
>>> from unittest.mock import patch
>>>
>>> @patch('mac_address.get_mac_address_table')
>>> def test_get_mac_table_and_normalize(mock_get_mac_address_table):
>>>     mock_get_mac_address_table.return_value = [{'destination_address': '01:00:0c:cc:cc:cc', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}]
>>>     macs = get_mac_table_and_normalize()
>>>     normalized_mac = macs[0].pop("destination_address")
>>>
>>>     assert re.match(r'([0-9a-fA-F]{4}[.]){2}([0-9a-fA-F]{4})', normalized_mac)
>>>     mock_get_mac_address_table.assert_called_once()

Commented test left out for brevity’s sake.

The first thing you may notice is that there are two new imports, import re and from unittest.mock import patch. The re library is not a necessary import to use mocks. I imported it for use in an assert statement that I’ll go over shortly. The unittest.mock import patch is necessary. This gives us access to the @patch() decorator that you see implemented on line 6. This decorator is what is actually doing the mocking. To use the decorator, you pass in the object that you want to mock using the syntax @patch(module.functionA) or @patch(module.ClassA). In this case, when I’m testing get_mac_table_and_normalize, I want to mock the get_mac_address_table function that gets called in my get_mac_table_and_normalize function. I implement my decorator using @patch('mac_address.get_mac_address_table'); and in my test function definition, I add the argument mock_get_mac_address_table. When you mock an object, that mocked object gets passed into the decorated function as an argument so you need to account for that. I typically name the function with mock_ prepended to it.

On line 8, I define what I want the return value to be when my mocked function is called. I can make this whatever I want, but you typically would make this similar to what you would expect from the original function. Line 9, I call our get_mac_table_and_normalize function. Line 10, I get the value for the destination_address key from the first dictionary in the returned list from calling get_mac_table_and_normalize and store it in a variable. In lines 12 and 13, I have two assertions taking place. The first ensures that the value I popped from our returned list conforms to our normalized MAC address format by leveraging regex. The second asserts that our mock function was called only one time. Let’s run our newly written test and check out the results.

=========================================================== test session starts =======================================================================================
platform linux -- Python 3.8.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /home/adam/.virtualenvs/blog/bin/python3
cachedir: .pytest_cache
rootdir: /home/adam/repo/Sandbox/blog_pytest
collected 1 item

test_mac_address.py::test_get_mac_table_and_normalize PASSED                                                                                                      [100%]

========================================================== 1 passed in 4.59s ===========================================================================================

You’ll notice our test passes and that it now took 4.59 seconds to complete. That is exactly what we expected as we mocked our call to our network device which took roughly 4 seconds. To provide another example, let’s say we wanted to mock the normalize_mac_address function. How would we go about that? We can follow the same process we did for the get_mac_address_table function.

>>> import pytest
>>> import re
>>> from mac_address import normalize_mac_address, get_mac_table_and_normalize
>>> from unittest.mock import patch
>>>
>>> @patch('mac_address.get_mac_address_table')
>>> @patch('mac_address.normalize_mac_address')
>>> def test_get_mac_table_and_normalize(mock_normalize_mac_address, mock_get_mac_address_table):
>>>     mock_get_mac_address_table.return_value = [{'destination_address': '01:00:0c:cc:cc:cc', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}]
>>>     mock_normalize_mac_address.return_value = [{'destination_address': '0100.0ccc.cccc', 'type': 'STATIC', 'vlan': 'All', 'destination_port': 'CPU'}]
>>>     macs = get_mac_table_and_normalize()
>>>     normalized_mac = macs[0].pop("destination_address")
>>>
>>>     assert re.match(r'([0-9a-fA-F]{4}[.]){2}([0-9a-fA-F]{4})', normalized_mac)
>>>     mock_get_mac_address_table.assert_called_once()

We are now mocking both functions that are called when we call get_mac_table_and_normalize. We added a patch decorator and included mac_address.normalize_mac_address in its parameter. We also have to add another argument to our test function definition. When mocking more than one object, the lowest patch decorator is mapped to the leftmost argument in the function definition. If we run this, we get the following output:

======================================================== test session starts =============================================================================================
platform linux -- Python 3.8.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/adam/repo/Sandbox/blog_pytest
collected 1 item

test_mac_address.py .                                                                                                                                                [100%]

======================================================== 1 passed in 4.45s ================================================================================================

Our test again passes. This time we have only a slight decrease in test time to 4.45 seconds. This was expected as the normalize_mac_address function is not doing anything extensive, so mocking it didn’t yield much decrease in computation time.

To prove the test is using the mocked return values, you can change the line 10 destination address to something that doesn’t conform to our normalized MAC format and the test will fail.

Mocking Tips

  • There are different implementations of mocking in pytest. I chose the above method because I felt it was the easiest to see and wrap your head around. To see the other implementations of mocking, you can check out the documentation here.
  • When referring to the object you want to mock in the decorator argument, you need to think about mocking the object where it is looked up rather than where it may be defined. This documentation goes more in-depth.
  • You can do a wide variety of things with a mocked object. You can manipulate it to return whatever you want (as shown in our example) or have it return dynamic results using side effects. More information on that and other ways you can manipulate mock functions can be found here.
  • A multitude of assertions can be made on mocked functions. The documentation here goes over them.

Conclusion

This blog is just the tip of the iceberg when it comes to mocking. It’s a pretty extensive feature and can get very complicated very quickly. While this application of mocking may not be the most practical, hopefully these examples were easy enough to follow along with and you were able to gather what mocking is and one of its applications. If you are interested in looking into more robust applications, you can check out the tests for our pyntc repository here. If you have any questions or want to discuss any tests we at NTC may have written that you’d like clarification on, definitely come by and reach out to us on our NTC community Slack.

-Adam



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Introducing the Nautobot Arista CloudVision SSoT Plugin

Blog Detail

The team at NTC is excited to announce the public release of the SSoT Arista CloudVision App for Nautobot. This Arista CloudVision SSoT app is built on top of the Single Source of Truth Nautobot app. I highly encourage you to read that blog post before reading this blog post as you will learn get a basic understanding of the intentions behind the SSoT app and some of its landing pages. This Arista SSoT app allows synchronization of data directly between CloudVision and Nautobot. It synchronizes user device tags from Nautobot into CloudVision while using devices and system tags from CloudVision to populate devices and device metadata in Nautobot too.

For all intents and purposes, “app” is synonymous with “plugin”.

Installing the Plugin

The app is available as a Python package in PyPI and can be installed atop an existing Nautobot installation using pip. For the full installation steps, refer to the app’s README.

Once installed, you should be able to navigate to Plugins > Single Source of Truth > Dashboard from the navigation bar (or go to /plugins/ssot/) on your Nautobot instance to view the SSoT dashboard. Once there, you should have CloudVision as an option under both Data Targets and Data Sources:

ssot_homepage

Using the Plugin

Usage of this app is very simple and follows the steps in the Single Source of Truth blog post. Let me go over what exactly is synced when running each of the jobs, how the data models on each side of the sync relate, and other noteworthy behaviors of the app.

Syncing CloudVision as a Data Source

When syncing with CloudVision as the data source, devices and system tags from CloudVision are synced to Nautobot.

Devices

Devices in CloudVision map to the device models in Nautobot. This means that if you have a device in CloudVision that does not exist in Nautobot, it will be created in Nautobot when you run the sync.

When syncing devices from CloudVision we wanted to ensure that only the Arista devices are in scope in Nautobot as CloudVision would only have Arista devices. To do this, when the sync runs, we only gather devices from Nautobot that have their device type manufacturer name set to Arista.

Now, if you are familiar with Nautobot, you know that when creating a device, you must provide a site, a device role, device role color, and a device status. To account for this, the app allows setting app variables to set default values to your liking. They are:

Configuration VariableDefault Value
from_cloudvision_default_sitecloudvision_imported
from_cloudvision_default_device_rolenetwork
from_cloudvision_default_device_role_colorff0000
from_cloudvision_default_device_statuscloudvision_imported
from_cloudvision_default_device_status_colorff0000

If these app settings are not defined, the default values above will be used on any devices imported into Nautobot.

When configuring these settings, if the value you set the environment variable to does not exist in Nautobot it will create that object. For example, if I set the from_cloudvision_default_device_role to super_switch, and the device role super_switch does not already exist in Nautobot, this app will create it for you.

Just as the app will create devices in Nautobot, it can delete devices as well. I say “can” delete because there is another app variable that can be set, delete_devices_on_sync_cv_source. This variable controls the behavior of the app when a device exists in Nautobot but not in CloudVision. If not defined in the app settings, the value is set to False which means the app will not delete devices from Nautobot that no longer exist in CloudVision. You can set the variable’s value to True by including the variable in your app settings. When set to True it will delete devices.

Here is screenshot of the landing page for the job to sync from CloudVision. You can see it displays those variables, as well as what they are currently set to.

configuration_page

System Tags

Along with devices, the system tags for each device are synced from CloudVision as well. When this app is installed, it creates the custom fields that are tied to the device model. The below shows the CloudVision system tags and how they map to custom fields when a sync occurs.

CloudVision System TagsNautobot Device Custom Field
bgpbgp
eosEOS Version
eos trainEOS Train
mlagmlag
modelDevice Platform*
mplsmpls
pimpim
pimbidirpimbidir
serialnumberDevice Serial Number
sflowsFlow
systypesystype
tapaggTAp Aggregation
terminattrTerminAttr Version
topology_network_typeTopology Network Type
topology_typeTopology Type
ztpztp

The model system tag is not mapped to a custom field. Instead, the model tag is tied to a Device Platform. If the Device Platform does not exist upon syncing, the app will create a new Device Platform.

While these system tags won’t be changing very often, they will be updated in the event of a change. An example being if you update software version on a device. CloudVision will automatically pick up the change, but you’d need to run a sync job to ensure the custom field in Nautobot gets updated.

Syncing with CloudVision as a Data Target

When syncing with CloudVision as the data target, tags from Nautobot are synced to CloudVision.

Tags

This sync job takes tags created in Nautobot and creates them in CloudVision as User tags. It also ensures devices that may be assigned those tags in Nautobot are updated in CloudVision as well. For example, let’s say we already have our devices synced between Nautobot and CloudVision. In Nautobot we create a tag that denotes whether or not a device is running Virtual Router Redundancy Protocol (VRRP). We create the tag vrrp:True in Nautobot and assign it to our device named nyc-switch01. Instead of having to go to CloudVision to create that tag and assign it there, we can run this sync. It’ll automatically create the user tag vrrp:True in CloudVision and assign it to the appropriate device.

But wait! There’s more!

Before using the Arista SSoT app, you may have user tags in CloudVision that you want to import into Nautobot. That’s where the Arista CloudVision Importer comes in. This CLI tool is used as an initial sync of your user tags in CloudVision to tags in Nautobot so that we can treat Nautobot as the Source of Truth for tags. In the interest of brevity for this blog post, installation and usage instructions can be found in the README file. One thing worth mentioning is that the slugs for any tags imported into Nautobot will follow the schema of arista_{tag_name}_{tag_value}. This serves two purposes:

  1. Shows in Nautobot which tags were imported from CloudVision.
  2. Ensures tags imported into Nautobot are unique.

Here is a small screen capture of tags that have been imported into Nautobot. 

tag_slug

The name of the tag is a direct copy. Only the slug adheres to the syntax schema mentioned above.


Conclusion

The Single Source of Truth Nautobot app is a very useful app that will allow Nautobot to be treated as the Single Source of Truth. This Arista integration is just the tip of the iceberg when it comes to what we can sync and what systems we can sync with. If you have any questions or ideas, don’t hesitate to reach out to us on our NTC community Slack.

-Adam



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Arista CloudVision ChatOps with Nautobot

Blog Detail

In the spirit of Network to Code’s continuing development in the ChatOps space, we are releasing a new Nautobot app. It is an application to interact with Arista CloudVision using the existing Nautobot ChatOps framework. This ChatOps integration comes pre-packaged with commands to gather various data about CloudVision (CV). You can now get key information from CV directly from chat!

This app can work with either an on-prem instance of CloudVision or CloudVision as-a-Service. For installation steps, refer to the app’s README. To install the underlying Nautobot ChatOps framework, refer to the documentation found here.

Looking at the image below gives a visual representation of how this app works with the core ChatOps app & plugin.

ChatOps_flow

The Nautobot ChatOps CloudVision plugin extends the capabilities of the Nautobot ChatOps framework addding a new chat command. The new CloudVision plugin introduces the following commands:

  • get-devices-in-container
  • get-configlet
  • get-device-configuration
  • get-task-logs
  • get-applied-configlets
  • get-active-events
  • get-tags
  • get-device-cve
cloudvision_commands

Get Devices in Container

The get-devices-in-container sub-command will prompt the user to choose a container from the existing containers in CloudVision. It will then give you a list of all devices in the chosen container.

Get Configlet

The get-configlet sub-command will query CloudVision for the currently defined configlets, ask you to choose one, and give you the contents of the chosen configlet.

Get Device Configuration

The get-device-configuration sub-command queries CloudVision for all of its devices, prompts you to choose one, and retrieves the specified device configuration.

Get Task Logs

The get-task-logs sub-command allows you to choose a task by Task ID. It will then give you the logs of that task.

Get Applied Configlets

The get-applied-configlets sub-command allows you to get a list of configlets applied to either a container or a device.

Get Active Events

The get-active-events sub-command allows you to get active events filtered by device, type, or severity within a given time frame. When the sub-command is executed, a prompt will ask you for the filter you wish to apply. From there it will ask you for a start time. We configured it so that you may specify a start time relative to the current time by using an h for hours, d for days, and w for weeks. For example, when prompted for a start time, you’d enter the value -2w if you wanted to go back 2 weeks from the current time. For 2 days, you’d type -2d. The last prompt asks you for an end time. You can again use the same syntax mentioned before but you may also type the word now to use the current time.

Get Tags

The get-tags sub-command will present a list of all the tags assigned to a specific device.

Get Device CVE

The get-device-cve sub-command gets a list of all CVE’s discovered by CloudVision for a specific device.


Conclusion

These commands only handle a subset of the information that can be gathered by a CloudVision chatbot. You can contribute more commands with minimal Python code! Because the Nautobot ChatOps plugin lowers the barrier of entry by already handling the interaction between Nautobot and chat applications like Mattermost, Microsoft Teams, Slack, and Webex, creating new commands is extremely easy. We encourage you to create your own commands by building on top of existing commands and plugins we at NTC have created, or to create your own command to interact with something you use on a daily basis. We’re going to continue creating new plugins for ChatOps, so keep an eye out for additional announcements here!

-Adam



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!