diff --git a/README.md b/README.md index 26aa803..e7bc87b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ PEP440 is the schema used to describe the versions of Ansible. ### Filter plugins Name | Description --- | --- +[ansible.utils.cidr_merge](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.cidr_merge_filter.rst)|This filter can be used to merge subnets or individual addresses. [ansible.utils.from_xml](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.from_xml_filter.rst)|Convert given XML string to native python dictionary. [ansible.utils.get_path](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.get_path_filter.rst)|Retrieve the value in a variable using a path [ansible.utils.index_of](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.index_of_filter.rst)|Find the indices of items in a list matching some criteria diff --git a/changelogs/fragments/add_cli_merge_filter_plugin.yaml b/changelogs/fragments/add_cli_merge_filter_plugin.yaml new file mode 100644 index 0000000..17a2055 --- /dev/null +++ b/changelogs/fragments/add_cli_merge_filter_plugin.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - Add cli_merge ipaddr filter plugin. diff --git a/docs/ansible.utils.cidr_merge_filter.rst b/docs/ansible.utils.cidr_merge_filter.rst new file mode 100644 index 0000000..ebb001d --- /dev/null +++ b/docs/ansible.utils.cidr_merge_filter.rst @@ -0,0 +1,186 @@ +.. _ansible.utils.cidr_merge_filter: + + +************************ +ansible.utils.cidr_merge +************************ + +**This filter can be used to merge subnets or individual addresses.** + + +Version added: 2.5.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This filter can be used to merge subnets or individual addresses into their minimal representation, collapsing +- overlapping subnets and merging adjacent ones wherever possible. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ action + +
+ string +
+
+ Default:
"merge"
+
+ +
Action to be performed.example merge,span
+
+
+ value + +
+ list + / elements=string + / required +
+
+ + +
list of subnets or individual address to be merged
+
+
+ + + + +Examples +-------- + +.. code-block:: yaml + + #### examples + - name: cidr_merge with merge action + ansible.builtin.set_fact: + value: + - 192.168.0.0/17 + - 192.168.128.0/17 + - 192.168.128.1 + - debug: + msg: "{{ value|ansible.utils.cidr_merge }}" + + # TASK [cidr_merge with merge action] ****************************************************************************************************************************** + # ok: [localhost] => { + # "ansible_facts": { + # "value": [ + # "192.168.0.0/17", + # "192.168.128.0/17", + # "192.168.128.1" + # ] + # }, + # "changed": false + # } + # TASK [debug] ***************************************************************************************************************************************************** + # ok: [loalhost] => { + # "msg": [ + # "192.168.0.0/16" + # ] + # } + + - name: Cidr_merge with span. + ansible.builtin.set_fact: + value: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 + - 192.168.1.4 + - debug: + msg: "{{ value|ansible.utils.cidr_merge('span') }}" + + # TASK [Cidr_merge with span.] ******************************************************************** + # ok: [localhost] => { + # "ansible_facts": { + # "value": [ + # "192.168.1.1", + # "192.168.1.2", + # "192.168.1.3", + # "192.168.1.4" + # ] + # }, + # "changed": false + # } + # + # TASK [debug] ************************************************************************************ + # ok: [localhost] => { + # "msg": "192.168.1.0/29" + # } + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this filter: + +.. raw:: html + + + + + + + + + + + + +
KeyReturnedDescription
+
+ data + +
+ - +
+
+
Returns a minified list of subnets or a single subnet that spans all of the inputs.
+
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ashwini Mhatre (@amhatre) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/plugins/filter/cidr_merge.py b/plugins/filter/cidr_merge.py new file mode 100644 index 0000000..29c68d6 --- /dev/null +++ b/plugins/filter/cidr_merge.py @@ -0,0 +1,194 @@ +# -*- 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) + +""" +filter plugin file for ipaddr filters: cidr_merge +""" +from __future__ import absolute_import, division, print_function +from functools import partial +from ansible_collections.ansible.utils.plugins.plugin_utils.base.ipaddress_utils import ( + _need_netaddr, +) +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +__metaclass__ = type + +try: + import netaddr + + HAS_NETADDR = True +except ImportError: + HAS_NETADDR = False +else: + + class mac_linux(netaddr.mac_unix): + pass + + mac_linux.word_fmt = "%.2x" + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + +DOCUMENTATION = """ + name: cidr_merge + author: Ashwini Mhatre (@amhatre) + version_added: "2.5.0" + short_description: This filter can be used to merge subnets or individual addresses. + description: + - This filter can be used to merge subnets or individual addresses into their minimal representation, collapsing + - overlapping subnets and merging adjacent ones wherever possible. + options: + value: + description: + - list of subnets or individual address to be merged + type: list + elements: str + required: True + action: + description: + - Action to be performed.example merge,span + default: merge + type: str + notes: +""" + +EXAMPLES = r""" +#### examples +- name: cidr_merge with merge action + ansible.builtin.set_fact: + value: + - 192.168.0.0/17 + - 192.168.128.0/17 + - 192.168.128.1 +- debug: + msg: "{{ value|ansible.utils.cidr_merge }}" + +# TASK [cidr_merge with merge action] ********************************************************************************** +# ok: [localhost] => { +# "ansible_facts": { +# "value": [ +# "192.168.0.0/17", +# "192.168.128.0/17", +# "192.168.128.1" +# ] +# }, +# "changed": false +# } +# TASK [debug] ********************************************************************************************************* +# ok: [loalhost] => { +# "msg": [ +# "192.168.0.0/16" +# ] +# } + +- name: Cidr_merge with span. + ansible.builtin.set_fact: + value: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 + - 192.168.1.4 +- debug: + msg: "{{ value|ansible.utils.cidr_merge('span') }}" + +# TASK [Cidr_merge with span.] ******************************************************************** +# ok: [localhost] => { +# "ansible_facts": { +# "value": [ +# "192.168.1.1", +# "192.168.1.2", +# "192.168.1.3", +# "192.168.1.4" +# ] +# }, +# "changed": false +# } +# +# TASK [debug] ************************************************************************************ +# ok: [localhost] => { +# "msg": "192.168.1.0/29" +# } + +""" + +RETURN = """ + data: + description: + - Returns a minified list of subnets or a single subnet that spans all of the inputs. +""" + + +@pass_environment +def _cidr_merge(*args, **kwargs): + """Convert the given data from json to xml.""" + keys = ["value", "action"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + aav = AnsibleArgSpecValidator( + data=data, schema=DOCUMENTATION, name="cidr_merge" + ) + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return cidr_merge(**updated_data) + + +def cidr_merge(value, action="merge"): + if not hasattr(value, "__iter__"): + raise AnsibleFilterError( + "cidr_merge: expected iterable, got " + repr(value) + ) + + if action == "merge": + try: + return [str(ip) for ip in netaddr.cidr_merge(value)] + except Exception as e: + raise AnsibleFilterError("cidr_merge: error in netaddr:\n%s" % e) + + elif action == "span": + # spanning_cidr needs at least two values + if len(value) == 0: + return None + elif len(value) == 1: + try: + return str(netaddr.IPNetwork(value[0])) + except Exception as e: + raise AnsibleFilterError( + "cidr_merge: error in netaddr:\n%s" % e + ) + else: + try: + return str(netaddr.spanning_cidr(value)) + except Exception as e: + raise AnsibleFilterError( + "cidr_merge: error in netaddr:\n%s" % e + ) + + else: + raise AnsibleFilterError("cidr_merge: invalid action '%s'" % action) + + +class FilterModule(object): + """IP address and network manipulation filters + """ + + filter_map = { + # IP addresses and networks + "cidr_merge": _cidr_merge + } + + def filters(self): + if HAS_NETADDR: + return self.filter_map + else: + # Need to install python's netaddr for these filters to work + return dict( + (f, partial(_need_netaddr, f)) for f in self.filter_map + ) diff --git a/plugins/plugin_utils/base/ipaddress_utils.py b/plugins/plugin_utils/base/ipaddress_utils.py index 25f7bc6..cbe8624 100644 --- a/plugins/plugin_utils/base/ipaddress_utils.py +++ b/plugins/plugin_utils/base/ipaddress_utils.py @@ -18,6 +18,7 @@ from functools import wraps from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( check_argspec, ) +from ansible import errors try: import ipaddress @@ -84,3 +85,10 @@ def _validate_args(plugin, doc, params): argspec_errors=argspec_result.get("errors"), ) ) + + +def _need_netaddr(f_name, *args, **kwargs): + raise errors.AnsibleFilterError( + "The %s filter requires python's netaddr be " + "installed on the ansible controller" % f_name + ) diff --git a/tests/integration/targets/utils_ipaddr_filter/tasks/cidr_merge.yaml b/tests/integration/targets/utils_ipaddr_filter/tasks/cidr_merge.yaml new file mode 100644 index 0000000..18b0bec --- /dev/null +++ b/tests/integration/targets/utils_ipaddr_filter/tasks/cidr_merge.yaml @@ -0,0 +1,31 @@ +--- +- name: cidr_merge with merge action + ansible.builtin.set_fact: + value: + - 192.168.0.0/17 + - 192.168.128.0/17 + - 192.168.128.1 + +- name: cidr_merge with merge action + ansible.builtin.set_fact: + result1: "{{ value|ansible.utils.cidr_merge }}" + +- name: Assert result for cidr_merge + assert: + that: "{{ result1 == cidr_result1 }}" + +- name: cidr_merge with span action + ansible.builtin.set_fact: + value: + - 192.168.1.1 + - 192.168.1.2 + - 192.168.1.3 + - 192.168.1.4 + +- name: cidr_merge with span action + ansible.builtin.set_fact: + result2: "{{ value|ansible.utils.cidr_merge('span') }}" + +- name: Assert result for cidr_merge(span) + assert: + that: "{{ result2 == cidr_result2 }}" diff --git a/tests/integration/targets/utils_ipaddr_filter/vars/main.yaml b/tests/integration/targets/utils_ipaddr_filter/vars/main.yaml index b17b503..555abd3 100644 --- a/tests/integration/targets/utils_ipaddr_filter/vars/main.yaml +++ b/tests/integration/targets/utils_ipaddr_filter/vars/main.yaml @@ -9,3 +9,8 @@ result2_val: result3_val: - "192.24.2.1" + +cidr_result1: + - 192.168.0.0/16 + +cidr_result2: 192.168.1.0/29 diff --git a/tests/unit/plugins/filter/test_cidr_merge.py b/tests/unit/plugins/filter/test_cidr_merge.py new file mode 100644 index 0000000..c4d411c --- /dev/null +++ b/tests/unit/plugins/filter/test_cidr_merge.py @@ -0,0 +1,67 @@ +# -*- 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) + +""" +Unit test file for cidr_merge filter plugin +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.filter.cidr_merge import ( + _cidr_merge, +) + +INVALID_DATA_MERGE = ["0.1234.34.44", "1.00000.2.000.22"] + +VALID_DATA_MEREGE = ["192.168.0.0/17", "192.168.128.0/17", "192.168.128.1"] + +VALID_OUTPUT_MERGE = ["192.168.0.0/16"] + +VALID_DATA_SPAN = ["192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"] + +VALID_OUTPUT_SPAN = "192.168.1.0/29" + + +class TestCidrMerge(unittest.TestCase): + def setUp(self): + pass + + def test_invalid_data_merge(self): + """Check passing invalid argspec""" + + args = ["", INVALID_DATA_MERGE, "merge"] + kwargs = {} + with self.assertRaises(AnsibleFilterError) as error: + _cidr_merge(*args, **kwargs) + self.assertIn("invalid IPNetwork 0.1234.34.44", str(error.exception)) + + def test_valid_data_merge(self): + """test for cidr_merge plugin with merge""" + + args = ["", VALID_DATA_MEREGE, "merge"] + result = _cidr_merge(*args) + self.assertEqual(result, VALID_OUTPUT_MERGE) + + def test_valid_data_span(self): + """test for cidr_merge plugin with span""" + + args = ["", VALID_DATA_SPAN, "span"] + result = _cidr_merge(*args) + self.assertEqual(result, VALID_OUTPUT_SPAN) + + def test_valid_data_with_invalid_action(self): + """Check passing valid data as per criteria""" + + args = ["", VALID_DATA_SPAN, "span1"] + kwargs = {} + with self.assertRaises(AnsibleFilterError) as error: + _cidr_merge(*args, **kwargs) + self.assertIn( + "cidr_merge: invalid action 'span1'", str(error.exception) + )