parsing XML configuration output in ansible

5,733 views
Skip to first unread message

Jeff Loughridge

unread,
Feb 8, 2017, 3:16:24 PM2/8/17
to Junos Python EZ

I want to get the primary loopback address of a router in ansible. Is there an easy way to do this using only the Juniper.junos role and built-in modules?

My solution seems verbose and sub-optimal. I had hoped that registering a variable in the  junos_get_config or junos_rpc modules would give me access to the rpc-reply. I wasn't able to make that work.

- name: Get primary lo0 address
  hosts: all
  connection: local
  gather_facts: no
  roles:
    - Juniper.junos
    - cmprescott.xml
  vars:
    temp_dir: /tmp
    USER: jeffl
    PASSWORD: pass123

  tasks:
    - name: create temporary filename
      shell:  mktemp "{{ temp_dir }}"/`date +"%Y%m%d%d%m"`"_{{ inventory_hostname }}".XXXXX.xml
      register: temp_file

    - name: Execute get_config RPC
      junos_rpc:
        host={{ inventory_hostname }}
        user={{ USER }}
        port=22
        passwd={{ PASSWORD }}
        rpc=get_config
        logfile=logs/{{ inventory_hostname }}_cli.log
        dest="{{ temp_file.stdout }}"
        format=xml
        filter_xml="<configuration><interfaces><interface><name>lo0</name></interface></interfaces></configuration>"

    - name: Parse XML
      xml:
        file={{ temp_file.stdout }}
        xpath=//unit[name='0']/family/inet/address/primary/preceding-sibling::name
        content=text
      register: loopback_address_match
      failed_when: loopback_address_match['matches'] is not defined

    - name: Set fact
      set_fact:
        loopback: "{{ loopback_address_match['matches'][0]['name'] }}"

    - name: Print fact
      debug:
        var: loopback

Nitin Kumar

unread,
Feb 9, 2017, 6:15:02 AM2/9/17
to Jeff Loughridge, Junos Python EZ
Hi Jeff,

It would be easier for you if you use format as JSON. You can use role module junos_rpc or even core junos_command for the same
refer 

Once the data is in JSON its easy to loop through to fetch loopback address (Saving all the task needed for opening file->reading and using xpath to fetch details)

Do let me know if you face nay problem.

Nitin Kr

--
You received this message because you are subscribed to the Google Groups "Junos Python EZ" group.
To unsubscribe from this group and stop receiving emails from it, send an email to junos-python-ez+unsubscribe@googlegroups.com.
Visit this group at https://groups.google.com/group/junos-python-ez.
To view this discussion on the web visit https://groups.google.com/d/msgid/junos-python-ez/47a3e890-17d3-4d55-ba58-2795cb458728%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jeff Loughridge

unread,
Feb 9, 2017, 8:27:46 AM2/9/17
to Junos Python EZ, jef...@gmail.com

Thanks, Nitin. I should have mentioned in my original post that the target devices all run code older than 14.2. The JSON solution is nice, and I would use it for networks with 14.2+.

After reading your post, I found pull request PR 192, in which Stacy Smith explains that the built-in junos_command can output XML to jxmlease. I'll use this technique so that the playbook works with older code.
Nitin Kr

To unsubscribe from this group and stop receiving emails from it, send an email to junos-python-...@googlegroups.com.

Stacy W. Smith

unread,
Feb 9, 2017, 1:47:54 PM2/9/17
to Jeff Loughridge, Junos Python EZ
Hi Jeff,

If you are using Ansible >= 2.1, then you can take advantage of the junos_facts module which is provided with Ansible Core. Unlike the junos_get_facts module provided in the Juniper.junos role, the core junos_facts module can retrieve the configuration and pass it through jxmlease so that you get the configuration as a data structure.

Here's an example of how you might use this to parse out the lo0 address from the configuration:

