Introducing Design Builder: Design Driven Network Automation

Blog Detail

Most people involved in network automation are familiar with the concept of a Source of Truth (SoT). The SoT is usually some form of database that maintains intended state of objects as well as their interdependency. The SoT provides a way to quickly ascertain what a network’s intended state should be, while often providing a way to see what the network’s state actually is. A new concept is emerging, known as Design Oriented Source of Truth. This idea takes network designs and codifies them, attaching additional meaning to the objects within the SoT. Nautobot is a source of truth that contains all sorts of information about a network’s state. Although many of the pieces of information within Nautobot are related, they are discretely managed. A new Nautobot App aims to simplify the process of codifying network designs and populating Nautobot objects based on these designs.

Introduction

It is very common to have a small set of standardized designs that are used to deploy many sites and services in enterprise networks. For example, branch office sites may have a few different designs depending on their size. There could be a design that uses a single branch office router for small sites. Another design could have two routers and an access switch for sites with a moderate user base. A third design could include a more complex switching infrastructure for sites with many employees. When companies do tech refreshes or new site builds, these standardized designs are used and new data must be created in the source of truth. The newly open-sourced Design Builder application was created to address this problem, and fulfills the idea that a standardized design can be taken from a network engineer and transformed into a format that can be consumed and executed by Nautobot. Design Builder can expand a minimal set of inputs into a full-fledged set of configuration objects within Nautobot. This includes any kind of data object that Nautobot can model. Everything from Rack and Device objects to IP addresses and BGP peering information.

Design Builder provides powerful mechanisms that make simple designs possible. The first is the ability to represent interrelated data in a meaningful hierarchy. For example, devices have interfaces and interfaces have IP addresses. Conceptually this seems like a very simple structure. However, if we were to manually use the REST API or ORM to handle creating objects like this, we would first have to create a device object and keep its ID in memory. We would then have to create interfaces with their device foreign-key set to the device ID we just created. Finally, we’d have to save all of the interface IDs and do the same with IP addresses. Design Builder provides a means to represent objects in YAML and produce their representation within the Nautobot database. A typical design workflow follows the following diagram:

Following this process, we can produce YAML files that intuitively represent the structure of the data we want to create. An example of a Design Builder YAML design can be seen in the following YAML document:

devices:
  - name: "Router 1"
    status__name: "Active"
    interfaces:
      - name: "GigabitEthernet0"
        type: "1000base-t"
        status__name: "Active"
        ip_addresses:
          - address: "192.168.0.1/24"
            status__name: "Active"

This YAML document would produce a single device, with a single Gigabit Ethernet interface. The interface itself has a single IP address. As demonstrated in the example, Design Builder automatically associates the parent/child relationships correctly, and there is no need to keep copies of primary and foreign keys. We can visually represent this YAML design with the following diagram:

Design Builder also provides a system to query for existing related objects using some attribute of the associated object. In the above example, the status field is actually a related object. Statuses are not just simple strings, they are first-class objects within the Nautobot database. In this case, the Status object with the name Active is predefined in Nautobot and does not need to be created. It does, however, need to be associated with the Device, the Interface, and the IPAddress objects.

This object relationship is actually a foreign-key relationship in the database and ORM. If we were using the Django ORM to associate objects, we would first need to look up the status before creating the associated objects. Design Builder provides a way to perform that lookup as part of the model hierarchy. Note that we’re looking up the status by its name: status__name. Design Builder has adopted similar syntax to Django’s field lookup. The field name and related field are separated by double underscores.

Use Cases

There are many use cases that are covered by the Design Builder, but we will highlight a very simple one in this post. Our example use case handles the creation of edge site designs within Nautobot. This use case is often seen when doing tech refreshes or new site build-outs.

Engineers commonly need to add a completely new set of data for a site. This could be the result of a project to refresh a site’s network infrastructure or it could be part of deploying a new site entirely. Even with small sites, the number of objects needing to be created or updated in Nautobot could be dozens or even hundreds. However, if a standardized design is developed then Design Builder can be used to auto-populate all of the data for new or refreshed sites.

Consider the following design, which will create a new site with edge routers, a single /24 prefix and two circuits for the site:

