SR OS Ansible 'get' playbook

Use Ansible to get configuration and state information


Introduction

If you have followed the SR OS with Ansible - 101 article you have some familiarity with well structured Ansible playbooks.  Sometimes, however, it is useful to have a single, simple, Ansible playbook in your pocket to perform tasks that you may do regularly during development cycles.


This article provides one such simple playbook that can be executed to obtain configuration or state information from your SR OS device with model-driven mode enabled.


Equipment and tools

This article is written in the form of a tutorial. It assumes that you have access to one or more Nokia SR OS 7750/7950 routers (or vSIMs) and that you have a valid license for these products (if you need any of these, please contact your Nokia representative). It also assumes that you have access to a Linux machine from which to work (although it is possible to manage your SR OS devices from other host operating systems).


Equipment used for the creation of this article:


Item Version
Linux Machine CentOS 7 (3.10.0-957.12.2.el7.x86_64)
xmllint libxml version 20901
jq 1.5
Ansible 2.8.5
SR OS 19.10.R1


The playbook

Create sros-ansible-get.yml playbook shown below using the editor of your choice.  This playbook provides the ability to obtain SR OS configuration or state information using the NETCONF model-driven interface and output it in either JSON, XML or YAML formats.


Please note: The JSON interpreter in Ansible does not conform to RFC 7951 for the conversion of YANG modelled types to JSON and so the type definitions of elements should not be relied upon.


---
### Playbook runs on all hosts in the inventory regardless of their group
- hosts: all
  ### Do not gather facts from the devices
  gather_facts: false
  vars:
    ### Provide the username and password for the NETCONF SR OS user
    - ansible_ssh_user: netconf
    - ansible_ssh_pass: nokia123
    ### Streamline SSH operations.  Alter as required for a more secure environment
    - ansible_ssh_common_args: '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
    - ansible_host_key_auto_add: yes
    - ansible_host_key_checking: False
  tasks:

### JSON STATE
    - block:
      - name: Get state in JSON format
        connection: netconf
        netconf_get:
          display: json
          filter: <state xmlns="urn:nokia.com:sros:ns:yang:sr:state"/>
        register: netconf_facts_state_json
      - name: Output state info to file
        copy:
          content: "{{ netconf_facts_state_json.output['rpc-reply'].data | to_nice_json }}"
          dest: "{{ inventory_hostname}}.json"
        delegate_to: localhost
        when: netconf_facts_state_json is defined
      - debug:
          msg: "Warning: Ansible JSON processing does not conform to RFC 7951"
      - debug:
          msg: "{{ inventory_hostname }}.json created"
      tags: ['never','json-state','state-json']

### JSON CONFIG
    - block:
      - name: Get config in JSON format
        connection: netconf
        netconf_get:
          display: json
          filter: <configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"/>
        register: netconf_facts_config_json
      - name: Output config to file
        copy:
          content: "{{ netconf_facts_config_json.output['rpc-reply'].data | to_nice_json }}"
          dest: "{{ inventory_hostname}}.json"
        delegate_to: localhost
        when: netconf_facts_config_json is defined
      - debug:
          msg: "Warning: Ansible JSON processing does not conform to RFC 7951"
      - debug:
          msg: "{{ inventory_hostname }}.json created"
      tags: ['never','json-config','config-json']

### YAML STATE
    - block:
      - name: Get state in JSON format
        connection: netconf
        netconf_get:
          display: json
          filter: <state xmlns="urn:nokia.com:sros:ns:yang:sr:state"/>
        register: netconf_facts_state_json
      - name: Output state info to file converted to YAML
        copy:
          content: "{{ netconf_facts_state_json.output['rpc-reply'].data | to_nice_yaml }}"
          dest: "{{ inventory_hostname}}.yaml"
        delegate_to: localhost
        when: netconf_facts_state_json is defined
      - debug:
          msg: "Warning: Ansible JSON processing does not conform to RFC 7951 which will effect the YAML output"
      - debug:
          msg: "{{ inventory_hostname }}.yaml created"
      tags: ['never','yaml-state','state-yaml','yml-state','state-yml']

### YAML CONFIG
    - block:
      - name: Get config in JSON format
        connection: netconf
        netconf_get:
          display: json
          filter: <configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"/>
        register: netconf_facts_config_json
      - name: Output config to file converted to YAML
        copy:
          content: "{{ netconf_facts_config_json.output['rpc-reply'].data | to_nice_yaml }}"
          dest: "{{ inventory_hostname}}.yaml"
        delegate_to: localhost
        when: netconf_facts_config_json is defined
      - debug:
          msg: "Warning: Ansible JSON processing does not conform to RFC 7951 which will effect the YAML output"
      - debug:
          msg: "{{ inventory_hostname }}.yaml created"
      tags: ['never','yaml-config','config-yaml','yml-config','config-yml']

### XML STATE
    - block:
      - name: Get state in XML format
        connection: netconf
        netconf_get:
          display: xml
          filter: <state xmlns="urn:nokia.com:sros:ns:yang:sr:state"/>
        register: netconf_facts_state_xml
      - name: Pretty format XML
        shell:
          cmd: "xmllint --xpath /rpc-reply/data/state - | xmllint --format -"
          stdin: "{{ netconf_facts_state_xml.output }}"
        register: netconf_facts_state_xml_formatted
        delegate_to: localhost
      - name: Output state info to file
        copy:
          content: "{{ netconf_facts_state_xml_formatted.stdout }}"
          dest: "{{ inventory_hostname}}.xml"
        delegate_to: localhost
        when: netconf_facts_state_xml_formatted is defined
      - debug:
          msg: "{{ inventory_hostname }}.xml created"
      tags: ['never','xml-state','state-xml']

