diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec139a..22840c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ * Added ZoneNameFilter processor to enable ignoring/alerting on type-os like octodns.com.octodns.com +* NetworkValueAllowlistFilter/NetworkValueRejectlistFilter added to + processors.filter to enable filtering A/AAAA records based on value. Can be + useful if you have records with non-routable values in an internal copy of a + zone, but want to exclude them when pushing the same zone publically (split + horizon) * ExcludeRootNsChanges processor that will error (or warn) if plan includes a change to root NS records * Include the octodns special section info in Record __repr__, makes it easier diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index 2de3b23..e7913d8 100644 --- a/octodns/processor/filter.py +++ b/octodns/processor/filter.py @@ -2,6 +2,8 @@ # # +from ipaddress import ip_address, ip_network +from itertools import product from logging import getLogger from re import compile as re_compile @@ -189,6 +191,91 @@ class NameRejectlistFilter(_NameBaseFilter, RejectsMixin): super().__init__(name, rejectlist) +class _NetworkValueBaseFilter(BaseProcessor): + def __init__(self, name, _list): + super().__init__(name) + self.networks = [] + for value in _list: + try: + self.networks.append(ip_network(value)) + except ValueError: + raise ValueError(f'{value} is not a valid CIDR to use') + + def _process(self, zone, *args, **kwargs): + for record in zone.records: + if record._type not in ['A', 'AAAA']: + continue + + ips = [ip_address(value) for value in record.values] + if any( + ip in network for ip, network in product(ips, self.networks) + ): + self.matches(zone, record) + else: + self.doesnt_match(zone, record) + + return zone + + process_source_zone = _process + process_target_zone = _process + + +class NetworkValueAllowlistFilter(_NetworkValueBaseFilter, AllowsMixin): + '''Only manage A and AAAA records with values that match the provider patterns + All other types will be left as-is. + + Example usage: + + processors: + only-these: + class: octodns.processor.filter.NetworkValueAllowlistFilter + allowlist: + - 127.0.0.1/32 + - 192.168.0.0/16 + - fd00::/8 + + zones: + exxampled.com.: + sources: + - config + processors: + - only-these + targets: + - route53 + ''' + + def __init__(self, name, allowlist): + super().__init__(name, allowlist) + + +class NetworkValueRejectlistFilter(_NetworkValueBaseFilter, RejectsMixin): + '''Reject managing A and AAAA records with value matching a that match the provider patterns + All other types will be left as-is. + + Example usage: + + processors: + not-these: + class: octodns.processor.filter.NetworkValueRejectlistFilter + rejectlist: + - 127.0.0.1/32 + - 192.168.0.0/16 + - fd00::/8 + + zones: + exxampled.com.: + sources: + - config + processors: + - not-these + targets: + - route53 + ''' + + def __init__(self, name, rejectlist): + super().__init__(name, rejectlist) + + class IgnoreRootNsFilter(BaseProcessor): '''Do not manage Root NS Records. diff --git a/tests/test_octodns_processor_filter.py b/tests/test_octodns_processor_filter.py index 2d9b881..7ee98a8 100644 --- a/tests/test_octodns_processor_filter.py +++ b/tests/test_octodns_processor_filter.py @@ -9,6 +9,8 @@ from octodns.processor.filter import ( IgnoreRootNsFilter, NameAllowlistFilter, NameRejectlistFilter, + NetworkValueAllowlistFilter, + NetworkValueRejectlistFilter, TypeAllowlistFilter, TypeRejectlistFilter, ZoneNameFilter, @@ -161,6 +163,66 @@ class TestNameRejectListFilter(TestCase): ) +class TestNetworkValueFilter(TestCase): + zone = Zone('unit.tests.', []) + for record in [ + Record.new( + zone, + 'private-ipv4', + {'type': 'A', 'ttl': 42, 'value': '10.42.42.42'}, + ), + Record.new( + zone, + 'public-ipv4', + {'type': 'A', 'ttl': 42, 'value': '42.42.42.42'}, + ), + Record.new( + zone, + 'private-ipv6', + {'type': 'AAAA', 'ttl': 42, 'value': 'fd12:3456:789a:1::1'}, + ), + Record.new( + zone, + 'public-ipv6', + {'type': 'AAAA', 'ttl': 42, 'value': 'dead:beef:cafe::1'}, + ), + Record.new( + zone, + 'keep-me', + {'ttl': 30, 'type': 'TXT', 'value': 'this should always be here'}, + ), + ]: + zone.add_record(record) + + def test_bad_config(self): + with self.assertRaises(ValueError): + NetworkValueRejectlistFilter( + 'rejectlist', set(('string', '42.42.42.42/43')) + ) + + def test_reject(self): + filter_private = NetworkValueRejectlistFilter( + 'rejectlist', set(('10.0.0.0/8', 'fd00::/8')) + ) + + got = filter_private.process_source_zone(self.zone.copy()) + self.assertEqual( + ['keep-me', 'public-ipv4', 'public-ipv6'], + sorted([r.name for r in got.records]), + ) + + def test_allow(self): + filter_private = NetworkValueAllowlistFilter( + 'allowlist', set(('10.0.0.0/8', 'fd00::/8')) + ) + + got = filter_private.process_source_zone(self.zone.copy()) + self.assertEqual( + ['keep-me', 'private-ipv4', 'private-ipv6'], + sorted([r.name for r in got.records]), + ) + + class TestIgnoreRootNsFilter(TestCase): zone = Zone('unit.tests.', []) root = Record.new(