---
sites:
  - name: "LWM1"
    status__name: "Staging"
    prefixes:
      - prefix: "10.37.27.0/24"
        status__name: "Reserved"
    devices:
      - name: "LWM1-LR1"
        status__name: "Planned"
        device_type__model: "C8300-1N1S-6T"
        device_role__name: "Edge Router"
        interfaces:
          - name: "GigabitEthernet0/0"
            type: "1000base-t"
            description: "Uplink to backbone"
            status__name: "Planned"
      - name: "LWM1-LR2"
        status__name: "Planned"
        device_type__model: "C8300-1N1S-6T"
        device_role__name: "Edge Router"      
        interfaces:
          - name: "GigabitEthernet0/0"
            type: "1000base-t"
            description: "Uplink to backbone"
            status__name: "Planned"

circuits:
  - cid: "LWM1-CKT-1"
    status__name: "Planned"
    provider__name: "NTC"
    type__name: "Ethernet"
    terminations:
      - term_side: "A"
        site__name: "LWM1"
      - term_side: "Z"
        provider_network__name: "NTC-WAN"

  - cid: "LWM1-CKT-2"
    status__name: "Planned"
    provider__name: "NTC"
    type__name: "Ethernet"
    terminations:
      - term_side: "A"
        site__name: "LWM1"
      - term_side: "Z"
        provider_network__name: "NTC-WAN"

This is still quite a bit of information to write. Luckily, the Design Builder application can consume Jinja templates to produce the design files. Using some Jinja templating, we can reduce the above design a bit:


---
sites:
  - name: "LWM1"
    status__name: "Staging"
    prefixes:
      - prefix: "10.37.27.0/24"
        status__name: "Reserved"
    devices:
    {% for i in range(2) %}
      - name: "LWM1-LR{{ i }}"
        status__name: "Planned"
        device_type__model: "C8300-1N1S-6T"
        device_role__name: "Edge Router"
        interfaces:
          - name: "GigabitEthernet0/0"
            type: "1000base-t"
            description: "Uplink to backbone"
            status__name: "Planned"
    {% endfor %}
circuits:
  {% for i in range(2) %}
  - cid: "LWM1-CKT-{{ i }}"
    status__name: "Planned"
    provider__name: "NTC"
    type__name: "Ethernet"
    terminations:
      - term_side: "A"
        site__name: "LWM1"
      - term_side: "Z"
        provider_network__name: "NTC-WAN"
  {% endfor %}

The above design file gets closer to a re-usable design. It has reduced the amount of information we have to represent by leveraging Jinja2 control structures, but there is still statically defined information. At the moment, the design includes hard coded site information (for the site name, device names and circuit IDs) as well as a hard coded IP prefix. Design Builder also provides a way for this information to be gathered dynamically. Fundamentally, all designs are just Nautobot Jobs. Therefore, a design Job can include user-supplied vars that are then copied into the Jinja2 render context. Consider the design job for our edge site design:

class EdgeDesign(DesignJob):
    """A basic design for design builder."""
    site_name = StringVar(label="Site Name", regex=r"\w{3}\d+")
    site_prefix = IPNetworkVar(label="Site Prefix")

#...

This design Job collects a site_name variable as well as a site_prefix variable from the user. Users provide values for these variables through the normal Job launch entrypoint:

Once the job has been launched, the Design Builder will provide these input variables to the Jinja rendering context. The variable names, within the jinja2 template, will match the attribute names used in the Design Job class. With the site_name and site_prefix variables now being defined dynamically, we can produce a final design document using them:

---

sites:
  - name: "{{ site_name }}"
    status__name: "Staging"
    prefixes:
      - prefix: "{{ site_prefix }}"
        status__name: "Reserved"
    devices:
    {% for i in range(2) %}
      - name: "{{ site_name }}-LR{{ i }}"
        status__name: "Planned"
        device_type__model: "C8300-1N1S-6T"
        device_role__name: "Edge Router"
        interfaces:
          - name: "GigabitEthernet0/0"
            type: "1000base-t"
            description: "Uplink to backbone"
            status__name: "Planned"
    {% endfor %}
circuits:
  {% for i in range(2) %}
  - cid: "{{ site_name }}-CKT-{{ i }}"
    status__name: "Planned"
    provider__name: "NTC"
    type__name: "Ethernet"
    terminations:
      - term_side: "A"
        site__name: "{{ site_name }}"
      - term_side: "Z"
        provider_network__name: "NTC-WAN"
  {% endfor %}

