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 Virtual Service Router instances.
Dependencies¶
This guide assumes that you have two (or more) Virtual Service 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
---
routers:
vars:
ansible_connection: netconf
ansible_user: admin
ansible_ssh_pass: admin # using default admin user/password
ansible_python_interpreter: python
hosts:
vsr1:
peer: vsr2
ifname: int0
port: pci-b0s4
ipaddr: 172.16.200.1
vsr2:
peer: vsr1
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: routers
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 vsr2
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>vsr2</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 [routers] **************************************************************
TASK [fetch initial state] **************************************************
ok: [vsr1]
ok: [vsr2]
TASK [print initial state] **************************************************
ok: [vsr2] => {
"state.output.data": {
"state": {
"system": {
"hostname": "localhost",
"product": {
"license": "valid",
"name": "Virtual Service Router",
"version": "3.5"
}
},
"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: [vsr1] => {
"state.output.data": {
"state": {
"system": {
"hostname": "localhost",
"product": {
"license": "valid",
"name": "Virtual Service Router",
"version": "3.5"
}
},
"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: [vsr2]
changed: [vsr1]
TASK [fetch state again] ****************************************************
ok: [vsr1]
ok: [vsr2]
TASK [print state after configuration has been applied] *********************
ok: [vsr2] => {
"state.output.data": {
"state": {
"system": {
"hostname": "vsr2",
"product": {
"license": "valid",
"name": "Virtual Service Router",
"version": "3.5"
}
},
"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: [vsr1] => {
"state.output.data": {
"state": {
"system": {
"hostname": "vsr1",
"product": {
"license": "valid",
"name": "Virtual Service Router",
"version": "3.5"
}
},
"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: [vsr1]
ok: [vsr2]
TASK [print ping outputs] ***************************************************
ok: [vsr2] => {
"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: [vsr1] => {
"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: [vsr2]
changed: [vsr1]
TASK [change ipv4 address (not add a new one)] ******************************
changed: [vsr2]
changed: [vsr1]
TASK [fetch state again] ****************************************************
ok: [vsr1]
ok: [vsr2]
TASK [print state after configuration has been modified] ********************
ok: [vsr1] => {
"state.output.data": {
"state": {
"system": {
"hostname": "vsr1",
"product": {
"license": "unknown",
"name": "Virtual Service Router",
"version": "3.5"
}
},
"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: [vsr2] => {
"state.output.data": {
"state": {
"system": {
"hostname": "vsr2",
"product": {
"license": "unknown",
"name": "Virtual Service Router",
"version": "3.5"
}
},
"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: [vsr1]
ok: [vsr2]
TASK [print ping outputs] ***************************************************
ok: [vsr1] => {
"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: [vsr2] => {
"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 ******************************************************************
vsr1: ok=13 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
vsr2: 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>