From 8ff83b8ed9e41e7d36e60bffbd11e4088b09a82b Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 12 Sep 2022 14:18:20 -0700 Subject: [PATCH 1/3] Implement TtlRestrictionFilter w/tests --- CHANGELOG.md | 1 + octodns/processor/base.py | 4 ++ octodns/processor/restrict.py | 60 ++++++++++++++++ tests/test_octodns_processor_restrict.py | 89 ++++++++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 octodns/processor/restrict.py create mode 100644 tests/test_octodns_processor_restrict.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 78685f6..54810c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * Now that it's used as it needed to be YamlProvider overrides Provider.supports and just always says Yes so that any dynamically registered types will be supported. +* Add TtlRestrictionFilter processor for adding ttl restriction/checking ## v0.9.18 - 2022-08-14 - Subzone handling diff --git a/octodns/processor/base.py b/octodns/processor/base.py index ac5c155..c6c368e 100644 --- a/octodns/processor/base.py +++ b/octodns/processor/base.py @@ -10,6 +10,10 @@ from __future__ import ( ) +class ProcessorException(Exception): + pass + + class BaseProcessor(object): def __init__(self, name): self.name = name diff --git a/octodns/processor/restrict.py b/octodns/processor/restrict.py new file mode 100644 index 0000000..c23e038 --- /dev/null +++ b/octodns/processor/restrict.py @@ -0,0 +1,60 @@ +# +# +# + +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from .base import BaseProcessor, ProcessorException + + +class RestrictionException(ProcessorException): + pass + + +class TtlRestrictionFilter(BaseProcessor): + ''' + Ensure that configured TTLs are between a configured minimum and maximum. + The default minimum is 1 (the behavior of 0 is undefined spec-wise) and the + default maximum is 604800 (seven days.) + + Example usage: + + processors: + min-max-ttl: + class: octodns.processor.restrict.TtlRestrictionFilter + min_ttl: 60 + max_ttl: 3600 + + zones: + exxampled.com.: + sources: + - config + processors: + - min-max-ttl + targets: + - azure + ''' + + SEVEN_DAYS = 60 * 60 * 24 * 7 + + def __init__(self, name, min_ttl=1, max_ttl=SEVEN_DAYS): + super().__init__(name) + self.min_ttl = min_ttl + self.max_ttl = max_ttl + + def process_source_zone(self, zone, *args, **kwargs): + for record in zone.records: + if record.ttl < self.min_ttl: + raise RestrictionException( + f'{record.fqdn} ttl={record.ttl} too low, min_ttl={self.min_ttl}' + ) + elif record.ttl > self.max_ttl: + raise RestrictionException( + f'{record.fqdn} ttl={record.ttl} too high, max_ttl={self.max_ttl}' + ) + return zone diff --git a/tests/test_octodns_processor_restrict.py b/tests/test_octodns_processor_restrict.py new file mode 100644 index 0000000..bab5b88 --- /dev/null +++ b/tests/test_octodns_processor_restrict.py @@ -0,0 +1,89 @@ +from unittest import TestCase + +from octodns.processor.restrict import ( + RestrictionException, + TtlRestrictionFilter, +) +from octodns.record import Record +from octodns.zone import Zone + + +class TestTtlRestrictionFilter(TestCase): + zone = Zone('unit.tests.', []) + matches = Record.new( + zone, 'matches', {'type': 'A', 'ttl': 42, 'value': '1.2.3.4'} + ) + zone.add_record(matches) + doesnt = Record.new( + zone, 'doesnt', {'type': 'A', 'ttl': 42, 'value': '2.3.4.5'} + ) + zone.add_record(doesnt) + matchable1 = Record.new( + zone, 'start-f43ad96-end', {'type': 'A', 'ttl': 42, 'value': '3.4.5.6'} + ) + zone.add_record(matchable1) + matchable2 = Record.new( + zone, 'start-a3b444c-end', {'type': 'A', 'ttl': 42, 'value': '4.5.6.7'} + ) + zone.add_record(matchable2) + + def test_restrict_ttl(self): + # configured values + restrictor = TtlRestrictionFilter('test', min_ttl=32, max_ttl=1024) + + zone = Zone('unit.tests.', []) + good = Record.new( + zone, 'good', {'type': 'A', 'ttl': 42, 'value': '1.2.3.4'} + ) + zone.add_record(good) + + restricted = restrictor.process_source_zone(zone) + self.assertEqual(zone.records, restricted.records) + + low = Record.new( + zone, 'low', {'type': 'A', 'ttl': 16, 'value': '1.2.3.4'} + ) + copy = zone.copy() + copy.add_record(low) + with self.assertRaises(RestrictionException) as ctx: + restrictor.process_source_zone(copy) + self.assertEqual( + 'low.unit.tests. ttl=16 too low, min_ttl=32', str(ctx.exception) + ) + + high = Record.new( + zone, 'high', {'type': 'A', 'ttl': 2048, 'value': '1.2.3.4'} + ) + copy = zone.copy() + copy.add_record(high) + with self.assertRaises(RestrictionException) as ctx: + restrictor.process_source_zone(copy) + self.assertEqual( + 'high.unit.tests. ttl=2048 too high, max_ttl=1024', + str(ctx.exception), + ) + + # defaults + restrictor = TtlRestrictionFilter('test') + low = Record.new( + zone, 'low', {'type': 'A', 'ttl': 0, 'value': '1.2.3.4'} + ) + copy = zone.copy() + copy.add_record(low) + with self.assertRaises(RestrictionException) as ctx: + restrictor.process_source_zone(copy) + self.assertEqual( + 'low.unit.tests. ttl=0 too low, min_ttl=1', str(ctx.exception) + ) + + high = Record.new( + zone, 'high', {'type': 'A', 'ttl': 999999, 'value': '1.2.3.4'} + ) + copy = zone.copy() + copy.add_record(high) + with self.assertRaises(RestrictionException) as ctx: + restrictor.process_source_zone(copy) + self.assertEqual( + 'high.unit.tests. ttl=999999 too high, max_ttl=604800', + str(ctx.exception), + ) From cabdd1222a22db9193537e173f7416540df74c8c Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 12 Sep 2022 14:32:23 -0700 Subject: [PATCH 2/3] Add lenient support to TtlRestrictionFilter --- octodns/processor/restrict.py | 14 ++++++++++++++ tests/test_octodns_processor_restrict.py | 21 ++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/octodns/processor/restrict.py b/octodns/processor/restrict.py index c23e038..8afc0a5 100644 --- a/octodns/processor/restrict.py +++ b/octodns/processor/restrict.py @@ -38,6 +38,18 @@ class TtlRestrictionFilter(BaseProcessor): - min-max-ttl targets: - azure + + The restriction can be skipped for specific records by setting the lenient + flag, e.g. + + a: + octodns: + lenient: true + ttl: 0 + value: 1.2.3.4 + + The higher level lenient flags are not checked as it would make more sense + to just avoid enabling the processor in those cases. ''' SEVEN_DAYS = 60 * 60 * 24 * 7 @@ -49,6 +61,8 @@ class TtlRestrictionFilter(BaseProcessor): def process_source_zone(self, zone, *args, **kwargs): for record in zone.records: + if record._octodns.get('lenient'): + continue if record.ttl < self.min_ttl: raise RestrictionException( f'{record.fqdn} ttl={record.ttl} too low, min_ttl={self.min_ttl}' diff --git a/tests/test_octodns_processor_restrict.py b/tests/test_octodns_processor_restrict.py index bab5b88..9aae744 100644 --- a/tests/test_octodns_processor_restrict.py +++ b/tests/test_octodns_processor_restrict.py @@ -40,6 +40,7 @@ class TestTtlRestrictionFilter(TestCase): restricted = restrictor.process_source_zone(zone) self.assertEqual(zone.records, restricted.records) + # too low low = Record.new( zone, 'low', {'type': 'A', 'ttl': 16, 'value': '1.2.3.4'} ) @@ -51,6 +52,23 @@ class TestTtlRestrictionFilter(TestCase): 'low.unit.tests. ttl=16 too low, min_ttl=32', str(ctx.exception) ) + # with lenient set, we can go lower + lenient = Record.new( + zone, + 'low', + { + 'octodns': {'lenient': True}, + 'type': 'A', + 'ttl': 16, + 'value': '1.2.3.4', + }, + ) + copy = zone.copy() + copy.add_record(lenient) + restricted = restrictor.process_source_zone(copy) + self.assertEqual(copy.records, restricted.records) + + # too high high = Record.new( zone, 'high', {'type': 'A', 'ttl': 2048, 'value': '1.2.3.4'} ) @@ -63,7 +81,7 @@ class TestTtlRestrictionFilter(TestCase): str(ctx.exception), ) - # defaults + # too low defaults restrictor = TtlRestrictionFilter('test') low = Record.new( zone, 'low', {'type': 'A', 'ttl': 0, 'value': '1.2.3.4'} @@ -76,6 +94,7 @@ class TestTtlRestrictionFilter(TestCase): 'low.unit.tests. ttl=0 too low, min_ttl=1', str(ctx.exception) ) + # too high defaults high = Record.new( zone, 'high', {'type': 'A', 'ttl': 999999, 'value': '1.2.3.4'} ) From 48831659e5896e4354f4affec71f8bdf5fe082bd Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 14 Sep 2022 06:46:13 -0700 Subject: [PATCH 3/3] Add allowed_ttls support to TtlRestrictionFilter --- octodns/processor/restrict.py | 17 +++++++--- tests/test_octodns_processor_restrict.py | 41 +++++++++++++----------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/octodns/processor/restrict.py b/octodns/processor/restrict.py index 8afc0a5..1903995 100644 --- a/octodns/processor/restrict.py +++ b/octodns/processor/restrict.py @@ -18,9 +18,12 @@ class RestrictionException(ProcessorException): class TtlRestrictionFilter(BaseProcessor): ''' - Ensure that configured TTLs are between a configured minimum and maximum. + Ensure that configured TTLs are between a configured minimum and maximum or + in an allowed set of values. + The default minimum is 1 (the behavior of 0 is undefined spec-wise) and the - default maximum is 604800 (seven days.) + default maximum is 604800 (seven days.) allowed_ttls is only used when + explicitly configured and min and max are ignored in that case. Example usage: @@ -29,6 +32,7 @@ class TtlRestrictionFilter(BaseProcessor): class: octodns.processor.restrict.TtlRestrictionFilter min_ttl: 60 max_ttl: 3600 + # allowed_ttls: [300, 900, 3600] zones: exxampled.com.: @@ -54,16 +58,21 @@ class TtlRestrictionFilter(BaseProcessor): SEVEN_DAYS = 60 * 60 * 24 * 7 - def __init__(self, name, min_ttl=1, max_ttl=SEVEN_DAYS): + def __init__(self, name, min_ttl=1, max_ttl=SEVEN_DAYS, allowed_ttls=None): super().__init__(name) self.min_ttl = min_ttl self.max_ttl = max_ttl + self.allowed_ttls = set(allowed_ttls) if allowed_ttls else None def process_source_zone(self, zone, *args, **kwargs): for record in zone.records: if record._octodns.get('lenient'): continue - if record.ttl < self.min_ttl: + if self.allowed_ttls and record.ttl not in self.allowed_ttls: + raise RestrictionException( + f'{record.fqdn} ttl={record.ttl} not an allowed value, allowed_ttls={self.allowed_ttls}' + ) + elif record.ttl < self.min_ttl: raise RestrictionException( f'{record.fqdn} ttl={record.ttl} too low, min_ttl={self.min_ttl}' ) diff --git a/tests/test_octodns_processor_restrict.py b/tests/test_octodns_processor_restrict.py index 9aae744..4848ae6 100644 --- a/tests/test_octodns_processor_restrict.py +++ b/tests/test_octodns_processor_restrict.py @@ -9,24 +9,6 @@ from octodns.zone import Zone class TestTtlRestrictionFilter(TestCase): - zone = Zone('unit.tests.', []) - matches = Record.new( - zone, 'matches', {'type': 'A', 'ttl': 42, 'value': '1.2.3.4'} - ) - zone.add_record(matches) - doesnt = Record.new( - zone, 'doesnt', {'type': 'A', 'ttl': 42, 'value': '2.3.4.5'} - ) - zone.add_record(doesnt) - matchable1 = Record.new( - zone, 'start-f43ad96-end', {'type': 'A', 'ttl': 42, 'value': '3.4.5.6'} - ) - zone.add_record(matchable1) - matchable2 = Record.new( - zone, 'start-a3b444c-end', {'type': 'A', 'ttl': 42, 'value': '4.5.6.7'} - ) - zone.add_record(matchable2) - def test_restrict_ttl(self): # configured values restrictor = TtlRestrictionFilter('test', min_ttl=32, max_ttl=1024) @@ -106,3 +88,26 @@ class TestTtlRestrictionFilter(TestCase): 'high.unit.tests. ttl=999999 too high, max_ttl=604800', str(ctx.exception), ) + + # allowed_ttls + restrictor = TtlRestrictionFilter('test', allowed_ttls=[42, 300]) + + # add 300 (42 is already there) + another = Record.new( + zone, 'another', {'type': 'A', 'ttl': 300, 'value': '4.5.6.7'} + ) + zone.add_record(another) + + # 42 and 300 are allowed through + restricted = restrictor.process_source_zone(zone) + self.assertEqual(zone.records, restricted.records) + + # 16 is not + copy = zone.copy() + copy.add_record(low) + with self.assertRaises(RestrictionException) as ctx: + restrictor.process_source_zone(copy) + self.assertEqual( + 'low.unit.tests. ttl=0 not an allowed value, allowed_ttls={42, 300}', + str(ctx.exception), + )