diff --git a/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst b/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst index 59e208bd25..01571621fb 100644 --- a/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst +++ b/docs/docsite/rst/user_guide/playbooks_filters_ipaddr.rst @@ -474,6 +474,28 @@ smaller subnets:: # {{ address | ipsubnet(18, -5) }} 192.168.144.0/27 +By specifying an other subnet as a second argument, if the second subnet include +the first you can have the rank of the first subnet in the second :: + + # The rank of the ip in the subnet (the ip is the 36870nth /32 of the subnet) + # {{ address | ipsubnet(subnet) }} + 36870 + + # The rank in the /24 that contain the address + # {{ address | ipsubnet('192.168.144.0/24') }} + 6 + + # An IP with the subnet in the first /30 in a /24 + # {{ '192.168.144.1/30' | ipsubnet('192.168.144.0/24') }} + 1 + + # The fifth subnet /30 in a /24 + # {{ '192.168.144.16/30' | ipsubnet('192.168.144.0/24') }} + 5 + +If the secound subnet doesn't include the first it raise an error + + You can use ``ipsubnet()`` filter with ``ipaddr()`` filter to for example split given ``/48`` prefix into smaller, ``/64`` subnets:: diff --git a/lib/ansible/plugins/filter/ipaddr.py b/lib/ansible/plugins/filter/ipaddr.py index b12101e8e1..9087d7d759 100644 --- a/lib/ansible/plugins/filter/ipaddr.py +++ b/lib/ansible/plugins/filter/ipaddr.py @@ -21,6 +21,7 @@ __metaclass__ = type from functools import partial import types +from ansible.module_utils import six try: import netaddr @@ -736,6 +737,9 @@ def ipv6(value, query=''): # # - address | ipsubnet(cidr, index) # returns next indexed subnet which contains given address +# +# - address/prefix | ipsubnet(subnet/prefix) +# return the index of the subnet in the subnet def ipsubnet(value, query='', index='x'): ''' Manipulate IPv4/IPv6 subnets ''' @@ -749,11 +753,11 @@ def ipsubnet(value, query='', index='x'): value = netaddr.IPNetwork(v) except: return False - + query_string = str(query) if not query: return str(value) - elif str(query).isdigit(): + elif query_string.isdigit(): vsize = ipaddr(v, 'size') query = int(query) @@ -786,6 +790,21 @@ def ipsubnet(value, query='', index='x'): except: return False + elif query_string: + vtype = ipaddr(query, 'type') + if vtype == 'address': + v = ipaddr(query, 'cidr') + elif vtype == 'network': + v = ipaddr(query, 'subnet') + else: + msg = 'You must pass a valid subnet or IP address; {0} is invalid'.format(query_string) + raise errors.AnsibleFilterError(msg) + query = netaddr.IPNetwork(v) + for i, subnet in enumerate(query.subnet(value.prefixlen), 1): + if subnet == value: + return str(i) + msg = '{0} is not in the subnet {1}'.format(value.cidr, query.cidr) + raise errors.AnsibleFilterError(msg) return False diff --git a/test/units/plugins/filter/test_ipaddr.py b/test/units/plugins/filter/test_ipaddr.py index cd375397cc..a19895d169 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.errors import AnsibleFilterError -from ansible.plugins.filter.ipaddr import (ipaddr, _netmask_query, nthhost, next_nth_usable, +from ansible.plugins.filter.ipaddr import (ipaddr, _netmask_query, nthhost, next_nth_usable, ipsubnet, previous_nth_usable, network_in_usable, network_in_network, cidr_merge, ipmath) netaddr = pytest.importorskip('netaddr') @@ -502,3 +502,44 @@ class TestIpFilter(unittest.TestCase): with self.assertRaises(AnsibleFilterError) as exc: ipmath('1.2.3.4', 'some_number') self.assertEqual(exc.exception.message, expected) + + def test_ipsubnet(self): + test_cases = ( + (('1.1.1.1/24', '30'), '64'), + (('1.1.1.1/25', '24'), '0'), + (('1.12.1.34/32', '1.12.1.34/24'), '35'), + (('192.168.50.0/24', '192.168.0.0/16'), '51'), + (('192.168.144.5', '192.168.0.0/16'), '36870'), + (('192.168.144.5', '192.168.144.5/24'), '6'), + (('192.168.144.5/32', '192.168.144.0/24'), '6'), + (('192.168.144.16/30', '192.168.144.0/24'), '5'), + (('192.168.144.5', ), '192.168.144.5/32'), + (('192.168.0.0/16', ), '192.168.0.0/16'), + (('192.168.144.5', ), '192.168.144.5/32'), + (('192.168.0.0/16', '20'), '16'), + (('192.168.0.0/16', '20', '0'), '192.168.0.0/20'), + (('192.168.0.0/16', '20', '-1'), '192.168.240.0/20'), + (('192.168.0.0/16', '20', '5'), '192.168.80.0/20'), + (('192.168.0.0/16', '20', '-5'), '192.168.176.0/20'), + (('192.168.144.5', '20'), '192.168.144.0/20'), + (('192.168.144.5', '18', '0'), '192.168.128.0/18'), + (('192.168.144.5', '18', '-1'), '192.168.144.4/31'), + (('192.168.144.5', '18', '5'), '192.168.144.0/23'), + (('192.168.144.5', '18', '-5'), '192.168.144.0/27'), + (('span', 'test', 'error'), False), + (('test', ), False), + (('192.168.144.5', '500000', '-5'), False), + (('192.168.144.5', '18', '500000'), False), + (('200000', '18', '-5'), '0.3.13.64/27'), + ) + for args, res in test_cases: + self._test_ipsubnet(args, res) + + def _test_ipsubnet(self, ipsubnet_args, expected_result): + self.assertEqual(ipsubnet(*ipsubnet_args), expected_result) + + with self.assertRaisesRegexp(AnsibleFilterError, 'You must pass a valid subnet or IP address; invalid_subnet is invalid'): + ipsubnet('192.168.144.5', 'invalid_subnet') + + with self.assertRaisesRegexp(AnsibleFilterError, '192.168.144.0/30 is not in the subnet 192.168.144.4/30'): + ipsubnet('192.168.144.1/30', '192.168.144.5/30')