---
- name: Get lo0 address from Junos routers
  hosts: junos-all
  connection: local
  gather_facts: no
  tasks:
    - name: Get Junos Facts
      junos_facts:
        host: "{{ inventory_hostname }}"
        username: "user"
        password: "user123"
        config: yes
        config_format: xml
    - name: Parse lo0 from config
      set_fact:
        lo0: "{{ item['unit']['family']['inet']['address']['name'] }}"
      with_items: "{{ config_json['configuration']['interfaces']['interface'] }}"
      when: item.name == "lo0"
    - name: Print lo0
      debug:
        var: lo0

I admit that retrieving information out of this complex data structure is cumbersome and you might prefer parsing the XML with an XPath as you've already done.

--Stacy

Jeff Loughridge

unread,
Feb 9, 2017, 2:20:42 PM2/9/17
to Junos Python EZ, jef...@gmail.com
Stacy,

Thank you. I noticed that junos_get_facts didn't include the loopack. I didn't know that the junos_facts module does.

You are right about the difficultly of parsing the data structure. I tried for a while this morning to parse the output of junos_command for the primary lo0 address. I couldn't figure it out.

I'll probably stick with using xpath via the cmprescott.xml role. Xpath is well-documented, and it's easy to check your expression using validators such as http://www.xpathtester.com/xpath.

Jeff L.

Stacy W. Smith

unread,
Feb 9, 2017, 6:23:48 PM2/9/17
to Jeff Loughridge, Junos Python EZ
On Feb 9, 2017, at 12:20 PM, Jeff Loughridge <jef...@gmail.com> wrote:
Thank you. I noticed that junos_get_facts didn't include the loopack. I didn't know that the junos_facts module does.

The junos_facts module doesn't directly include the loopback. It includes the configuration, and you can parse the loopback from the configuration.

You are right about the difficultly of parsing the data structure. I tried for a while this morning to parse the output of junos_command for the primary lo0 address. I couldn't figure it out.

FWIW, I do prefer getting the information from the operational state of the device rather than parsing the configuration. That's because:
1) Parsing the config correctly takes extra effort. You need to account for apply-groups, commit scripts, ephemeral database, committed vs. candidate config, etc.
2) Parsing the configuration means the user must be in a login class which has permissions to view the configuration.

If you were to use junos_command, one option looks like this:

---
- name: Get lo0 address from Junos routers
  hosts: junos-all
  connection: local
  gather_facts: no
  tasks:
    - name: Get lo0 IP
      junos_command:

        host: "{{ inventory_hostname }}"
        username: "user"
        password: "user123"
        rpcs:
          - "get-interface-information interface_name=lo0.0 terse=True"
        format: xml
      register: result
    - name: Parse lo0 IP from output
      set_fact:
        lo0_ip: "{{ item['interface-address']['ifa-local'] }}"
      with_items: "{{ result.stdout[0]['interface-information']['logical-interface']['address-family'] }}"
      when: item['address-family-name'] == "inet"
    - name: Print lo0 IP
      debug:
        var: lo0_ip


I'll probably stick with using xpath via the cmprescott.xml role. Xpath is well-documented, and it's easy to check your expression using validators such as http://www.xpathtester.com/xpath.

I personally prefer the XML/XPath route as well. 


You can avoid a temporary file if you use junos_command with XML, like this:
---
- name: Get lo0 address from Junos routers
  hosts: junos-all
  connection: local
  gather_facts: no
  roles:
    - cmprescott.xml
  tasks:
    - name: Get lo0 IP
      junos_command:

        host: "{{ inventory_hostname }}"
        username: "user"
        password: "user123"
        rpcs:
          - "get-interface-information interface_name=lo0.0 terse=True"
        format: xml
      register: result
    - name: Parse the lo0 IP
      xml:
        xmlstring: "{{ result.xml[0] }}"
        xpath: "./logical-interface[name='lo0.0']/address-family[address-family-name='inet']/interface-address/ifa-local"
        content: 'text'
      register: result
      failed_when: result['matches'] is not defined
    - name: Set the lo0 IP Fact
      set_fact:
          lo0_ip: "{{ result['matches'][0]['ifa-local'] }}"
    - name: Print the lo0 IP
      debug:
        var: lo0_ip

