Avatar

How encoding and transport of the data-model gives you power and flexibility

In the first blog, I attempted to make the point that the declarative approach of using function-specific modules in Ansible is not scalable. In the second blog, I introduced data models into the conversation to help organize all of the key/value pairs that define your network. In this blog, I’ll explain how the encoding and transport of the data-model give us quite a bit of power and flexibility when paired with Ansible.

Data Models help organize the key/value pairs that define the network and YANG gives us a way to describe the meaning of the key/value pairs in the data structure, but we still need to communicate that information to the devices. This is where NETCONF comes in. NETCONF gives us several operational advantages over CLI, including:

  • multiple configuration data stores (e.g. candidate, running, startup)
  • configuration validation and testing
  • differentiation between configuration and state data

For the purposes of this blog, however, the combination of YANG and NETCONF give us structure and determinism to enable programmability. As an example, let’s consider the problem from the first blog: maintaining NTP server lists. This is what the ntp server configuration looks like in IOS CLI syntax:

line vty 2 4
 transport input ssh
!
ntp server 1.1.1.1
ntp server 2.2.2.2
!

I left the ‘line vty’ block to illustrate that the ntp configuration is just thrown in there at the root level. After all, the CLI was created for the humans and the humans are messy.

To illustrate the advantages of the model-driven method, let’s use netconf-console to get and set the NTP servers on an IOS-XE device. First, let’s see what our NTP servers are currently set to:

# netconf-console --host <device IP> --port 830 --user admin --password admin --db running --get-config --xpath /native/ntp

<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
  <native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
    <ntp>
      <server xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-ntp">
        <server-list>
          <ip-address>1.1.1.1</ip-address>
        </server-list>
        <server-list>
          <ip-address>2.2.2.2</ip-address>
        </server-list>
      </server>
    </ntp>
  </native>
</data>

While this is a wordier rendering of the same configuration, it is deterministic. All of the ntp servers and their associated configuration are organized into one section of the tree. We can deal with it as a separate entity. Also, note that we were able to ask the device for just the ntp configuration information. No parsing required.

Now let’s change our NTP servers. First, we take the previous output, change the IP address of the second NTP server, and specify that this operation should replace the server section. All of this goes into a file named ntp.xml:

<native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
  <ntp>
    <server xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-ntp" operation='replace'>
      <server-list>
        <ip-address>1.1.1.1</ip-address>
      </server-list>
      <server-list>
        <ip-address>3.3.3.3</ip-address>
      </server-list>
    </server>
  </ntp>
</native>

Next, we push the XML payload to the device:

# netconf-console --host <device IP> --port 830 --user admin --password admin --db running --edit-config ntp.xml
<ok xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"/>

The device accepted the change, so let’s look at the result:

# netconf-console --host <device IP> --port 830 --user admin --password admin --db running --get-config --xpath /native/ntp
<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
  <native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
    <ntp>
      <server xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-ntp">
        <server-list>
          <ip-address>1.1.1.1</ip-address>
        </server-list>
        <server-list>
          <ip-address>3.3.3.3</ip-address>
        </server-list>
      </server>
    </ntp>
  </native>
</data>

Huzzah! We’ve taken a change that normally takes less than a minute to do by hand and turned it into a 10-minute excursion. Welcome to programmability. You see, programmability by itself is useless to the humans. It’s great that we can use netconf-console or gin up some python code to push and pull data to a single device, but it provides us no operational value. That is why this blog series is entitled “Automating Your Network Operations” and not “How to Make your Network Operations 10 Times Slower with Programmability”.

This is where Ansible comes back in. Ansible is an automation tool. There are many automation tools that we could use, but Ansible is my preferred tool because it is more simple to use than other tools. Now simplicity is a double-edged sword. To achieve this simplicity, Ansible had to make compromises over more capable languages. Nested loops, for example, are a real pain in Ansible. That is why the model-driven approach is best for Ansible. It simplifies the use case such that it fits nicely within Ansible’s capabilities. Let’s look at how we’d do this same task using the netconf_rpc module in Ansible.

- hosts: routers
  connection: netconf
  gather_facts: no
  tasks:
    - netconf_rpc:
        rpc: edit-config
        content: |
          <target>
            <running/>
          </target>
          <config>
            <native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
              <ntp>
                <server xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-ntp" operation='replace'>
                  <server-list>
                    <ip-address>1.1.1.1</ip-address>
                  </server-list>
                  <server-list>
                    <ip-address>3.3.3.3</ip-address>
                  </server-list>
                </server>
              </ntp>
            </native>
          </config>

Great! We are using Ansible, so we are automating, right? We are one step closer. Ansible’s inventory capabilities provide the mechanism to run this playbook over multiple devices. However, this playbook is neither portable nor reusable. We really want to separate out the key/value pairs embedded in the playbook from the tasks. We’ll go deeper into Ansible’s inventory capabilities in the next blog, but let’s start by putting the NTP servers into a simple data structure:

ntp_servers:
  - 1.1.1.1
  - 3.3.3.3

To use this data structure, we create a Jinja2 template to create the XML payload for the NETCONF call and put it into a file named ntp.j2:

<target>
  <running/>
</target>
<config>
  <native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
    <ntp>
      <server xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-ntp" operation='replace'>
{% for server in ntp_servers %}
        <server-list>
          <ip-address>{{ server }}</ip-address>
        </server-list>
{% endfor %}
      </server>
    </ntp>
  </native>
</config>

This template statically specifies the XML from above but dynamically adds the servers from the ntp_servers list, allowing code re-use by simply changing the data instead of the playbook. Then we use the lookup plug-in to process the template before we pass it into the netconf_rpc module:

- hosts: routers
  connection: netconf
  gather_facts: no
  tasks:
    - netconf_rpc:
        rpc: edit-config
        content: "{{ lookup('template', 'ntp-netconf.j2') }}"

The addition of Jinja2 gives us a powerful templating language to dynamically create XML payloads for NETCONF. It also adds more programmatic tools (like nested loops) for processing our data models to reduce the consumption of adult beverages needed to write a given playbook. Essentially, we take the model, encode it into an XML payload with a Jinja2 template, then send it via NETCONF with the netconf_rpc module. We can use this same technique, changing the encoding and the transport, to deliver our data model in a variety of ways to support nearly any device:

Automating Network Operations

As an example of how we can change the encoding and transport to deliver the same data model in a different way, here is a Jinja2 template that encodes it into IOS-XE CLI:

{% for server in ntp_servers %}
ntp server {{ server }}
{% endfor %}

Then we use the cli_command module to deliver the payload via SSH:

- hosts: routers
  connection: network_cli
  gather_facts: no
  tasks:
    - cli_config:
        config: "{{ lookup('template', 'ntp-cli.j2') }}"

This is the framework that we’ll use in this blog series for taking models and delivering them to devices. NTP is a simple example, but this framework will work with any deployment, as we’ll see as this blog series progresses. In the next blog, I’ll dig into the specifics of how we store our inventory and data for Ansible to reference and how to make our playbooks reusable, portable, and testable with Ansible Roles. In the meantime, I encourage you to check out more NETCONF training at Cisco DevNet and test NETCONF with your devices using netconf-console.

Take advantage of all that DevNet has to offer. Get your free DevNet account for access to network automation resources, learning labs, docs, and sandboxes.