Add new filter to parse xml output for network use cases (#31562)

* Add new filter to parse xml output for network use cases

Fixes #31026
*  Add parse_xml filter
*  Add documentation for parse_xml filter

* Edited for clarity.

* Fix review comment and add unit tests

* Fix unit test CI failure

* Fix CI issues

* Fix unit test failures

* Fix review comments

* More copy edits.
This commit is contained in:
Ganesh Nalawade 2017-11-21 12:16:18 +05:30 committed by GitHub
commit 0ddf092ae3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 393 additions and 6 deletions

View file

@ -0,0 +1,34 @@
<rpc-reply>
<configuration>
<vlans>
<vlan>
<name>test-1</name>
<vlan-id>100</vlan-id>
</vlan>
<vlan>
<name>test-2</name>
</vlan>
<vlan>
<name>test-3</name>
<vlan-id>300</vlan-id>
<description>test vlan-3</description>
<interface>
<name>em3.0</name>
</interface>
</vlan>
<vlan inactive="inactive">
<name>test-4</name>
<description>test vlan-4</description>
<vlan-id>400</vlan-id>
</vlan>
<vlan inactive="inactive">
<name>test-5</name>
<description>test vlan-5</description>
<vlan-id>500</vlan-id>
<interface>
<name>em5.0</name>
</interface>
</vlan>
</vlans>
</configuration>
</rpc-reply>

View file

@ -0,0 +1,11 @@
---
vars:
vlan: "{{ item.name }}"
keys:
vlans:
type: list
value: "{{ vlan }}"
top: configuration/vlans/vlan
items:
name: name

View file

@ -0,0 +1,21 @@
---
vars:
vlan:
vlan_id: "{{ item.vlan_id }}"
name: "{{ item.name }}"
desc: "{{ item.desc }}"
interface: "{{ item.intf }}"
enabled: "{{ item.state.get('inactive') != 'inactive' }}"
state: "{% if item.state.get('inactive') == 'inactive'%}inactive{% else %}active{% endif %}"
keys:
vlans:
type: list
value: "{{ vlan }}"
top: configuration/vlans/vlan
items:
vlan_id: vlan-id
name: name
desc: description
intf: interface/name
state: ".[@inactive='inactive']"

View file

@ -0,0 +1,22 @@
---
vars:
vlan:
vlan_id: "{{ item.vlan_id }}"
name: "{{ item.name }}"
desc: "{{ item.desc }}"
interface: "{{ item.intf }}"
enabled: "{{ item.state.get('inactive') != 'inactive' }}"
state: "{% if item.state.get('inactive') == 'inactive'%}inactive{% else %}active{% endif %}"
keys:
vlans:
type: list
value: "{{ vlan }}"
top: configuration/vlans/vlan
items:
vlan_id: vlan-id
name: name
desc: description
intf: interface/name
state: ".[@inactive='inactive']"
when: item.name == 'test-5'

View file

@ -0,0 +1,23 @@
---
vars:
vlan:
key: "{{ item.name }}"
values:
vlan_id: "{{ item.vlan_id }}"
name: "{{ item.name }}"
desc: "{{ item.desc }}"
interface: "{{ item.intf }}"
enabled: "{{ item.state.get('inactive') != 'inactive' }}"
state: "{% if item.state.get('inactive') == 'inactive'%}inactive{% else %}active{% endif %}"
keys:
vlans:
type: list
value: "{{ vlan }}"
top: configuration/vlans/vlan
items:
vlan_id: vlan-id
name: name
desc: description
intf: interface/name
state: ".[@inactive='inactive']"

View file

@ -0,0 +1,80 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import sys
from ansible.compat.tests import unittest
from ansible.plugins.filter.network import parse_xml
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'network')
with open(os.path.join(fixture_path, 'show_vlans_xml_output.txt')) as f:
output_xml = f.read()
class TestNetworkParseFilter(unittest.TestCase):
@unittest.skipIf(sys.version_info[:2] == (2, 6), 'XPath expression not supported in this version')
def test_parse_xml_to_list_of_dict(self):
spec_file_path = os.path.join(fixture_path, 'show_vlans_xml_spec.yml')
parsed = parse_xml(output_xml, spec_file_path)
expected = {'vlans': [{'name': 'test-1', 'enabled': True, 'state': 'active', 'interface': None, 'vlan_id': 100, 'desc': None},
{'name': 'test-2', 'enabled': True, 'state': 'active', 'interface': None, 'vlan_id': None, 'desc': None},
{'name': 'test-3', 'enabled': True, 'state': 'active', 'interface': 'em3.0', 'vlan_id': 300, 'desc': 'test vlan-3'},
{'name': 'test-4', 'enabled': False, 'state': 'inactive', 'interface': None, 'vlan_id': 400, 'desc': 'test vlan-4'},
{'name': 'test-5', 'enabled': False, 'state': 'inactive', 'interface': 'em5.0', 'vlan_id': 500, 'desc': 'test vlan-5'}]}
self.assertEqual(parsed, expected)
@unittest.skipIf(sys.version_info[:2] == (2, 6), 'XPath expression not supported in this version')
def test_parse_xml_to_dict(self):
spec_file_path = os.path.join(fixture_path, 'show_vlans_xml_with_key_spec.yml')
parsed = parse_xml(output_xml, spec_file_path)
expected = {'vlans': {'test-4': {'name': 'test-4', 'enabled': False, 'state': 'inactive', 'interface': None, 'vlan_id': 400, 'desc': 'test vlan-4'},
'test-3': {'name': 'test-3', 'enabled': True, 'state': 'active', 'interface': 'em3.0', 'vlan_id': 300, 'desc': 'test vlan-3'},
'test-1': {'name': 'test-1', 'enabled': True, 'state': 'active', 'interface': None, 'vlan_id': 100, 'desc': None},
'test-5': {'name': 'test-5', 'enabled': False, 'state': 'inactive', 'interface': 'em5.0', 'vlan_id': 500, 'desc': 'test vlan-5'},
'test-2': {'name': 'test-2', 'enabled': True, 'state': 'active', 'interface': None, 'vlan_id': None, 'desc': None}}
}
self.assertEqual(parsed, expected)
@unittest.skipIf(sys.version_info[:2] == (2, 6), 'XPath expression not supported in this version')
def test_parse_xml_with_condition_spec(self):
spec_file_path = os.path.join(fixture_path, 'show_vlans_xml_with_condition_spec.yml')
parsed = parse_xml(output_xml, spec_file_path)
expected = {'vlans': [{'name': 'test-5', 'enabled': False, 'state': 'inactive', 'interface': 'em5.0', 'vlan_id': 500, 'desc': 'test vlan-5'}]}
self.assertEqual(parsed, expected)
def test_parse_xml_with_single_value_spec(self):
spec_file_path = os.path.join(fixture_path, 'show_vlans_xml_single_value_spec.yml')
parsed = parse_xml(output_xml, spec_file_path)
expected = {'vlans': ['test-1', 'test-2', 'test-3', 'test-4', 'test-5']}
self.assertEqual(parsed, expected)
def test_parse_xml_validate_input(self):
spec_file_path = os.path.join(fixture_path, 'show_vlans_xml_spec.yml')
output = 10
with self.assertRaises(Exception) as e:
parse_xml(output_xml, 'junk_path')
self.assertEqual("unable to locate parse_cli template: junk_path", str(e.exception))
with self.assertRaises(Exception) as e:
parse_xml(output, spec_file_path)
self.assertEqual("parse_xml works on string input, but given input of : %s" % type(output), str(e.exception))