--Stacy


Jeff Loughridge

unread,
Feb 11, 2017, 1:43:01 PM2/11/17
to Junos Python EZ
Stacy,

I'll switch to querying operational state for the reasons you mentioned.

Looks like Ansible 2.3 dropped support for XML output from junos_command. Issue #20265.

I'll using junos_rpc instead. Thanks for all the help.

Jeff L.

zach shivers

unread,
Feb 2, 2018, 1:49:53 PM2/2/18
to Junos Python EZ
Stacy,

I'm trying to implement your code to test.
I've tried multiple ways but seems I can not get xmlstring to work.

NOTE:  The xml role is in ansible now ( https://docs.ansible.com/ansible/2.4/xml_module.html )

I keep getting this error:

fatal: [10.63.64.96]: FAILED! => {
    "msg": "The task includes an option with an undefined variable. The error was: 'dict object' has no attribute 'xml'\n\nThe error appears to have been in '/home/automation/ansible/playbooks/get_loopback_ip_address.yml': line 19, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n      register: result\n    - name: Parse the lo0 IP\n      ^ here\n\nexception type: <class 'ansible.errors.AnsibleUndefinedVariable'>\nexception: 'dict object' has no attribute 'xml'"
}



See below my relevant code:
---
- name: Get lo0 address from Junos routers
  hosts: lab-acx-3
  connection: local
  gather_facts: no
  roles:
    - Juniper.junos
  tasks:
    - name: Get lo0 IP
      junos_command:

        host: "{{ inventory_hostname }}"
        username: "authorized_user"
        password: "authorized_password"
        rpcs:
          - "get-interface-information interface_name=lo0.0 terse=True"
        format: xml
      register: result
    - name: Parse the lo0 IP
      xml:
        xmlstring: "{{ result.xml[0] }}"
        xpath: "./logical-interface[name='lo0.0']/address-family[address-family-name='inet']/interface-address/ifa-local"
        content: 'text'
      register: result
      failed_when: result['matches'] is not defined
    - name: Set the lo0 IP Fact
      set_fact:
          lo0_ip: "{{ result['matches'][0]['ifa-local'] }}"
    - name: Print the lo0 IP
      debug:
        var: lo0_ip

zach shivers

unread,
Feb 2, 2018, 1:59:18 PM2/2/18
to Junos Python EZ
When I comment out the following, the playbook runs without error and the debug prints the value of result variable
#    - name: Parse the lo0 IP
#      xml:
#        xmlstring: "{{ result.xml[0] }}"
#        xpath: "./logical-interface[name='lo0.0']/address-family[address-family-name='inet']/interface-address/ifa-local"
#        content: 'text'
#      register: result
#      failed_when: result['matches'] is not defined
#    - name: Set the lo0 IP Fact
#      set_fact:
#          lo0_ip: "{{ result['matches'][0]['ifa-local'] }}"
#    - name: Print the lo0 IP
#      debug:
#        var: lo0_ip


Debug output

PLAY [Get lo0 address from Junos routers] ************************************************************************************************************************************************************************************************

TASK [Get lo0 IP] ************************************************************************************************************************************************************************************************************************
 [WARNING]: argument username has been deprecated and will be removed in a future version

 [WARNING]: argument host has been deprecated and will be removed in a future version

 [WARNING]: argument password has been deprecated and will be removed in a future version

ok: [1.1.1.1]

TASK [Printe result variable] ************************************************************************************************************************************************************************************************************
ok: [1.1.1.1] => {
    "result": {
        "changed": false,
        "failed": false,
        "output": [
            {
                "rpc-reply": {
                    "interface-information": {
                        "logical-interface": {
                            "address-family": {
                                "address-family-name": "inet",
                                "interface-address": [
                                    {
                                        "ifa-destination": "0/0",
                                        "ifa-local": "1.1.1.1"
                                    },
                                    {
                                        "ifa-destination": "0/0",
                                        "ifa-local": "127.0.0.1"
                                    }
                                ]
                            },
                            "admin-status": "up",
                            "filter-information": "",
                            "name": "lo0.0",
                            "oper-status": "up"
                        }
                    }
                }
            }
        ],
        "stdout": [
            "<rpc-reply message-id=\"urn:uuid:78a03c04-7052-42d4-8762-072c5f51e5d5\">\n<interface-information style=\"terse\">\n<logical-interface>\n<name>\nlo0.0\n</name>\n<admin-status>\nup\n</admin-status>\n<oper-status>\nup\n</oper-status>\n<filter-information>\n</filter-information>\n<address-family>\n<address-family-name>\ninet\n</address-family-name>\n<interface-address>\n<ifa-local>\n1.1.1.1\n</ifa-local>\n<ifa-destination emit=\"emit\">\n0/0\n</ifa-destination>\n</interface-address>\n<interface-address>\n<ifa-local>\n127.0.0.1\n</ifa-local>\n<ifa-destination emit=\"emit\">\n0/0\n</ifa-destination>\n</interface-address>\n</address-family>\n</logical-interface>\n</interface-information>\n</rpc-reply>"
        ],
        "stdout_lines": [
            [
                "<rpc-reply message-id=\"urn:uuid:78a03c04-7052-42d4-8762-072c5f51e5d5\">",
                "<interface-information style=\"terse\">",
                "<logical-interface>",
                "<name>",
                "lo0.0",
                "</name>",
                "<admin-status>",
                "up",
                "</admin-status>",
                "<oper-status>",
                "up",
                "</oper-status>",
                "<filter-information>",
                "</filter-information>",
                "<address-family>",
                "<address-family-name>",
                "inet",
                "</address-family-name>",
                "<interface-address>",
                "<ifa-local>",
                "1.1.1.1",
                "</ifa-local>",
                "<ifa-destination emit=\"emit\">",
                "0/0",
                "</ifa-destination>",
                "</interface-address>",
                "<interface-address>",
                "<ifa-local>",
                "127.0.0.1",
                "</ifa-local>",
                "<ifa-destination emit=\"emit\">",
                "0/0",
                "</ifa-destination>",
                "</interface-address>",
                "</address-family>",
                "</logical-interface>",
                "</interface-information>",
                "</rpc-reply>"
            ]
        ],
        "warnings": [
            "argument username has been deprecated and will be removed in a future version",
            "argument host has been deprecated and will be removed in a future version",
            "argument password has been deprecated and will be removed in a future version"
        ]
    }
}

PLAY RECAP *******************************************************************************************************************************************************************************************************************************
1.1.1.1                : ok=2    changed=0    unreachable=0    failed=0

Stacy Smith

unread,
Feb 3, 2018, 7:58:10 PM2/3/18
to Junos Python EZ


On Friday, February 2, 2018 at 11:59:18 AM UTC-7, zach shivers wrote:
#      set_fact:
#          lo0_ip: "{{ result['matches'][0]['ifa-local'] }}"


Debug output
                                "interface-address": [
                                    {
                                        "ifa-destination": "0/0",
                                        "ifa-local": "1.1.1.1"
                                    },
                                    {
                                        "ifa-destination": "0/0",
                                        "ifa-local": "127.0.0.1"
                                    }
                                ] 
                            },
                           

