vRouter Management Automation with Ansible and NETCONF

In a recent blog, we talked about provisioning for bare metal and VM platforms leveraging PXE and cloud-init. Once the vRouter is booted, it can be managed via its NETCONF API using automation tools such as Ansible, Python scripting or higher level orchestration tools.

In this blog post, I will present a practical example of Ansible usage with the vRouter NETCONF API. Ansible is an open-source software provisioning, configuration management, and application deployment tool written in Python. It supports the NETCONF protocol since version 2.4.0.

 

Preparation

Ansible is not a provisioning tool, it requires the machines it will configure to be booted and accessible on the network (NETCONF uses TCP port 830). You will find detailed instructions in 6WIND vRouter Getting Started Guide. I have booted 2 vRouter instances into 6WIND’s development network and I gave them DNS hostnames for clarity purposes.

Both machines have two physical network interfaces. One is used for management and the other one is used for production traffic. Mind that this is not a real world use case; I oversimplified it to make the example easier to grasp. Here is an overview of the setup:

Both management interfaces have already been configured automatically on boot by cloud-init and DHCP. The “production” interfaces have the same physical port identifier (pci-b0s4) and are named int0 and ext0 for vrouter1 and vrouter2 respectively. I want to use Ansible to configure the IP addresses of these “production” interfaces and the hostnames of both machines.

To avoid messing up my system packages, I chose to install Ansible into a python virtualenv. In order to support executing arbitrary NETCONF RPCs, Ansible version greater than 2.7.10 along with the additional ncclient and jxmlease python libraries are required.

$ python3 --version
Python 3.5.3
$ python3 -m venv /tmp/ansible-netconf
$ . /tmp/ansible-netconf/bin/activate
$ which pip python
/tmp/ansible-netconf/bin/pip
/tmp/ansible-netconf/bin/python
$ pip install -U pip setuptools wheel
...
Successfully installed pip-19.1.1 setuptools-41.0.1 wheel-0.33.4
$ pip install "ansible > 2.7.10" ncclient jxmlease
...
Successfully installed MarkupSafe-1.1.1 PyYAML-5.1 ansible-2.8.0 asn1crypto-0.24.0 bcrypt-3.1.6 cffi-1.12.3 cryptography-2.6.1 jinja2-2.10.1 jxmlease-1.0.1 lxml-4.3.3 ncclient-0.6.4 paramiko-2.4.2 pyasn1-0.4.5 pycparser-2.19 pynacl-1.3.0 six-1.12.0

 

Configuration

Inventory

We need an inventory file that will reference all machines that we want to control with Ansible. Here we are using the YAML inventory format which is more readable than the default INI format.

# /tmp/ansible-netconf/hosts.yml
---
vrouters:
  vars:
    ansible_connection: netconf
    ansible_user: admin
    ansible_ssh_pass: admin      # using default admin user/password
    ansible_python_interpreter: python
  hosts:
    vrouter1:
      peer: vrouter2
      ifname: int0
      port: pci-b0s4
      ipaddr: 172.16.200.1
    vrouter2:
      peer: vrouter1
      ifname: ext0
      port: pci-b0s4
      ipaddr: 172.16.200.2

Playbook

We also need to write a playbook. Here is a basic example that configures the hostname depending on the Ansible inventory name, and that configures a physical interface on both machines. Then, it runs the ping NETCONF RPC to check that the IP addresses have been properly configured on both machines.

In the playbook, I use Ansible built-in netconf_get, netconf_config and netconf_rpc modules.

