From 2f071599d0ae3b129b29cfeffc0e6a3d88e7d9a2 Mon Sep 17 00:00:00 2001 From: Rohit Thakur Date: Thu, 12 Aug 2021 10:45:44 +0530 Subject: [PATCH] resource_list_compare filter plugin added (#89) resource_list_compare filter plugin added Signed-off-by: Rohit Thakur rohitthakur2590@outlook.com SUMMARY resolves: #88 Test Coverage plugins/filter/param_list_compare.py 95% tests/unit/plugins/filter/test_param_list_compare.py 100% ISSUE TYPE Feature Pull Request COMPONENT NAME ADDITIONAL INFORMATION Reviewed-by: Ganesh Nalawade Reviewed-by: Bradley A. Thornton Reviewed-by: Rohit Thakur Reviewed-by: None --- ...8_compare_resource_list_filter_plugin.yaml | 3 + plugins/filter/param_list_compare.py | 195 ++++++++++++++++++ .../tasks/include/simple.yaml | 56 +++++ .../param_list_compare/tasks/main.yaml | 13 ++ .../plugins/filter/test_param_list_compare.py | 93 +++++++++ 5 files changed, 360 insertions(+) create mode 100644 changelogs/fragments/88_compare_resource_list_filter_plugin.yaml create mode 100644 plugins/filter/param_list_compare.py create mode 100644 tests/integration/targets/param_list_compare/tasks/include/simple.yaml create mode 100644 tests/integration/targets/param_list_compare/tasks/main.yaml create mode 100644 tests/unit/plugins/filter/test_param_list_compare.py diff --git a/changelogs/fragments/88_compare_resource_list_filter_plugin.yaml b/changelogs/fragments/88_compare_resource_list_filter_plugin.yaml new file mode 100644 index 0000000..df20625 --- /dev/null +++ b/changelogs/fragments/88_compare_resource_list_filter_plugin.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - Add new plugin param_list_compare that generates the final param list after comparing base and provided/target param list. diff --git a/plugins/filter/param_list_compare.py b/plugins/filter/param_list_compare.py new file mode 100644 index 0000000..e0268a0 --- /dev/null +++ b/plugins/filter/param_list_compare.py @@ -0,0 +1,195 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + name: param_list_compare + author: Rohit Thakur (@rohitthakur2590) + version_added: "2.4.0" + short_description: Generate the final param list combining/comparing base and provided parameters. + description: + - Generate the final list of parameters after comparing with base list and provided/target list of params/bangs. + options: + base: + description: Specify the base list. + type: list + elements: str + target: + description: Specify the target list. + type: list + elements: str +""" + +EXAMPLES = r""" +- set_fact: + base: ['1','2','3','4','5'] + +- set_fact: + target: ['!all','2','4'] + +- name: Get final list of parameters + register: result + set_fact: + final_params: "{{ base|param_list_compare(target) }}" + +# TASK [Target list] ********************************************************** +# ok: [localhost] => { +# "msg": { +# "actionable": [ +# "2", +# "4" +# ], +# "unsupported": [] +# } +# } + +# Network Specific Example +# ----------- +- set_fact: + ios_resources: + - "acl_interfaces" + - "acls" + - "bgp_address_family" + - "bgp_global" + - "interfaces" + - "l2_interfaces" + - "l3_interfaces" + - "lacp" + - "lacp_interfaces" + - "lag_interfaces" + - "lldp_global" + - "lldp_interfaces" + - "logging_global" + - "ospf_interfaces" + - "ospfv2" + - "ospfv3" + - "prefix_lists" + - "route_maps" + - "static_routes" + - "vlans" + +- set_fact: + target_resources: + - '!all' + - 'vlan' + - 'bgp_global' + +- name: Get final list of target resources/params + register: result + set_fact: + network_resources: "{{ ios_resources|param_list_compare(target_resources) }}" + +- name: Target list of network resources + debug: + msg: "{{ network_resources }}" + +# TASK [Target list of network resources] ******************************************************************************************************************* +# ok: [localhost] => { +# "msg": { +# "actionable": [ +# "bgp_global", +# "vlans" +# ], +# "unsupported": [] +# } +# } + +- name: Get final list of target resources/params + register: result + set_fact: + network_resources: "{{ ios_resources|param_list_compare(target=['vla', 'ntp_global', 'logging_global']) }}" + +- name: Target list of network resources + debug: + msg: "{{ network_resources }}" + +# TASK [Target list of network resources] ************************************************ +# ok: [localhost] => { +# "msg": { +# "actionable": [ +# "logging_global" +# ], +# "unsupported": [ +# "vla", +# "ntp_global" +# ] +# } +# } + +""" + +RETURN = """ + actionable: + description: list of combined params + type: list + + unsupported: + description: list of unsupported params + type: list + +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + check_argspec, +) + +ARGSPEC_CONDITIONALS = {} + + +def param_list_compare(*args, **kwargs): + params = ["base", "target"] + data = dict(zip(params, args)) + data.update(kwargs) + + if len(data) < 2: + raise AnsibleFilterError( + "Missing either 'base' or 'other value in filter input," + "refer 'ansible.utils.param_list_compare' filter plugin documentation for details" + ) + + valid, argspec_result, updated_params = check_argspec( + DOCUMENTATION, + "param_list_compare filter", + schema_conditionals=ARGSPEC_CONDITIONALS, + **data + ) + if not valid: + raise AnsibleFilterError( + "{argspec_result} with errors: {argspec_errors}".format( + argspec_result=argspec_result.get("msg"), + argspec_errors=argspec_result.get("errors"), + ) + ) + base = data["base"] + other = data["target"] + combined = [] + alls = [x for x in other if x == "all"] + bangs = [x[1:] for x in other if x.startswith("!")] + rbangs = [x for x in other if x.startswith("!")] + remain = [ + x for x in other if x not in alls and x not in rbangs and x in base + ] + unsupported = [ + x for x in other if x not in alls and x not in rbangs and x not in base + ] + + if alls: + combined = base + for entry in bangs: + if entry in combined: + combined.remove(entry) + for entry in remain: + if entry not in combined: + combined.append(entry) + combined.sort() + output = {"actionable": combined, "unsupported": unsupported} + return output + + +class FilterModule(object): + """ param_list_compare """ + + def filters(self): + """a mapping of filter names to functions""" + return {"param_list_compare": param_list_compare} diff --git a/tests/integration/targets/param_list_compare/tasks/include/simple.yaml b/tests/integration/targets/param_list_compare/tasks/include/simple.yaml new file mode 100644 index 0000000..070cf7c --- /dev/null +++ b/tests/integration/targets/param_list_compare/tasks/include/simple.yaml @@ -0,0 +1,56 @@ +--- +- debug: + msg: "START param_list_compare integration tests on connection={{ ansible_connection }}" + +- name: Setup supported resource module list json + ansible.builtin.set_fact: + network_resources: + modules: + - 'acl_interfaces' + - 'acls' + - 'bgp_address_family' + - 'bgp_global' + - 'interfaces' + - 'l2_interfaces' + - 'l3_interfaces' + - 'lacp' + - 'lacp_interfaces' + - 'lag_interfaces' + - 'lldp_global' + - 'lldp_interfaces' + - 'logging_global' + - 'ospf_interfaces' + - 'ospfv2' + - 'ospfv3' + - 'prefix_lists' + - 'route_maps' + - 'static_routes' + - 'vlans' + +- name: Setup target resources with bangs + ansible.builtin.set_fact: + provided_resources: + - '!all' + - '!acl_interfaces' + - 'acls' + - 'bgp_address_family' + - 'bgp_global' + +- name: Setup target resources with bangs + ansible.builtin.set_fact: + expected_network_resources: + - 'acls' + - 'bgp_address_family' + - 'bgp_global' + +- name: Get the final list of resources + set_fact: + final_network_resources: "{{ network_resources['modules']|ansible.utils.param_list_compare(provided_resources) }}" + +- name: Assert final network resources + assert: + that: + - "{{ expected_network_resources | symmetric_difference(final_network_resources['actionable']) |length\ + \ == 0 }}" +- debug: + msg: "END param_list_compare integration tests on connection={{ ansible_connection }}" diff --git a/tests/integration/targets/param_list_compare/tasks/main.yaml b/tests/integration/targets/param_list_compare/tasks/main.yaml new file mode 100644 index 0000000..4274d75 --- /dev/null +++ b/tests/integration/targets/param_list_compare/tasks/main.yaml @@ -0,0 +1,13 @@ +--- +- name: Recursively find all test files + find: + file_type: file + paths: "{{ role_path }}/tasks/include" + recurse: true + use_regex: true + patterns: + - '^(?!_).+$' + register: found + +- include: "{{ item.path }}" + loop: "{{ found.files }}" diff --git a/tests/unit/plugins/filter/test_param_list_compare.py b/tests/unit/plugins/filter/test_param_list_compare.py new file mode 100644 index 0000000..ed157b8 --- /dev/null +++ b/tests/unit/plugins/filter/test_param_list_compare.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.filter.param_list_compare import ( + param_list_compare, +) + + +class TestParam_list_compare_merge(unittest.TestCase): + def test_valid_data(self): + """Check passing valid data as per criteria""" + + base = ["interfaces", "l2_interfaces", "l3_interfaces"] + other = ["all"] + args = [base, other] + kwargs = {} + result = param_list_compare(*args, **kwargs) + self.assertEqual(result["actionable"], base) + + def test_valid_data_same_contents(self): + """Check passing valid data as per criteria""" + + base = ["interfaces", "l2_interfaces", "l3_interfaces"] + other = ["interfaces", "l2_interfaces", "l3_interfaces"] + args = [base, other] + kwargs = {} + result = param_list_compare(*args, **kwargs) + self.assertEqual(result["actionable"], base) + + def test_valid_data_with_not_bang(self): + """Check passing valid data as per criteria""" + + base = ["interfaces", "l2_interfaces", "l3_interfaces"] + other = ["!l2_interfaces", "all"] + args = [base, other] + expected = ["interfaces", "l3_interfaces"] + kwargs = {} + result = param_list_compare(*args, **kwargs) + self.assertEqual(result["actionable"], expected) + + def test_invalid_args_length_data(self): + """Check passing valid data as per criteria""" + + base = {} + args = [base] + kwargs = {} + with self.assertRaises(AnsibleFilterError) as error: + param_list_compare(*args, **kwargs) + self.assertIn( + "Missing either 'base' or 'other value in filter input", + str(error.exception), + ) + + def test_invalid_base_type_data(self): + """Check passing valid data as per criteria""" + + base = {} + other = ["all"] + args = [base, other] + kwargs = {} + with self.assertRaises(AnsibleFilterError) as error: + param_list_compare(*args, **kwargs) + self.assertIn("cannot be converted to a list", str(error.exception)) + + def test_invalid_other_type_data(self): + """Check passing valid data as per criteria""" + + base = ["interfaces"] + other = {"all": None} + args = [base, other] + kwargs = {} + with self.assertRaises(AnsibleFilterError) as error: + param_list_compare(*args, **kwargs) + self.assertIn("cannot be converted to a list", str(error.exception)) + + def test_invalid_unsupported_bang(self): + """Check passing valid data as per criteria""" + + base = ["interfaces"] + other = ["every"] + args = [base, other] + kwargs = {} + result = param_list_compare(*args, **kwargs) + self.assertEqual(result["unsupported"], other)