From e2c158920131845db0a5e7259270297832ace30e Mon Sep 17 00:00:00 2001 From: flowerysong Date: Wed, 23 May 2018 15:35:23 -0400 Subject: [PATCH] Add cidr_merge filter (#36081) --- .../user_guide/playbooks_filters_ipaddr.rst | 23 +++++++++++++ lib/ansible/plugins/filter/ipaddr.py | 33 +++++++++++++++++++ test/units/plugins/filter/test_ipaddr.py | 18 +++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst b/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst index 6ca84cb1e7..ceec8c3ed9 100644 --- a/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst +++ b/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst @@ -462,6 +462,29 @@ Because of the size of IPv6 subnets, iteration over all of them to find the correct one may take some time on slower computers, depending on the size difference between subnets. +Subnet Merging +^^^^^^^^^^^^^^ + +.. versionadded:: 2.6 + +The `cidr_merge` filter can be used to merge subnets or individual addresses +into their minimal representation, collapsing overlapping subnets and merging +adjacent ones wherever possible:: + + {{ ['192.168.0.0/17', '192.168.128.0/17', '192.168.128.1' ] | cidr_merge }} + # => ['192.168.0.0/16'] + + {{ ['192.168.0.0/24', '192.168.1.0/24', '192.168.3.0/24'] | cidr_merge }} + # => ['192.168.0.0/23', '192.168.3.0/24'] + +Changing the action from 'merge' to 'span' will instead return the smallest +subnet which contains all of the inputs:: + + {{ ['192.168.0.0/24', '192.168.3.0/24'] | cidr_merge('span') }} + # => '192.168.0.0/22' + + {{ ['192.168.1.42', '192.168.42.1'] | cidr_merge('span') }} + # => '192.168.0.0/18' MAC address filter ^^^^^^^^^^^^^^^^^^ diff --git a/lib/ansible/plugins/filter/ipaddr.py b/lib/ansible/plugins/filter/ipaddr.py index 00a9a1028d..bdca960521 100644 --- a/lib/ansible/plugins/filter/ipaddr.py +++ b/lib/ansible/plugins/filter/ipaddr.py @@ -413,6 +413,38 @@ def _win_query(v): # ---- IP address and network filters ---- + +# Returns a minified list of subnets or a single subnet that spans all of +# the inputs. +def cidr_merge(value, action='merge'): + if not hasattr(value, '__iter__'): + raise errors.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 errors.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 errors.AnsibleFilterError('cidr_merge: error in netaddr:\n%s' % e) + else: + try: + return str(netaddr.spanning_cidr(value)) + except Exception as e: + raise errors.AnsibleFilterError('cidr_merge: error in netaddr:\n%s' % e) + + else: + raise errors.AnsibleFilterError("cidr_merge: invalid action '%s'" % action) + + def ipaddr(value, query='', version=False, alias='ipaddr'): ''' Check if string is an IP address or network and filter it ''' @@ -1026,6 +1058,7 @@ class FilterModule(object): ''' IP address and network manipulation filters ''' filter_map = { # IP addresses and networks + 'cidr_merge': cidr_merge, 'ipaddr': ipaddr, 'ipwrap': ipwrap, 'ip4_hex': ip4_hex, diff --git a/test/units/plugins/filter/test_ipaddr.py b/test/units/plugins/filter/test_ipaddr.py index fb6a92ee30..785d57e371 100644 --- a/test/units/plugins/filter/test_ipaddr.py +++ b/test/units/plugins/filter/test_ipaddr.py @@ -21,7 +21,7 @@ import pytest from ansible.compat.tests import unittest from ansible.plugins.filter.ipaddr import (ipaddr, _netmask_query, nthhost, next_nth_usable, - previous_nth_usable, network_in_usable, network_in_network) + previous_nth_usable, network_in_usable, network_in_network, cidr_merge) netaddr = pytest.importorskip('netaddr') @@ -457,3 +457,19 @@ class TestIpFilter(unittest.TestCase): subnet = '1.12.1.0/24' address = '1.12.2.0' self.assertEqual(network_in_network(subnet, address), False) + + def test_cidr_merge(self): + self.assertEqual(cidr_merge([]), []) + self.assertEqual(cidr_merge([], 'span'), None) + subnets = ['1.12.1.0/24'] + self.assertEqual(cidr_merge(subnets), subnets) + self.assertEqual(cidr_merge(subnets, 'span'), subnets[0]) + subnets = ['1.12.1.0/25', '1.12.1.128/25'] + self.assertEqual(cidr_merge(subnets), ['1.12.1.0/24']) + self.assertEqual(cidr_merge(subnets, 'span'), '1.12.1.0/24') + subnets = ['1.12.1.0/25', '1.12.1.128/25', '1.12.2.0/24'] + self.assertEqual(cidr_merge(subnets), ['1.12.1.0/24', '1.12.2.0/24']) + self.assertEqual(cidr_merge(subnets, 'span'), '1.12.0.0/22') + subnets = ['1.12.1.1', '1.12.1.255'] + self.assertEqual(cidr_merge(subnets), ['1.12.1.1/32', '1.12.1.255/32']) + self.assertEqual(cidr_merge(subnets, 'span'), '1.12.1.0/24')