# /tmp/ansible-netconf/playbook.yml
---
- hosts: vrouters
  gather_facts: false  # facts gathering is not supported at the moment
  tasks:
    - name: fetch initial state
      netconf_get:
        display: json
        filter: "{{lookup('file', 'filter.xml')}}"
      register: state

    - name: print initial state
      debug:
        var: state.output.data

    - name: configure
      netconf_config:
        content: "{{lookup('template', 'config.xml')}}"

    - name: fetch state again
      netconf_get:
        display: json
        filter: "{{lookup('file', 'filter.xml')}}"
      register: state

    - name: print state after configuration has been applied
      debug:
        var: state.output.data

    - name: check connection both ways
      netconf_rpc:
        rpc: ping
        display: json
        xmlns: 'urn:6wind:vrouter/system'
        content: |
          <count>1</count>
          <destination>{{hostvars[peer].ipaddr}}</destination>
      register: ping

    - name: print ping outputs
      debug:
        msg: "{{ping.output['nc:rpc-reply']['buffer'].splitlines()}}"

    - name: unset hostname
      netconf_config:
        content: |
          <config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
            <config xmlns="urn:6wind:vrouter">
              <system xmlns="urn:6wind:vrouter/system">
                <hostname xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete"/>
              </system>
            </config>
          </config>

    - name: change ipv4 address (not add a new one)
      netconf_config:
        content: |
          <config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
            <config xmlns="urn:6wind:vrouter">
              <vrf>
                <name>main</name>
                <interface xmlns="urn:6wind:vrouter/interface">
                  <physical>
                    <name>{{ifname}}</name>
                    <ipv4 xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="replace">
                      <address>
                        <ip>{{ipaddr}}00/24</ip>
                      </address>
                    </ipv4>
                  </physical>
                </interface>
              </vrf>
            </config>
          </config>

    - name: fetch state again
      netconf_get:
        display: json
        filter: "{{lookup('file', 'filter.xml')}}"
      register: state

    - name: print state after configuration has been modified
      debug:
        var: state.output.data

    - name: check connection both ways (again)
      netconf_rpc:
        rpc: ping
        display: json
        xmlns: 'urn:6wind:vrouter/system'
        content: |
          <count>1</count>
          <destination>{{hostvars[peer].ipaddr}}00</destination>
      register: ping

    - name: print ping outputs
      debug:
        msg: "{{ping.output['nc:rpc-reply']['buffer'].splitlines()}}"

Two additional XML files are referenced by the playbook via the lookup template functions. They should be placed next to the playbook file itself.

 

Config

<!-- /tmp/ansible-netconf/config.xml -->
<config xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <config xmlns="urn:6wind:vrouter">
    <system xmlns="urn:6wind:vrouter/system">
      <hostname>{{inventory_hostname}}</hostname>
    </system>
    <vrf>
      <name>main</name>
      <interface xmlns="urn:6wind:vrouter/interface">
        <physical>
          <name>{{ifname}}</name>
          <port>{{port}}</port>
          <ipv4>
            <address>
              <ip>{{ipaddr}}/24</ip>
            </address>
          </ipv4>
        </physical>
      </interface>
    </vrf>
  </config>
</config>

The structure of config.xml may be generated by running the following CLI commands directly on one of the vRouters:

localhost> edit running
localhost running config# system hostname vrouter2
localhost running config# vrf main interface physical ext0 port pci-b0s4 ipv4 address 172.16.200.2/24
localhost running config# show config xml absolute nodefault
<config xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname>vrouter2</hostname>
  </system>
  <vrf>
    <name>main</name>
    <interface xmlns="urn:6wind:vrouter/interface">
      <physical>
        <name>ext0</name>
        <port>pci-b0s4</port>
        <ipv4>
          <address>
            <ip>172.16.200.2/24</ip>
          </address>
        </ipv4>
      </physical>
    </interface>
  </vrf>
</config>

By default, the contents of the <config> XML node are merged with the current configuration. This is explained extensively in RFC 6241, Section 7.2..

In order to replace or delete some parts of the configuration, the operation XML attribute must be specified on the related XML nodes. The example playbook makes use of this attribute to unset a previously set hostname and replace an IPv4 address.

 

Filter

