diff --git a/CHANGELOG.md b/CHANGELOG.md index b4cd240..19c4aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,23 @@ ## v1.3.0 - 2023-??-?? - ??? -* Added ZoneNameFilter processor to enable ignoring/alerting on type-os like - octodns.com.octodns.com +#### Noteworthy changes + +* Added octodns.__version__ to replace octodns.__VERSION__ as the former is more + of a standard, per pep-8. __VERSION__ is deprecated and will go away in 2.x * Fixed issues with handling of chunking large TXT values for providers that use the in-built `rrs` method +* Removed code that included sha in module version number when installing from + repo as it caused problems with non-binary installs. + +#### Stuff + +* 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/__init__.py b/octodns/__init__.py index e106d6c..1ae31b0 100644 --- a/octodns/__init__.py +++ b/octodns/__init__.py @@ -1,3 +1,4 @@ 'OctoDNS: DNS as code - Tools for managing DNS across multiple providers' -__VERSION__ = '1.2.1' +# TODO: remove __VERSION__ w/2.x +__version__ = __VERSION__ = '1.2.1' diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index 31c12c9..cd1c82d 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 @@ -213,6 +215,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/setup.py b/setup.py index aa02a1a..a3e9ec5 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,7 @@ #!/usr/bin/env python from io import StringIO -from os import environ from os.path import dirname, join -from subprocess import CalledProcessError, check_output import octodns @@ -49,19 +47,6 @@ def long_description(): return buf.getvalue() -def version(): - # pep440 style public & local version numbers - if environ.get('OCTODNS_RELEASE', False): - # public - return octodns.__VERSION__ - try: - sha = check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8')[:8] - except (CalledProcessError, FileNotFoundError): - sha = 'unknown' - # local - return f'{octodns.__VERSION__}+{sha}' - - tests_require = ('pytest>=6.2.5', 'pytest-cov>=3.0.0', 'pytest-network>=0.0.1') setup( @@ -102,5 +87,5 @@ setup( python_requires='>=3.8', tests_require=tests_require, url='https://github.com/octodns/octodns', - version=version(), + version=octodns.__version__, ) diff --git a/tests/test_octodns_processor_filter.py b/tests/test_octodns_processor_filter.py index d4c36ef..1880a16 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, @@ -177,6 +179,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(