The design render context is actually much more flexible than simple user entry via script vars. Design Builder provides a complete system for managing the render context, including loading variables from YAML files and providing dynamic content via Python code. The official documentation covers all of the capabilities of the design context.

In addition to the YAML rendering capabilities, Design Builder includes a way to perform just-in-time operations while creating and updating Nautobot objects. For instance, in the above example, the site prefix is specified by the user that launches the job. It may be desirable for this prefix to be auto-assigned and provisioned out of a larger parent prefix. Design Builder provides a means to perform these just-in-time lookups and calculations in the form of something called an “action tag”. Action tags are evaluated during the object creation phase of a design’s implementation. That means that database lookups can occur and computations can take place as the design is being implemented. One of the provided action tags is the next_prefix action tag. This tag accepts query parameters to find a parent prefix, and also a parameter that specifies the length of the required new prefix. For example, if we want to provision a /24 prefix from the 10.0.0.0/16 parent, we could use the following:

prefixes:
  - "!next_prefix":
      prefix: "10.0.0.0/16"
      length: 24
    status__name: "Active"

The next_prefix action tag will find the parent prefix 10.0.00/16 and look for the first available /24 in that parent. Once found, Design Builder will create that child prefix with the status Active.

Several action tags are provided out of the box, but one of the most powerful features of Design Builder is the ability to include custom action tags in a design. Action tags are implemented in Python as specialized classes, and can perform any operation necessary to produce a just-in-time result.

There is quite a lot to understand with Design Builder, and we have only touched on a few of its capabilities. While there are several moving parts, the following diagram illustrates the high-level process that the Design Builder application uses to go from design files and templates to an implemented design.

Design Builder starts with some optional input variables from the Nautobot job and combines them with optional context variables written either in YAML or Python or both. This render context is used by the Jinja2 renderer to resolve variable names in Jinja2 templates. The Jinja2 templates are rendered into YAML documents that are unmarshaled as Python dictionaries and provided to the Builder. The Builder iterates all of the objects in this dictionary and performs necessary database creations and updates. In the process of creating and updating objects, any action tags that are present are evaluated. The final result is a set of objects in Nautobot that have been created or updated by Design Builder.

Roadmap

Our plans for Design Builder are far from over. There are many more features we’re currently working on, as well as some that are still in the planning stages. Some of the near-term features include design lifecycle and object protection.

The design lifecycle feature allows the implementations of a design to be tracked. Design instances can be created (such as an instance of the edge site design above) and can be subsequently decommissioned. Objects that belong to a design instance will be reverted to their state prior to the design implementation, or they may be removed entirely (if created specifically for a design). Designs can also track inter-design dependencies so that a design cannot be decommissioned if other design instances depend on it. The design lifecycle feature will also allow designs to be versioned so that an implementation can be updated over time.

The ability to protect objects that belong to a design is also planned. The idea is that if an object is created as part of a design implementation, any attributes that were initially set in this design cannot be updated outside of that design’s lifecycle. This object protection assures that our source of truth has data that complies with a design and prevents manually introduced errors.


Conclusion

Design Builder is a great tool that ensures your network designs are used for every deployment, and simplifies populating data in Nautobot along the way. It provides a streamlined way to represent hierarchical relationships with a clear syntax and concepts that should be familiar to those that have started to embark on their NetDevOps journey. I encourage you to try it out.

-Andrew, Christian and Paddy



ntc img
ntc img

Contact Us to Learn More

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

Developing Nautobot Plugins – Part 1

Blog Detail

This post is the first installment in a series on how to write plugins for Nautobot. Nautobot plugins are a way to extend the base functionality of Nautobot. Plugins can extend the database schema, add custom pages, and even update existing pages within Nautobot; the possibilities are nearly endless. In this blog series we will be developing a plugin for modeling and managing DNS zone data within Nautobot. We will cover setting up a development environment, creating models and views, and more advanced topics such as query filters and GraphQL integration.

This first post will provide an overview of Nautobot plugins and will cover getting started, including setting up a development environment and creating the basic source code layout for a new plugin.

What Is a Nautobot Plugin