In your setup there is more than one interface address configured. So, instead of:

       set_fact:
          lo0_ip: "{{ result['matches'][0]['ifa-local'] }}"

You would need:
       set_fact:
          lo0_ip: "{{ result['matches'][0]['ifa-local'][0] }}"

zach shivers

unread,
Feb 3, 2018, 8:53:06 PM2/3/18
to Junos Python EZ
Stacy,

Thank you for taking the time to reply.

Please pardon my competency level.  I am primarily a network engineer these days and haven't done code work for many years.

I have updated the file and now have a new error message.

ansible-playbook -vvvv -i ../inventory/junos get_loopback_ip_address.yml
ansible-playbook 2.4.2.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/zshivers/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible-playbook
  python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
Using /etc/ansible/ansible.cfg as config file
setting up inventory plugins
Parsed /home/automation/ansible/inventory/junos inventory source with ini plugin
Loading callback plugin default of type stdout, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc
Loading callback plugin jsnapy of type aggregate, v2.0 from /usr/lib/python2.7/site-packages/ansible/plugins/callback/__init__.pyc

PLAYBOOK: get_loopback_ip_address.yml ****************************************************************************************************************************************************************************************************
1 plays in get_loopback_ip_address.yml

PLAY [Get lo0 address from Junos routers] ************************************************************************************************************************************************************************************************
META: ran handlers

