NSO has a ton of features. This repository is the first of a series which will show a simple use case, along with a feature. The purpose is dual:
This example is using:
Interface Loopback Config
and the features I am showcasing are:
RESTCONF Ansible Module
This example assumes a working knowledge of Ansible and NSO.
Reserve the NSO Reservable Sandbox
If you need to revisit some of the NSO Basics, you can start here.
Use some type of file transfer program or VS Code has remote SSH (drag and drop the package into the packages directory)
Log into the devbox (IP: 10.10.20.50) and install the RESTCONF Ansible collection:
ansible-galaxy collection install ansible.netcommon
Clone or copy this repo onto the Devbox (IP: 10.10.20.50) home directory. Change to that directory to use the playbooks.
$ git clone https://github.com/jabelk/ansible-nso-restconf.git
Cloning into 'ansible-nso-restconf'...
remote: Enumerating objects: 14, done.
remote: Counting objects: 100% (14/14), done.
remote: Compressing objects: 100% (11/11), done.
Unpacking objects: 100% (14/14), done.
remote: Total 14 (delta 0), reused 14 (delta 0), pack-reused 0
$ cd ansible-nso-restconf/
ansible-nso-restconf$
The playbooks are split up into a build
and push
two-phase approach. This is meant to mimic the mindset of Infra as Code, where the config is generated, edited, reviewed, tested and then pushed as separate steps.
To run the playbooks, ensure you are in the directory with the playbooks in them and then use the ansible-playbook
command:
ansible-playbook pb_restconf_get_build.yaml
The playbook will run, generating the vars/int_vars.yaml
file. That file is already present in the repo, so you can delete it and the int_vars_to_push.yaml
files if you want to simulate creating the files from scratch.
Next, if you deleted the int_vars_to_push.yaml
file to simulate needing to make it yourself, you will need to copy and paste the Loopback YAML into a new int_vars_to_push.yaml
file and edit it.
The point is that you don't need to write YAML from scratch because the int_vars.yaml
creates the YAML structure for you, and all you need to do is copy and paste the part you want to merge into the device.
Next, to push the new config, run the next playbook
ansible-playbook pb_restconf_push.yaml
The config will be pushed and some output will be shown.
Even though there are already official Ansible NSO Modules, there are good reasons why you might want to use the Ansible RESTCONF modules with NSO.
The official NSO Ansible modules use the JSON-RPC API, which allows for fancy commit options, and more nerd nobs. The downside is that many network engineers are not familiar with JSON-RPC, but are familiar with REST and/or RESTCONF. This means troubleshooting and development might be easier if using the RESTCONF modules. Also, currently the JSON-RPC Ansible modules accept JSON and not YAML as an input payload, which adds another layer of complexity in translating files written and maintained in YAML into JSON before sending them off.
Just as a point of clarification for those who do not already know, we are using the NSO RESTCONF API to configure the application, which then in turn uses the NSO NEDs to sculpt CLI commands to the device. We are not using RESTCONF to talk directly to devices. Since NSO conforms to the IETF standard way of responding to RESTCONF calls, we can simply reuse the RESTCONF Ansible modules to configure NSO.
We are using two files with variables (apart from the two int_vars*
files we build during the playbook execution), group_vars/all.yaml
and inventory.yaml
The group vars file allows us to define the credentials, and a few other relevant HTTP based settings to have our playbooks be clean and simple.
--- # Connectivity parameters ansible_connection: "httpapi" ansible_network_os: "restconf" ansible_httpapi_use_ssl: true ansible_httpapi_validate_certs: false ansible_httpapi_port: 443 ansible_httpapi_restconf_root: "/restconf/data/" # default ansible_user: "developer" ansible_password: "C1sco12345"
The inventory file simply has the IP address and inventory hostname.
--- all: children: nso_server: hosts: latest: ansible_host: "10.10.20.49" ...
I used the following template to and a Jinja filter to transform our JSON response from NSO into YAML:
{{ int_config | to_nice_yaml }}
The "build" playbook uses the RESTCONF GET module to get the interface config from dist-rtr01
and store that in a restconf_result
variable. We then extract out the response
payload data and plug that into our Jinja template to transform it into YAML.
- name: BUILD CONFIG WITH RESTCONF GET AND NSO hosts: "nso_server" gather_facts: no tasks: - name: RESTCONF GET CONFIG FROM DIST-RTR01 FROM NSO ansible.netcommon.restconf_get: path: "/tailf-ncs:devices/device=dist-rtr01/config/tailf-ned-cisco-ios:interface/" register: restconf_result - name: VIEW DEVICE INTERFACE CONFIG debug: var=restconf_result - name: SET FACT TO EXTRACT RESPONSE VALUE set_fact: int_config: "{{ restconf_result.response }}" - name: USE JINJA2 TO TRANSFORM JSON OUTPUT TO YAML FILE template: lstrip_blocks: yes src: json2yaml.j2 dest: vars/int_vars.yaml # then create vars/int_vars_to_push.yaml file manually and copy and paste the Loopback YAML # in there to have config to work with to push with next playbook # Loopback: # - description: to # ip: # no-address: # address: false # name: '0' # shutdown: # - null
The assumption is, if this was run the first time, with no files in vars/
, it would generate a YAML file with all the interface config. Then you could take the loopback config from the generated file:
tailf-ned-cisco-ios:interface: GigabitEthernet: - description: to port6.sandbox-backend ip: address: primary: address: 10.10.20.175 mask: 255.255.255.0 mop: sysid: false xenabled: false name: '1' negotiation: auto: true vrf: forwarding: Mgmt-intf - description: L3 Link to core-rtr01 ip: address: primary: address: 172.16.252.21 mask: 255.255.255.252 mop: sysid: false xenabled: false name: '2' negotiation: auto: true - description: L3 Link to core-rtr02 ip: address: primary: address: 172.16.252.25 mask: 255.255.255.252 mop: sysid: false xenabled: false name: '3' negotiation: auto: true - description: L3 Link to dist-sw01 ip: address: primary: address: 172.16.252.2 mask: 255.255.255.252 mop: sysid: false xenabled: false name: '4' negotiation: auto: true - description: L3 Link to dist-sw02 ip: address: primary: address: 172.16.252.10 mask: 255.255.255.252 mop: sysid: false xenabled: false name: '5' negotiation: auto: true - description: L3 Link to dist-rtr02 ip: address: primary: address: 172.16.252.17 mask: 255.255.255.252 mop: sysid: false xenabled: false name: '6' negotiation: auto: true Loopback: - description: to ip: no-address: address: false name: '0' shutdown: - null
and edit it to add a description and new loopback number in int_vars_to_push.yaml
:
tailf-ned-cisco-ios:interface: Loopback: - description: "DESCRIPTION BY ANSIBLE" ip: no-address: address: false name: '100' shutdown: - null
The push playbook assumes the int_vars_to_push.yaml
file is ready to go. It loads in the variable file, then uses RESTCONF PATCH to dist-rtr01
to push the new loopback config. There is then a task to print out the new interface list with the new loopback.
Finally, I added a task to delete the loopback, just to show how that would be done, and also so when the playbook was run multiple times for testing, it would always create the loopback.
- name: PUSH CONFIG WITH RESTCONF CONFIG AND NSO hosts: "nso_server" gather_facts: no tasks: - name: LOAD LOOPBACK CONFIG include_vars: file: int_vars_to_push.yaml name: int_vars_file - name: ADD LOOPBACK 100 TO NSO AND DEVICE register: restconf_result ansible.netcommon.restconf_config: path: "/tailf-ncs:devices/device=dist-rtr01/config/tailf-ned-cisco-ios:interface" method: "patch" content: "{{ int_vars_file | to_json }}" - name: VERIFY NEW LOOPBACK IS PRESENT ansible.netcommon.restconf_get: path: "/tailf-ncs:devices/device=dist-rtr01/config/tailf-ned-cisco-ios:interface/" register: restconf_result # used .get() to grab key to avoid expected token 'end of print statement', got ':' error - name: VIEW DEVICE INTERFACE CONFIG debug: var: "restconf_result.response.get('tailf-ned-cisco-ios:interface').Loopback" - name: DELETE LOOPBACK 100 FROM NSO AND DEVICE register: restconf_result ansible.netcommon.restconf_config: path: "/tailf-ncs:devices/device=dist-rtr01/config/tailf-ned-cisco-ios:interface/Loopback=100" method: "delete"
(py3venv) [developer@devbox ansible-nso-restconf]$ ansible-playbook pb_restconf_get_build.yaml PLAY [BUILD CONFIG WITH RESTCONF GET AND NSO] ************************************************************************************** TASK [RESTCONF GET CONFIG FROM DIST-RTR01 FROM NSO] ******************************************************************************** ok: [latest] TASK [VIEW DEVICE INTERFACE CONFIG] ************************************************************************************************ ok: [latest] => { "restconf_result": { "changed": false, "failed": false, "response": { "tailf-ned-cisco-ios:interface": { "GigabitEthernet": [ { "description": "to port6.sandbox-backend", "ip": { "address": { "primary": { "address": "10.10.20.175", "mask": "255.255.255.0" } } }, "mop": { "sysid": false, "xenabled": false }, "name": "1", "negotiation": { "auto": true }, "vrf": { "forwarding": "Mgmt-intf" } }, { "description": "L3 Link to core-rtr01", "ip": { "address": { "primary": { "address": "172.16.252.21", "mask": "255.255.255.252" } } }, "mop": { "sysid": false, "xenabled": false }, "name": "2", "negotiation": { "auto": true } }, { "description": "L3 Link to core-rtr02", "ip": { "address": { "primary": { "address": "172.16.252.25", "mask": "255.255.255.252" } } }, "mop": { "sysid": false, "xenabled": false }, "name": "3", "negotiation": { "auto": true } }, { "description": "L3 Link to dist-sw01", "ip": { "address": { "primary": { "address": "172.16.252.2", "mask": "255.255.255.252" } } }, "mop": { "sysid": false, "xenabled": false }, "name": "4", "negotiation": { "auto": true } }, { "description": "L3 Link to dist-sw02", "ip": { "address": { "primary": { "address": "172.16.252.10", "mask": "255.255.255.252" } } }, "mop": { "sysid": false, "xenabled": false }, "name": "5", "negotiation": { "auto": true } }, { "description": "L3 Link to dist-rtr02", "ip": { "address": { "primary": { "address": "172.16.252.17", "mask": "255.255.255.252" } } }, "mop": { "sysid": false, "xenabled": false }, "name": "6", "negotiation": { "auto": true } } ], "Loopback": [ { "description": "to", "ip": { "no-address": { "address": false } }, "name": "0", "shutdown": [ null ] } ] } } } } TASK [SET FACT TO EXTRACT RESPONSE VALUE] ****************************************************************************************** ok: [latest] TASK [USE JINJA2 TO TRANSFORM JSON OUTPUT TO YAML FILE] **************************************************************************** ok: [latest] PLAY RECAP ************************************************************************************************************************* latest : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 (py3venv) [developer@devbox ansible-nso-restconf]$
(py3venv) [developer@devbox ansible-nso-restconf]$ ansible-playbook pb_restconf_push.yaml PLAY [PUSH CONFIG WITH RESTCONF CONFIG AND NSO] ************************************************************************************ TASK [LOAD LOOPBACK CONFIG] ******************************************************************************************************** ok: [latest] TASK [ADD LOOPBACK 100 TO NSO AND DEVICE] ****************************************************************************************** changed: [latest] TASK [VERIFY NEW LOOPBACK IS PRESENT] ********************************************************************************************** ok: [latest] TASK [VIEW DEVICE INTERFACE CONFIG] ************************************************************************************************ ok: [latest] => { "restconf_result.response.get('tailf-ned-cisco-ios:interface').Loopback": [ { "description": "to", "ip": { "no-address": { "address": false } }, "name": "0", "shutdown": [ null ] }, { "description": "DESCRIPTION BY ANSIBLE", "ip": { "no-address": { "address": false } }, "name": "100", "shutdown": [ null ] } ] } TASK [DELETE LOOPBACK 100 FROM NSO AND DEVICE] ************************************************************************************* changed: [latest] PLAY RECAP ************************************************************************************************************************* latest : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 (py3venv) [developer@devbox ansible-nso-restconf]$
Owner
Contributors
Categories
Products
Network Services Orchestrator (NSO)Programming Languages
HTMLLicense
Code Exchange Community
Get help, share code, and collaborate with other developers in the Code Exchange community.View Community