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
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Configuration |
+ Comments |
+
+
+
+
+ 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
+
+
+
+ Key |
+ Returned |
+ Description |
+
+
+
+
+ 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)
+ )