Nautobot is an open-source project that provides a Network Source of Truth (NSOT). While the core Nautobot application provides many, if not most, of the data models needed to manage network automation and resources, no tool set can cover every use case that an organization may encounter. Therefore, Nautobot includes the ability to extend its base data models and views to fit any use case.

One aspect of network data modeling that is not included out of the box is the Domain Name System (DNS) record management. While many organizations have other tools for managing their DNS, smaller organizations may not. In this blog series we will demonstrate how to write a plugin that provides the data models and views for managing DNS records.

Nautobot itself is implemented as a Django web application, and Django is a web framework implemented in Python. Django provides many out-of-the-box features including the concept of “applications”. A Django web application can be composed of many other discrete subapplications. In the case of Nautobot, plugins are essentially implemented as Django applications. However, Nautobot provides many extensions to Django to facilitate the development process.

Before any plugin development can take place, a suitable development environment must first be set up. The next section covers setting up the development environment and launching Nautobot with a basic plugin structure.

Development Environment Prerequisites

For our development environment we will be using Visual Studio Code (VS Code)Poetry, and Docker. Additionally, since Nautobot is written in Python a local installation of Python is necessary, and the minimum required version of Python is 3.7. Follow the linked instructions to install VS CodePython, and Docker.

Poetry is used in the plugin to manage Python dependencies and also to provide a virtual environment for development. Poetry is used in many projects at Network to Code and is often used in Nautobot plugin development. This blog series will use Poetry to manage the dependencies in the demo plugin. Instructions for installing Poetry can be found here.

Once the development tools have been installed, the source code can be loaded into the development environment.

Project Source Code

All of the code for this blog series is located in the GitHub repository. Each article in the series is tagged in the source repository to facilitate diffing the code as the blog progresses. For this installment the source code is tagged part1. In order to clone the source code repo, VS Code will need to be configured for Git and GitHub. Instructions for setting up VS Code for GitHub can be found here.

Once VS Code has been set up to use Git and GitHub, select the “Source Control” tab from the Activity Bar and click the “Clone Repository” button.