TASK [Get lo0 IP] ************************************************************************************************************************************************************************************************************************
task path: /home/automation/ansible/playbooks/get_loopback_ip_address.yml:9
<1.1.1.1> using connection plugin netconf
<1.1.1.1> socket_path: /home/zshivers/.ansible/pc/5d32538b86
Using module file /usr/lib/python2.7/site-packages/ansible/modules/network/junos/junos_command.py
<1.1.1.1> ESTABLISH LOCAL CONNECTION FOR USER: zshivers
<1.1.1.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<1.1.1.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/zshivers/.ansible/tmp/ansible-tmp-1517708698.49-193065815375954 `" && echo ansible-tmp-1517708698.49-193065815375954="` echo /home/zshivers/.ansible/tmp/ansible-tmp-1517708698.49-193065815375954 `" ) && sleep 0'
<1.1.1.1> PUT /tmp/tmpVo3rnL TO /home/zshivers/.ansible/tmp/ansible-tmp-1517708698.49-193065815375954/junos_command.py
<1.1.1.1> EXEC /bin/sh -c 'chmod u+x /home/zshivers/.ansible/tmp/ansible-tmp-1517708698.49-193065815375954/ /home/zshivers/.ansible/tmp/ansible-tmp-1517708698.49-193065815375954/junos_command.py && sleep 0'
<1.1.1.1> EXEC /bin/sh -c '/usr/bin/python /home/zshivers/.ansible/tmp/ansible-tmp-1517708698.49-193065815375954/junos_command.py; rm -rf "/home/zshivers/.ansible/tmp/ansible-tmp-1517708698.49-193065815375954/" > /dev/null 2>&1 && sleep 0'
 [WARNING]: argument username has been deprecated and will be removed in a future version

 [WARNING]: argument host has been deprecated and will be removed in a future version

 [WARNING]: argument password has been deprecated and will be removed in a future version

ok: [1.1.1.1] => {
    "changed": false,
    "invocation": {
        "module_args": {
            "commands": null,
            "display": "xml",
            "format": "xml",
            "host": "1.1.1.1",
            "interval": 1,
            "match": "all",
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "port": null,
            "provider": null,
            "retries": 10,
            "rpcs": [
                "get-interface-information interface_name=lo0.0 terse=True"
            ],
            "ssh_keyfile": null,
            "timeout": null,
            "transport": null,
            "username": "authorized_user",
            "wait_for": null
        }
    },
    "output": [
        {
            "rpc-reply": {
                "interface-information": {
                    "logical-interface": {
                        "address-family": {
                            "address-family-name": "inet",
                            "interface-address": [
                                {
                                    "ifa-destination": "0/0",
                                    "ifa-local": "1.1.1.1"
                                },
                                {
                                    "ifa-destination": "0/0",
                                    "ifa-local": "127.0.0.1"
                                }
                            ]
                        },
                        "admin-status": "up",
                        "filter-information": "",
                        "name": "lo0.0",
                        "oper-status": "up"
                    }
                }
            }
        }
    ],
    "stdout": [
        "<rpc-reply message-id=\"urn:uuid:379319a3-45fe-4997-8c7a-1704cb4057ef\">\n<interface-information style=\"terse\">\n<logical-interface>\n<name>\nlo0.0\n</name>\n<admin-status>\nup\n</admin-status>\n<oper-status>\nup\n</oper-status>\n<filter-information>\n</filter-information>\n<address-family>\n<address-family-name>\ninet\n</address-family-name>\n<interface-address>\n<ifa-local>\n1.1.1.1\n</ifa-local>\n<ifa-destination emit=\"emit\">\n0/0\n</ifa-destination>\n</interface-address>\n<interface-address>\n<ifa-local>\n127.0.0.1\n</ifa-local>\n<ifa-destination emit=\"emit\">\n0/0\n</ifa-destination>\n</interface-address>\n</address-family>\n</logical-interface>\n</interface-information>\n</rpc-reply>"
    ],
    "stdout_lines": [
        [
            "<rpc-reply message-id=\"urn:uuid:379319a3-45fe-4997-8c7a-1704cb4057ef\">",
TASK [Printe result variable] ************************************************************************************************************************************************************************************************************
task path: /home/automation/ansible/playbooks/get_loopback_ip_address.yml:19
ok: [1.1.1.1] => {
    "result": {
        "changed": false,
        "failed": false,
        "output": [
            {
                "rpc-reply": {
                    "interface-information": {
                        "logical-interface": {
                            "address-family": {
                                "address-family-name": "inet",
                                "interface-address": [
                                    {
                                        "ifa-destination": "0/0",
                                        "ifa-local": "1.1.1.1"
                                    },
                                    {
                                        "ifa-destination": "0/0",
                                        "ifa-local": "127.0.0.1"
                                    }
                                ]
                            },
                            "admin-status": "up",
                            "filter-information": "",
                            "name": "lo0.0",
                            "oper-status": "up"
                        }
                    }
                }
            }
        ],
        "stdout": [
            "<rpc-reply message-id=\"urn:uuid:379319a3-45fe-4997-8c7a-1704cb4057ef\">\n<interface-information style=\"terse\">\n<logical-interface>\n<name>\nlo0.0\n</name>\n<admin-status>\nup\n</admin-status>\n<oper-status>\nup\n</oper-status>\n<filter-information>\n</filter-information>\n<address-family>\n<address-family-name>\ninet\n</address-family-name>\n<interface-address>\n<ifa-local>\n1.1.1.1\n</ifa-local>\n<ifa-destination emit=\"emit\">\n0/0\n</ifa-destination>\n</interface-address>\n<interface-address>\n<ifa-local>\n127.0.0.1\n</ifa-local>\n<ifa-destination emit=\"emit\">\n0/0\n</ifa-destination>\n</interface-address>\n</address-family>\n</logical-interface>\n</interface-information>\n</rpc-reply>"
        ],
        "stdout_lines": [
            [
                "<rpc-reply message-id=\"urn:uuid:379319a3-45fe-4997-8c7a-1704cb4057ef\">",
TASK [Parse the lo0 IP] ******************************************************************************************************************************************************************************************************************
task path: /home/automation/ansible/playbooks/get_loopback_ip_address.yml:22
fatal: [1.1.1.1]: FAILED! => {
    "msg": "The task includes an option with an undefined variable. The error was: 'dict object' has no attribute 'xml'\n\nThe error appears to have been in '/home/automation/ansible/playbooks/get_loopback_ip_address.yml': line 22, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n        var: result\n    - name: Parse the lo0 IP\n      ^ here\n\nexception type: <class 'ansible.errors.AnsibleUndefinedVariable'>\nexception: 'dict object' has no attribute 'xml'"
}
        to retry, use: --limit @/home/automation/ansible/playbooks/get_loopback_ip_address.retry

PLAY RECAP *******************************************************************************************************************************************************************************************************************************
1.1.1.1                : ok=2    changed=0    unreachable=0    failed=1

-bash-4.2$

Stacy Smith

unread,
Feb 3, 2018, 9:06:27 PM2/3/18
to Junos Python EZ

zach shivers

unread,
Feb 3, 2018, 9:12:01 PM2/3/18
to Junos Python EZ
I don't understand this reply.
Reply all
Reply to author
Forward
0 new messages