Ansible NETCONF Automation

Ansible supports configuring remote hosts using NETCONF (instead of the default SSH connection along with Linux shell commands). This guide explains how to leverage Ansible to configure multiple Turbo Router instances.

Dependencies

This guide assumes that you have two (or more) Turbo Router instances that are booted and accessible on the network (NETCONF uses TCP port 830). Also, for clarity purposes, these machines should be reachable with their respective hostnames (thus, DNS or /etc/hosts must be configured accordingly).

To make sure it works, ansible version greater than 2.7.10 along with the ncclient and jxmlease python libraries are required. Here is how to install this in a python virtualenv:

$ python3 -m venv /tmp/ansible-netconf
$ . /tmp/ansible-netconf/bin/activate
$ which python
/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.

# /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}}/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()}}"

See also

The official Ansible documentation of the netconf_get, netconf_config and netconf_rpc modules.

Two additional XML files are referenced. 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:

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>

Important

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/>
    <product xmlns="urn:6wind:vrouter/system/product"/>
  </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
<state xmlns="urn:6wind:vrouter">
  <system xmlns="urn:6wind:vrouter/system">
    <hostname>localhost</hostname>
...
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>
...

Note

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, you may 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",
                "product": {
                    "license": "valid",
                    "name": "Turbo Router",
                    "version": "3.1"
                }
            },
            "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"
            }
        }
    }
}
ok: [vrouter1] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "localhost",
                "product": {
                    "license": "valid",
                    "name": "Turbo Router",
                    "version": "3.1"
                }
            },
            "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",
                "product": {
                    "license": "valid",
                    "name": "Turbo Router",
                    "version": "3.1"
                }
            },
            "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.2/24"
                                }
                            },
                            "name": "ext0",
                            "oper-status": "UP",
                            "port": "pci-b0s4"
                        }
                    ]
                },
                "name": "main"
            }
        }
    }
}
ok: [vrouter1] => {
    "state.output.data": {
        "state": {
            "system": {
                "hostname": "vrouter1",
                "product": {
                    "license": "valid",
                    "name": "Turbo Router",
                    "version": "3.1"
                }
            },
            "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",
                "product": {
                    "license": "unknown",
                    "name": "Turbo Router",
                    "version": "3.1"
                }
            },
            "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",
                "product": {
                    "license": "unknown",
                    "name": "Turbo Router",
                    "version": "3.1"
                }
            },
            "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.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

Additional examples

To copy the running configuration in startup, use:

- name: copy running startup
  netconf_rpc:
    rpc: copy-config
    xmlns: 'urn:ietf:params:xml:ns:netconf:base:1.0'
    content: |
      <target><startup/></target>
      <source><running/></source>

To delete the startup configuration, use:

- name: delete startup
  netconf_rpc:
    rpc: delete-config
    xmlns: 'urn:ietf:params:xml:ns:netconf:base:1.0'
    content: |
      <target><startup/></target>