Paste the repository URL (https://github.com/networktocode-llc/nautobot-example-dns-manager.git) into the URL text box and VS Code will then prompt for a destination folder to store the local copy. Finally, once the repo has been cloned, open the source code by responding when prompted to “Open the cloned repository.”

Once the code has been cloned and opened in VS Code, the local repository will need to be switched to the part1 tag. Open the command palette (Shift+Command+P on Mac or Ctrl+Shift+P on Windows and Linux) and select Git: Checkout to... then select the tag part1. This will switch the local repository to match the code for part 1.

Now that the local git repo is on the correct tag, open a terminal window within VS Code and run the command poetry shell. This command will initialize the Poetry virtual environment within the project. Upon creating the virtual environment, VS Code should provide a prompt to use the newly created virtual environment, click the “yes” button:

If Visual Studio does not prompt you to change the virtual environment, you can manually select the project’s virtual environment from the command palette (Shift+Command+P on Mac or Ctrl+Shift+P on Windows and Linux), select Python: Select Interpreter, and then either enter the path to the virtual environment’s python or select it from the list.

Once the virtual environment has been activated, run the command poetry install to install all of the dependencies. One of the dependencies that will be installed is invokeInvoke is used for managing command line tasks, similar in concept to make and Makefile. The plugin source code includes an invoke tasks.py that includes all the tasks necessary to bootstrap and run a set of Docker images required for the Nautobot application stack. This includes a PostgreSQL database, a Redis server, a Nautobot Celery worker, a Nautobot Celery Beat worker, and a Nautobot application server. A complete discussion of all the components in a Nautobot installation is outside the scope of this tutorial. For additional information please see the Nautobot official documentation.

Several environment variables must be set before the development instance of Nautobot can be started. An example environment file is provided in development/creds.example.env. Before starting the plugin’s development environment, copy the file development/creds.example.env to development/creds.env. Docker will load this file and export the values into the running container as environment variables. On a local development system it is safe to use the defaults in this file. However, never use the example values for any production instance.

Once the environment file has been copied or created, start the application stack by running the command invoke build debug. This command will build the necessary Docker images and start all of the required containers using docker-compose. The debug target instructs invoke to follow the container logs in the running terminal. Note that Docker must be installed and running for this invoke command to succeed. After issuing the invoke command you should see logging messages similar to the following:

$ invoke debug
Starting Nautobot in debug mode...
Running docker-compose command "up"
Creating network "nautobot_example_dns_manager_default" with the default driver
Creating volume "nautobot_example_dns_manager_postgres_data" with default driver
Creating nautobot_example_dns_manager_db_1   ... 
Creating nautobot_example_dns_managerr_redis_1 ... 
Creating nautobot_example_dns_manager_db_1    ... done
Creating nautobot_example_dns_manager_redis_1 ... done
Creating nautobot_example_dns_manager_nautobot_1 ... 
Creating nautobot_example_dns_manager_nautobot_1 ... done
Creating nautobot_example_dns_manager_worker_1   ... 
Creating nautobot_example_dns_manager_worker_1   ... done
Attaching to nautobot_example_dns_manager_db_1, nautobot_example_dns_manager_redis_1, nautobot_example_dns_manager_nautobot_1, nautobot_example_dns_manager_worker_1
...
...
nautobot_1  | Django version 3.2.14, using settings 'nautobot_config'
nautobot_1  | Starting development server at http://0.0.0.0:8080/
nautobot_1  | Quit the server with CONTROL-C.
nautobot_1  |   Nautobot initialized!

Once you see the message Nautobot initialized! you should be able to navigate to http://127.0.0.1:8080/ and see the Nautobot dashboard. Log in using the admin credentials set in the development/creds.env environment file and click the Plugins menu item, then click Installed Plugins.

The new plugin should be displayed in the list of installed plugins:

Nautobot Plugin Files

Most Nautobot plugins have very similar directory structures. The minimum structure required for a plugin is a directory matching the plugin’s package name and an __init__.py file containing the plugin configuration. Our plugin will start with the absolute minimum requirements:

├── development/   # Configuration files for the plugin's local Nautobot instance.
|                  # Most of the files in this directory can be left alone
|
├── nautobot_example_dns_manager/
│   └── __init__.py  # Plugin configuration

The file development/nautobot_config.py is a standard config file for Nautobot. Any plugins you intend to use with Nautobot (including the one under development) must be enabled in the config. For instance:

PLUGINS = ["nautobot_example_dns_manager"]

Nautobot will attempt to load the plugin by importing the plugin’s package. Once the package has been imported, Nautobot will look for the variable config within the package. This variable should be assigned a class that extends Nautobot’s PluginConfig class. Our plugin defines the following in the nautobot_example_dns_manager/__init__.py file:

class NautobotExampleDNSManagerConfig(PluginConfig):
    """Plugin configuration for the nautobot_example_dns_manager plugin."""

    name = "nautobot_example_dns_manager"
    verbose_name = "Nautobot Example DNS Manager"
    version = __version__
    author = "Network to Code, LLC"
    description = "Nautobot Example DNS Manager."
    base_url = "example-dns-manager"
    required_settings = []
    min_version = "1.4.0"
    max_version = "1.9999"
    default_settings = {}
    caching_config = {}


config = NautobotExampleDNSManagerConfig

This config class has a number of different attributes. If a plugin depends on a feature introduced in a specific version of Nautobot, then the min_version should reflect that. For instance, Nautobot introduced dynamic groups in version 1.3.0. If a plugin depends on dynamic groups, then the min_version must be set to 1.3.0.

The required_settings and default_settings are two of the more commonly modified attributes. The value of required_settings should be a list of configuration keys that are required to be present in the nautobot_config.py file. Likewise, default_settings is a dictionary that includes default values for the required settings. If required settings are added to the plugin, the nautobot_config.py will need to be updated accordingly:

PLUGINS_CONFIG = {
    'nautobot_example_dns_manager': {
        'setting1': 'value1',
        # ...
    }
}

References

The following list provides some additional information and resources for Nautobot development:


Conclusion

In this blog post we have introduced Nautobot plugins and provided an overview of their use. We’ve setup a working development environment and cloned the source repo to our local development system. We’ve also configured the environment and invoked the Nautobot application stack. At this point, a running instance of Nautobot should be enabled with a new plugin installed.

Now that our development environment is squared away, we can get to writing the core of our plugin with Models, closely followed by Views, URLs, and Navigation. By the end of the next article, the plugin will begin to take shape and we’ll be able to see our changes in the GUI.



ntc img
ntc img

Contact Us to Learn More

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