<!-- /tmp/ansible-netconf/filter.xml -->
<state xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname/>
  </system>
  <vrf>
    <name>main</name>
    <interface xmlns="urn:6wind:vrouter/interface">
      <physical>
        <name/>
        <ipv4>
          <address/>
        </ipv4>
        <port/>
        <oper-status/>
      </physical>
    </interface>
  </vrf>
</state>

The structure of filter.xml may be generated from combining the output of the following CLI commands:

localhost> show state xml absolute nodefault system hostname
<state xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname>localhost</hostname>
  </system>
</state>
localhost> show state xml absolute nodefault vrf main interface physical ens3
<state xmlns="urn:6wind:vrouter">
  <vrf>
    <name>main</name>
    <interface xmlns="urn:6wind:vrouter/interface">
      <physical>
        <name>ens3</name>
        <ipv4>
          <address>
...

The playbook.yml and config.xml files contain templating placeholders that will be replaced by respective host variables when the playbook is executed. See Ansible official documentation for more details.

 

Execution

Once all these files are created, let’s run ansible-playbook as follows:

$ ansible-playbook -i /tmp/ansible-netconf/hosts.yml /tmp/ansible-netconf/playbook.yml

PLAY [vrouters] *************************************************************

TASK [fetch initial state] **************************************************
ok: [vrouter1]
ok: [vrouter2]

TASK [print initial state] **************************************************
ok: [vrouter2] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "localhost"
            },
            "vrf": {
                "interface": {
                    "physical": [
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "10.0.2.16/24"
                                }
                            },
                            "name": "ens3",
                            "oper-status": "UP",
                            "port": "pci-b0s3"
                        },
                        {
                            "name": "ens4",
                            "oper-status": "DOWN",
                            "port": "pci-b0s4"
                        }
                    ]
                },
                "name": "main"
            }
        }
    }
}
ok: [vrouter1] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "localhost"
            },
            "vrf": {
                "interface": {
                    "physical": [
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "10.0.2.15/24"
                                }
                            },
                            "name": "ens3",
                            "oper-status": "UP",
                            "port": "pci-b0s3"
                        },
                        {
                            "name": "ens4",
                            "oper-status": "DOWN",
                            "port": "pci-b0s4"
                        }
                    ]
                },
                "name": "main"
            }
        }
    }
}

TASK [configure] ************************************************************
changed: [vrouter2]
changed: [vrouter1]

TASK [fetch state again] ****************************************************
ok: [vrouter1]
ok: [vrouter2]

TASK [print state after configuration has been applied] *********************
ok: [vrouter2] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "vrouter2"
            },
            "vrf": {
                "interface": {
                    "physical": [
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "10.0.2.16/24"
                                }
                            },
                            "name": "ens3",
                            "oper-status": "UP",
                            "port": "pci-b0s3"
                        },
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "172.16.200.2/24"
                                }
                            },
                            "name": "ext0",
                            "oper-status": "UP",
                            "port": "pci-b0s4"
                        }
                    ]
                },
                "name": "main"
            }
        }
    }
}
ok: [vrouter1] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "vrouter1"
            },
            "vrf": {
                "interface": {
                    "physical": [
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "10.0.2.15/24"
                                }
                            },
                            "name": "ens3",
                            "oper-status": "UP",
                            "port": "pci-b0s3"
                        },
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "172.16.200.1/24"
                                }
                            },
                            "name": "int0",
                            "oper-status": "UP",
                            "port": "pci-b0s4"
                        }
                    ]
                },
                "name": "main"
            }
        }
    }
}

TASK [check connection both ways] *******************************************
ok: [vrouter1]
ok: [vrouter2]