### XML CONFIG
    - block:
      - name: Get config in XML format
        connection: netconf
        netconf_get:
          display: xml
          filter: <configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"/>
        register: netconf_facts_config_xml
      - name: Pretty format XML
        shell:
          cmd: "xmllint --xpath /rpc-reply/data/configure - | xmllint --format -"
          stdin: "{{ netconf_facts_config_xml.output }}"
        register: netconf_facts_config_xml_formatted
        delegate_to: localhost
      - name: Output config info to file
        copy:
          content: "{{ netconf_facts_config_xml_formatted.stdout }}"
          dest: "{{ inventory_hostname}}.xml"
        delegate_to: localhost
        when: netconf_facts_config_xml_formatted is defined
      - debug:
          msg: "{{ inventory_hostname }}.xml created"
      tags: ['never','xml-config','config-xml']


The playbook is split into blocks and each block is selected at runtime by providing a tag to the ansible-playbook command.  Providing no tags will not execute any of the playbook and will return no output.


The following tags can be provided to the ansible-playbook command at runtime:


Tag Datastore Output Style
state-xml state Formatted XML
xml-state state Formatted XML
config-xml configuration Formatted XML
xml-config configuration Formatted XML
state-json state Formatted JSON
json-state state Formatted JSON
config-json configuration Formatted JSON
json-config configuration Formatted JSON
state-yaml state YAML
state-yml state YAML
yaml-state state YAML
yml-state state YAML
config-yaml configuration YAML
config-yml configuration YAML
yaml-config configuration YAML
yml-config configuration YAML


The tags are provided in either order to provide an enhanced user experience, for example, state-xml and xml-state.


The never tag in the playbook is a special tag to alter the default behaviour of Ansible.  By default, if no tags are specified at execution time then Ansible will execute all plays.  The addition of the never tag means that plays will only be executed if the tag is specifically provided.


Inventory file

The playbook takes a standard Ansible inventory file as it's input and will execute the required functions on all devices in the inventory.


The username and password of the SR OS user with NETCONF access can be defined in the playbook (as above with 'netconf' and 'nokia123' as the example) or within the inventory file on a per node basis (as shown for device 'test2' below).  These are defined as the fields ansible_ssh_user and ansible_ssh_pass respectively.


Example inventory file:


[routers]
test1	ansible_host=172.16.123.1
test2	ansible_host=172.16.123.2 ansible_ssh_user=netconf ansible_ssh_pass=nokia123


Running the playbook

Executing the playbook is performed using ansible-playbook command and passing it the inventory file and the tags you require (see above).  Use the following flags to pass the arguments:


  • -i <inventory-filename>
  • -t <tags>


Here is an example execution:


ansible-playbook -i inventory sros-ansible-get.yml -t json-config


Which yeilds the following output:


PLAY [all] *********************************************************************************************************************************************************

TASK [Get config in JSON format] ***********************************************************************************************************************************
ok: [test1]
ok: [test2]

TASK [Output config to file] ***************************************************************************************************************************************
changed: [test1 -> localhost]
changed: [test2 -> localhost]

TASK [debug] *******************************************************************************************************************************************************
ok: [test1] => {
    "msg": "Warning: Ansible JSON processing does not conform to RFC 7951"
}
ok: [test2] => {
    "msg": "Warning: Ansible JSON processing does not conform to RFC 7951"
}

TASK [debug] *******************************************************************************************************************************************************
ok: [test1] => {
    "msg": "test1.json created"
}
ok: [test2] => {
    "msg": "test2.json created"
}

PLAY RECAP *********************************************************************************************************************************************************
test1                      : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
test2                      : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0


The resultant output is a set of filenames with the same name as the router name in the inventory file.  Each file has the appropriate extension depending on the required output (.json, .yaml, .xml).



Something a little extra

Having generated these output files it is often useful to query the data (state or configuration) for specific branches or leaves.  


Using the JSON formatted output from this Ansible playbook and the jq tool available on most Linux distributions you can quickly integrate the information and return specific elements that you are interested in.  


Here are some examples that use the JSON output from the configuration of the test1 router:


Identify the configured hostname


jq '.configure.system.name' test1.json


Returns:


"test1"


Identify the configured NTP information


jq '.configure.system.time.ntp' test1.json


Returns:


{
  "admin-state": "enable",
  "authentication-check": "false",
  "server": {
    "ip-address": "172.16.123.254",
    "router-instance": "management"
  }
}


Here are some examples that use the JSON output from the state of the test1 router:


Obtain all user statistics on the router


jq '.state.system.security["user-params"]["local-user"]' test1.json


Returns:


{
  "user": [
    {
      "attempted-logins": "11",
      "failed-logins": "0",
      "locked-out": "false",
      "password-changed-time": "2019-11-01T16:22:31.0Z",
      "user-name": "admin"
    },
    {
      "attempted-logins": "0",
      "failed-logins": "0",
      "locked-out": "false",
      "password-changed-time": "2019-11-01T16:22:33.0Z",
      "user-name": "grpc"
    },
    {
      "attempted-logins": "1477",
      "failed-logins": "0",
      "locked-out": "false",
      "password-changed-time": "2019-11-01T16:22:33.0Z",
      "user-name": "netconf"
    }
  ]
}


Please note:  Many JSON tools, including jq, are unable to parse the '-' character without quoting it.  This is the reason that in the above example the jq filter is '.state.system.security["user-params"]["local-user"]' rather than '.state.system.security.user-params.local-user'