Creating Custom Chat Commands Using Nautobot ChatOps

ChatOps and chat bots are becoming an ever more popular method to interact with network systems. Whether the command is to get operational status, to full-on operational configuration commands, interactions via chat is here. Nautobot not only is a Source of Truth, but is also a Network Automation Platform to build apps on top of! One app already released is the ChatOps plugin that you can use to accelerate the implementation of your individual chatbot needs! You can explore a public demo of the ChatOps plugin in the Network to Code (NTC) Slack channel #nautobot-chatops. The ChatOps plugin today has support for integration with Microsoft Teams, Cisco Webex, Slack, and Mattermost. To install and configure the plugin with your own particular chat environment, take a look at the docs.

Extending the ChatOps plugin to support your own commands is quite straightforward. This post provides a walk-through guide to getting up and started with your own custom chat commands. Underneath the entire plugin is Python, which allows your chat commands to interact with nearly any system you want. Want to get information from a third-party API and present it back? Want to kick off an Ansible playbook execution in AWX/Tower? Want to interact with a network device over SSH? All of these are absolutely possible!

Exploring ChatOps Connection to Nautobot

By default and out of the box, the Nautobot ChatOps plugin will provide the capability to query the Nautobot environment directly. This is what is enabled on the NTC Slack channel. It is tied to the demo instance of Nautobot. Take a look at our NFD talk introduction to ChatOps and demonstration for more examples of Nautobot ChatOps features available with installation.

Exploring Slack-Specific Commands

Slack Command Help

Within Slack specifically, to get started you probably want to see all of the commands that are available for the chatbot. In this case the bot name is nautobot, so when you send the command /nautobot to the channel you get the response with the following:

Some points to note as you start thinking about adding your own commands:

  • Ordering of the commands: This is controlled by the order of the functions as they get defined in the plugin definition file.
  • Help text: This is gathered from the first line of the docstring that accompanies the function.

Slack Command Example – Get Devices

First, looking at the help output for get-devices it shows /nautobot get-devices [filter-type] [filter-value]. If one were to issue the command with the filter-type and filter-value defined, then that is all that is needed to kick off the command. The second option is to just issue the command /nautobot get-devices and the bot will prompt back for those particular items. As the prompt gets answered, the next option continues on. So in this example you can get the data with either the command where you follow the prompts, or just issue the complete command /nautobot get-devices site nyc.

The result is a text entry that is:

           Name              Status   Tenant       Site        Rack     Role     Type   IP Address
==================================================================================================
nyc-bb-01.infra.ntc.com      Active            New York City          Backbone   vMX              
nyc-leaf-01.infra.ntc.com    Active            New York City          leaf       vEOS             
nyc-leaf-02.infra.ntc.com    Active            New York City          leaf       vEOS             
nyc-rtr-01.infra.ntc.com     Active            New York City          Router     vMX              
nyc-rtr-02.infra.ntc.com     Active            New York City          Router     vMX              
nyc-spine-01.infra.ntc.com   Active            New York City          spine      vEOS             
nyc-spine-02.infra.ntc.com   Active            New York City          spine      vEOS      

Command Output Observations

Some things from the code here:

  • Site nyc is a slug, as in the imagery it shows the full name New York City but in the output shortcut that gets added it shows nyc. This matches the slug since spaces are not allowed in the section of the commands. Each space is a separator for the command to process.
  • Shortcut text is specified and written in the code had as part of the response for the bot. The bot itself does not add this or any other text.

Creating Your Own Chat Command

This walk-through will be to write a new chat command (as a Nautobot ChatOps plugin) that will be named netchat within Slack. The focus here is on getting started writing your own code rather than the chat platform itself. What gets outlined here should work for any other chat platform with minimal modifications. The document for chat setup referenced earlier is your guide to walk through getting the chat platform ready.

Once you have written the code, the same code is designed to work with Slack, Microsoft Teams, Cisco Webex, and Mattermost. Nautobot ChatOps can interact with multiple platforms from the same API endpoint. Allowing for interactions from any of the platforms defined in the Nautobot configuration.

There is not a limitation to only these four chat platforms. This can be expanded with additional dispatchers to be added into the ChatOps plugin. Recently this was extended to support Mattermost as an example, which was not supported at the beginning of 2021.

In this walk-through, we will be adding a command to get the device inventory from a Meraki Organization by interacting with the Meraki Dashboard API.

Demo Setup

Demo Setup Assumptions

  • Nautobot is locally installed via the local installation methods
  • Nautobot ChatOps has been successfully installed
  • The Nautobot ChatOps Plugin is required for adding custom chat commands

Demo Setup – Nautobot Configuration

The first step is to set up the Nautobot configuration. Within (/opt/nautobot/nautobot_config.py) a dictionary is added to PLUGINS_CONFIG. It shows as follows:

PLUGINS_CONFIG = {
    'nautobot_chatops': {
        'enable_slack': True,
        'slack_api_token': os.getenv("SLACK_API_TOKEN"),
        'slack_signing_secret': os.getenv("SLACK_SIGNING_SECRET")
    }
}
  • nautobot_chatops is the required first key to indicate this is the plugin configuration for the ChatOps plugin.
  • The value of nautobot_chatops is another dictionary that has the keys:
    • enable_slack: Boolean field for having Slack enabled. There are enable_* values for each of the chat platforms there are dispatchers for. There can be multiple platforms enabled at one time.
    • slack_api_token: Token for the bot defined within the chatbot config on api.slack.com
    • slack_signing_secret: Signing secret as defined within the chatbot config on api.slack.com
  • There are other keys per platform, depending on the chat platform

Demo Setup – Nautobot Environment Setup

The Slack configuration is configured in the environment, not in the configuration file. The environment is read by the configuration file and thus loaded into Nautobot. In order to get the updated environment, the Nautobot service files are updated to reference the environment file /opt/nautobot.env.

  • Add EnvironmentFile=/opt/nautobot/.env to both /etc/systemd/system/nautobot.service and /etc/systemd/system/nautobot-worker.service files.
[Unit]
Description=Nautobot WSGI Service
Documentation=https://nautobot.readthedocs.io/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Environment="NAUTOBOT_ROOT=/opt/nautobot"
EnvironmentFile=/opt/nautobot/.env

User=nautobot
Group=nautobot
PIDFile=/var/tmp/nautobot.pid
WorkingDirectory=/opt/nautobot

ExecStart=/opt/nautobot/bin/nautobot-server start --pidfile /var/tmp/nautobot.pid --ini /opt/nautobot/uwsgi.ini

Restart=on-failure
RestartSec=30
PrivateTmp=true

[Install]
WantedBy=multi-user.target
  • The environment file (here, /opt/nautobot/.env) is set up with the corresponding environment variables and the file permissions are set to 0600.
cat /opt/nautobot/.env
SLACK_API_TOKEN=xoxb-0000000000000-0000000000000-abcdefg00000ABCDEFG01238
SLACK_SIGNING_SECRET=00000000000000000000000000000000
MERAKI_DASHBOARD_API_KEY=0000000000000000000000000000000000000000
  • After modifying the systemd files, execute daemon-reload and restart the services. Once the service is restarted it is recommended to check the status of the bot by issuing a Slack command /nautobot to get the help context.
sudo daemon-reload
sudo systemctl restart nautobot nautobot-worker

Inside the environment file is the Meraki API key to put the API key into the environment as required by the Meraki SDK.

Using Existing Methods for Portability

The first recommendation is to look at the design docs that show how the plugin is designed. It is recommended, wherever possible, to use the Python methods provided, especially in the interaction back to the chat platform. For example, one could set up the mention of @username easily in the response to tag a user. However, there is a Python method within the dispatcher to get the user mention. This way, should a platform need to be migrated say from Slack to Microsoft Teams, updates are not needed to the code, just a change of the settings. This allows for the chat bot to interact with multiple chat platforms at the same time. Take a look at the code inside of GitHub for your specific platform to see what methods are available!

Creating the Custom Chat Commands

After setting up the settings mentioned above to load the configuration into the environment, I logged into the user account for nautobot with sudo -iu nautobot. Following the instructions for Nautobot v1.0.0b2 or later, this has the user root in /opt/nautobot. Here create a new directory plugins/netchat (/opt/nautobot/plugins/netchat/).

The directory structure that will be built out is:

$ tree /opt/nautobot/plugins/
/opt/nautobot/plugins/
└── netchat
    ├── netchat
    │   ├── __init__.py
    │   └── worker.py
    ├── poetry.lock
    └── pyproject.toml

First step in this is to build out the directory structure and adding the __init__.py file. This creates a new /opt/nautobot/plugins directory, as well as the directory structure for the package netchat.

cd /opt/nautobot
mkdir -p plugins/netchat/netchat
touch /opt/nautobot/plugins/netchat/netchat/__init__.py

This ChatOps plugin will use Python Poetry to handle the packaging and configuration. To install Poetry, take a look at the installation steps outlined by Poetry. This should follow their installation methods, and do not install with Python PIP. They were specifically chosen to help with the entry point configuration which is required to add onto the chatops plugin. The configuration pieces specific to the netchat plugin for the /opt/nautobot/plugins/netchat/pyproject.toml:

[tool.poetry]
name = "netchat"
version = "0.1.1"
description = ""
authors = ["Network to Code <opensource@networktocode.com>"]

[tool.poetry.plugins."nautobot.workers"]
"netchat" = "netchat.worker:netchat"

The first section of tool.poetry outlines the Python package itself, in this instance it is called netchat. The second section tool.poetry.plugins."nautobot.workers" defines the “entry point” to register the code as an extension of the ChatOps plugin.

The left-hand side of "netchat" = corresponds to the slash command that is being installed, and the right-hand side of the line = "netchat.worker:netchat is how to get to the function. In this case, netchat.worker refers to the file netchat/worker.py, and the netchat to the right of the : is the function name inside of this file.

Take a look at the gist, if preferred.

Netchat – Worker