TASK [print ping outputs] ***************************************************
ok: [vrouter2] => {
    "msg": [
        "PING 172.16.200.1 (172.16.200.1) 56(84) bytes of data.",
        "64 bytes from 172.16.200.1: icmp_seq=1 ttl=64 time=0.652 ms",
        "",
        "--- 172.16.200.1 ping statistics ---",
        "1 packets transmitted, 1 received, 0% packet loss, time 0ms",
        "rtt min/avg/max/mdev = 0.652/0.652/0.652/0.000 ms"
    ]
}
ok: [vrouter1] => {
    "msg": [
        "PING 172.16.200.2 (172.16.200.2) 56(84) bytes of data.",
        "64 bytes from 172.16.200.2: icmp_seq=1 ttl=64 time=0.758 ms",
        "",
        "--- 172.16.200.2 ping statistics ---",
        "1 packets transmitted, 1 received, 0% packet loss, time 0ms",
        "rtt min/avg/max/mdev = 0.758/0.758/0.758/0.000 ms"
    ]
}

TASK [unset hostname] *******************************************************
changed: [vrouter2]
changed: [vrouter1]

TASK [change ipv4 address (not add a new one)] ******************************
changed: [vrouter2]
changed: [vrouter1]

TASK [fetch state again] ****************************************************
ok: [vrouter1]
ok: [vrouter2]

TASK [print state after configuration has been modified] ********************
ok: [vrouter1] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "vrouter1"
            },
            "vrf": {
                "interface": {
                    "physical": [
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "10.0.2.15/24"
                                }
                            },
                            "name": "ens3",
                            "oper-status": "UP",
                            "port": "pci-b0s3"
                        },
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "172.16.200.100/24"
                                }
                            },
                            "name": "int0",
                            "oper-status": "UP",
                            "port": "pci-b0s4"
                        }
                    ]
                },
                "name": "main"
            }
        }
    }
}
ok: [vrouter2] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "vrouter2"
            },
            "vrf": {
                "interface": {
                    "physical": [
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "10.0.2.16/24"
                                }
                            },
                            "name": "ens3",
                            "oper-status": "UP",
                            "port": "pci-b0s3"
                        },
                        {
                            "ipv4": {
                                "address": {
                                    "ip": "172.16.200.200/24"
                                }
                            },
                            "name": "ext0",
                            "oper-status": "UP",
                            "port": "pci-b0s4"
                        }
                    ]
                },
                "name": "main"
            }
        }
    }
}

TASK [check connection both ways (again)] ***********************************
ok: [vrouter1]
ok: [vrouter2]

TASK [print ping outputs] ***************************************************
ok: [vrouter1] => {
    "msg": [
        "PING 172.16.200.200 (172.16.200.200) 56(84) bytes of data.",
        "64 bytes from 172.16.200.200: icmp_seq=1 ttl=64 time=1.07 ms",
        "",
        "--- 172.16.200.200 ping statistics ---",
        "1 packets transmitted, 1 received, 0% packet loss, time 0ms",
        "rtt min/avg/max/mdev = 1.076/1.076/1.076/0.000 ms"
    ]
}
ok: [vrouter2] => {
    "msg": [
        "PING 172.16.200.100 (172.16.200.100) 56(84) bytes of data.",
        "64 bytes from 172.16.200.100: icmp_seq=1 ttl=64 time=10.1 ms",
        "",
        "--- 172.16.200.100 ping statistics ---",
        "1 packets transmitted, 1 received, 0% packet loss, time 0ms",
        "rtt min/avg/max/mdev = 10.119/10.119/10.119/0.000 ms"
    ]
}

PLAY RECAP ******************************************************************
vrouter1: ok=13  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
vrouter2: ok=13  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

Conclusion

I hope this example gave you some perspective about what can be done with Ansible and the vRouter NETCONF API. Of course, I only scratched the surface and real world use cases will require more complexity. A lot more information is available in Ansible official documentation and 6WIND vRouter NETCONF API documentation.

Feel free to contact us if you have any further questions or to request an evaluation. We will be happy to hear from you.


Robin Jarry is a Software Engineer at 6WIND.

Leave a Reply