In the netchat plugin, the code for the chat bot will be housed within /opt/nautobot/plugins/netchat/worker.py. This is from the reference in the pyproject.toml file in the configuration "netchat" = "netchat.worker:netchat". This could be any file name, something that meaningful to the plugin setup. The code for worker.py is:

"""Demo netchat addition to Nautobot."""
import logging

from django_rq import job
from nautobot_chatops.workers import subcommand_of, handle_subcommands
from nautobot_chatops.choices import CommandStatusChoices

import meraki

logger = logging.getLogger("rq.worker")


@job("default")
def netchat(subcommand, **kwargs):
    """Interact with netchat."""
    return handle_subcommands("netchat", subcommand, **kwargs)


def get_meraki_orgs():
    """Query the Meraki Dashboard API for a list of defined organizations."""
    dashboard = meraki.DashboardAPI(suppress_logging=True)
    return dashboard.organizations.getOrganizations()


def meraki_devices(org_name):
    """Query the Meraki Dashboard API for a list of devices in the given organization."""
    # Get the org ID from the Get Meraki Orgs
    org_list = get_meraki_orgs()
    dashboard = meraki.DashboardAPI(suppress_logging=True)

    for org in org_list:
        if org["name"] == org_name:
            device_list = dashboard.organizations.getOrganizationDevices(organizationId=org["id"])
            return device_list

        return []


@subcommand_of("netchat")
def get_meraki_devices(dispatcher, org_name=None):
    """Gathers devices from Meraki API endpoint."""
    logger.info(f"ORG NAME: {org_name}")
    if not org_name:
        # The user didn't specify an organization, so prompt them to pick one
        org_list = get_meraki_orgs()
        
        # Build the list of sites, each as a pair of (user-visible string, internal value) entries
        choices = [(x["name"], x["name"]) for x in org_list]
        dispatcher.prompt_from_menu(f"netchat get-meraki-devices", "Select Organization", choices)
        
        # Returning False indicates that the command needed to prompt the user for more information 
        return False

    # If gathering information from another system may take some time, it's useful to send the user
    dispatcher.send_markdown(
        f"Stand by {dispatcher.user_mention()}, I'm getting the devices at the Organization {org_name}!"
    )

    devices = meraki_devices(org_name)
    
    # Render the list of devices to Markdown for display to the user's chat client
    blocks = [
        dispatcher.markdown_block(f"{dispatcher.user_mention()} here are the devices at {org_name}"),
        dispatcher.markdown_block("\n".join([x["name"] for x in devices])),
    ]

    dispatcher.send_blocks(blocks)

    return CommandStatusChoices.STATUS_SUCCEEDED

The important parts are, first, the function registration of the chat command:

@job("default")
def netchat(subcommand, **kwargs):
    """Interact with netchat."""
    return handle_subcommands("netchat", subcommand, **kwargs)

This registers the netchat slash command to the ChatOps plugin. It corresponds with the entry point registration. Note the docstring of the function is what will be displayed to the chat user when listing available commands.

The chat command itself is then handled by the next defined function get_meraki_devices. This function registers itself to be a subcommand of netchat The first argument must always be dispatcher, which is an object used for all interactions with the chat platform. The remaining arguments are potential user inputs to the chat command; here we have just one, the org_name argument. This is the part of the chat command `/netchat get-meraki-devices `. When a user first sends the command `/netchat get-meraki-devices`, the org_name is `None`, which allows for the function to gather a list of choices and present it back.

@subcommand_of("netchat")
def get_meraki_devices(dispatcher, org_name=None):
    """Gathers devices from Meraki API endpoint."""
    if not org_name:
        <gather organizations>

    # When org_name is passed in
    <do more with code>
    ... # More Code 

Through the prompts, the chat applications will not sort the data. In the example, everything is alphabetized because the code called for it to be alphabetized. Slack itself does not alphabetize the options. The text boxes are something that you can type in to filter for yourself.

Installation via Poetry

The final step after writing the code is that the package must be installed into the Nautobot environment. In this case since the package has not been published to PyPI, we will use Poetry to install it directly from source into the venv. This does require the virtual environment to be activated. As the Nautobot user:

cd ~
source bin/activate

Once activated, move to the plugin directory, use Poetry to add requirements (in this case typing-extensions and meraki), then install the local package to the virtual environment.

pwd
/opt/nautobot

cd plugins/netchat/
poetry add typing-extensions
poetry add meraki
poetry install

Once the netchat package is installed, exit out of the Nautobot user. Then restart the Nautobot application and Nautobot worker process. If there are access grants that are restricting access to the chatbot, be sure to make the necessary updates in Nautobot to allow the new slash command.

sudo systemctl restart nautobot nautobot-worker

With the workers restarted, the slash command should be available on the chat application!


Conclusion

The ChatOps plugin is an excellent starting point for your own custom chat application. As demonstrated by its built-in commands, you can write chat commands that interact directly with Nautobot. But the ChatOps plugin can do far more than that. With Python, the capabilities to interact with other third-party services are endless!



ntc img
ntc img

Contact Us to Learn More

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

Author