From 79b2a2568460d0a97810320e7b1b3a94d317cd25 Mon Sep 17 00:00:00 2001 From: William Gauthier Date: Wed, 17 Apr 2024 19:33:25 +0200 Subject: [PATCH] Add a priority option to AutoArpa --- docs/auto_arpa.md | 2 + docs/records.md | 11 +++++ octodns/processor/arpa.py | 16 +++++-- tests/test_octodns_processor_arpa.py | 65 +++++++++++++++++++--------- 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/docs/auto_arpa.md b/docs/auto_arpa.md index 9f7eb61..a5411c2 100644 --- a/docs/auto_arpa.md +++ b/docs/auto_arpa.md @@ -19,6 +19,8 @@ manager: populate_should_replace: false # Explicitly set the TTL of auto-created records, default is 3600s, 1hr ttl: 1800 + # Set how many PTR records will be created for the same IP, default: 999 + max_auto_arpa: 1 ``` Once enabled, a singleton `AutoArpa` instance, `auto-arpa`, will be added to the pool of providers and globally configured to run as the very last global processor so that it will see all records as they will be seen by targets. Further all zones ending with `arpa.` will be held back and processed after all other zones have been completed so that all `A` and `AAAA` records will have been seen prior to planning the `arpa.` zones. diff --git a/docs/records.md b/docs/records.md index d0100d3..e3b878e 100644 --- a/docs/records.md +++ b/docs/records.md @@ -94,6 +94,17 @@ octoDNS is fairly strict in terms of standards compliance and is opinionated in It's best to think of the `lenient` flag as "I know what I'm doing and accept any problems I run across." The main reason being is that some providers may allow the non-compliant setup and others may not. The behavior of the non-compliant records may even vary from one provider to another. Caveat emptor. +#### Record priority for AutoArpa +When multiple A or AAAA records point to the same IP, it is possible to set an optional priority on each record. The records with the lowest priority will have the highest preference when being processed by AutoArpa. The AutoArpa provider will create PTR records in order of preference, up to a set limit defined by the `max_auto_arpa` option in the provider configuration. + +```yaml +test: +- type: A + value: 1.2.3.4 + octodns: + auto_arpa_priority: 1 +``` + #### octodns-dump If you're trying to import a zone into octoDNS config file using `octodns-dump` which fails due to validation errors you can supply the `--lenient` argument to tell octoDNS that you acknowledge that things aren't lining up with its expectations, but you'd like it to go ahead anyway. This will do its best to populate the zone and dump the results out into an octoDNS zone file and include the non-compliant bits. If you go to use that config file octoDNS will again complain about the validation problems. You can correct them in cases where that makes sense, but if you need to preserve the non-compliant records read on for options. diff --git a/octodns/processor/arpa.py b/octodns/processor/arpa.py index 960fb55..1e121eb 100644 --- a/octodns/processor/arpa.py +++ b/octodns/processor/arpa.py @@ -11,17 +11,19 @@ from .base import BaseProcessor class AutoArpa(BaseProcessor): - def __init__(self, name, ttl=3600, populate_should_replace=False): + def __init__(self, name, ttl=3600, populate_should_replace=False, max_auto_arpa=999): super().__init__(name) self.log = getLogger(f'AutoArpa[{name}]') self.log.info( - '__init__: ttl=%d, populate_should_replace=%s', + '__init__: ttl=%d, populate_should_replace=%s, max_auto_arpa=%d', ttl, populate_should_replace, + max_auto_arpa ) self.ttl = ttl self.populate_should_replace = populate_should_replace - self._records = defaultdict(set) + self.max_auto_arpa = max_auto_arpa + self._records = defaultdict(list) def process_source_zone(self, desired, sources): for record in desired.records: @@ -37,7 +39,10 @@ class AutoArpa(BaseProcessor): for ip in ips: ptr = ip_address(ip).reverse_pointer - self._records[f'{ptr}.'].add(record.fqdn) + auto_arpa_priority = record.octodns.get('auto_arpa_priority', 999) + self._records[f'{ptr}.'].append((auto_arpa_priority, record.fqdn)) + unique_list = list(set(self._records[f'{ptr}.'])) + self._records[f'{ptr}.'] = unique_list return desired @@ -57,6 +62,9 @@ class AutoArpa(BaseProcessor): if arpa.endswith(f'.{zone_name}'): name = arpa[:-n] fqdns = sorted(fqdns) + fqdns = [d[1] for d in fqdns] + fqdns = fqdns[:self.max_auto_arpa] + record = Record.new( zone, name, diff --git a/tests/test_octodns_processor_arpa.py b/tests/test_octodns_processor_arpa.py index 32fc49d..124ca0a 100644 --- a/tests/test_octodns_processor_arpa.py +++ b/tests/test_octodns_processor_arpa.py @@ -27,7 +27,7 @@ class TestAutoArpa(TestCase): aa = AutoArpa('auto-arpa') aa.process_source_zone(zone, []) self.assertEqual( - {'4.3.2.1.in-addr.arpa.': {'a.unit.tests.'}}, aa._records + {'4.3.2.1.in-addr.arpa.': [(999, 'a.unit.tests.')]}, aa._records ) # matching zone @@ -56,8 +56,8 @@ class TestAutoArpa(TestCase): aa.process_source_zone(zone, []) self.assertEqual( { - '4.3.2.1.in-addr.arpa.': {'a.unit.tests.'}, - '5.3.2.1.in-addr.arpa.': {'a.unit.tests.'}, + '4.3.2.1.in-addr.arpa.': [(999, 'a.unit.tests.')], + '5.3.2.1.in-addr.arpa.': [(999, 'a.unit.tests.')], }, aa._records, ) @@ -81,7 +81,7 @@ class TestAutoArpa(TestCase): aa = AutoArpa('auto-arpa') aa.process_source_zone(zone, []) ip6_arpa = '2.0.0.0.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.c.0.0.0.f.f.0.0.ip6.arpa.' - self.assertEqual({ip6_arpa: {'aaaa.unit.tests.'}}, aa._records) + self.assertEqual({ip6_arpa: [(999, 'aaaa.unit.tests.')]}, aa._records) # matching zone arpa = Zone('c.0.0.0.f.f.0.0.ip6.arpa.', []) @@ -117,13 +117,13 @@ class TestAutoArpa(TestCase): aa.process_source_zone(zone, []) self.assertEqual( { - '1.1.1.1.in-addr.arpa.': {'geo.unit.tests.'}, - '2.2.2.2.in-addr.arpa.': {'geo.unit.tests.'}, - '3.3.3.3.in-addr.arpa.': {'geo.unit.tests.'}, - '4.4.4.4.in-addr.arpa.': {'geo.unit.tests.'}, - '5.5.5.5.in-addr.arpa.': {'geo.unit.tests.'}, - '4.3.2.1.in-addr.arpa.': {'geo.unit.tests.'}, - '5.3.2.1.in-addr.arpa.': {'geo.unit.tests.'}, + '4.3.2.1.in-addr.arpa.': [(999, 'geo.unit.tests.')], + '5.3.2.1.in-addr.arpa.': [(999, 'geo.unit.tests.')], + '1.1.1.1.in-addr.arpa.': [(999, 'geo.unit.tests.')], + '2.2.2.2.in-addr.arpa.': [(999, 'geo.unit.tests.')], + '3.3.3.3.in-addr.arpa.': [(999, 'geo.unit.tests.')], + '4.4.4.4.in-addr.arpa.': [(999, 'geo.unit.tests.')], + '5.5.5.5.in-addr.arpa.': [(999, 'geo.unit.tests.')] }, aa._records, ) @@ -167,11 +167,11 @@ class TestAutoArpa(TestCase): aa.process_source_zone(zone, []) self.assertEqual( { - '3.3.3.3.in-addr.arpa.': {'dynamic.unit.tests.'}, - '4.4.4.4.in-addr.arpa.': {'dynamic.unit.tests.'}, - '5.5.5.5.in-addr.arpa.': {'dynamic.unit.tests.'}, - '4.3.2.1.in-addr.arpa.': {'dynamic.unit.tests.'}, - '5.3.2.1.in-addr.arpa.': {'dynamic.unit.tests.'}, + '4.3.2.1.in-addr.arpa.': [(999, 'dynamic.unit.tests.')], + '5.3.2.1.in-addr.arpa.': [(999, 'dynamic.unit.tests.')], + '3.3.3.3.in-addr.arpa.': [(999, 'dynamic.unit.tests.')], + '4.4.4.4.in-addr.arpa.': [(999, 'dynamic.unit.tests.')], + '5.5.5.5.in-addr.arpa.': [(999, 'dynamic.unit.tests.')], }, aa._records, ) @@ -188,11 +188,13 @@ class TestAutoArpa(TestCase): zone.add_record(record2) aa = AutoArpa('auto-arpa') aa.process_source_zone(zone, []) + sorted_records = sorted(aa._records['4.3.2.1.in-addr.arpa.']) self.assertEqual( - {'4.3.2.1.in-addr.arpa.': {'a1.unit.tests.', 'a2.unit.tests.'}}, - aa._records, + {'4.3.2.1.in-addr.arpa.': [(999, 'a1.unit.tests.'), (999, 'a2.unit.tests.')]}, + {'4.3.2.1.in-addr.arpa.': sorted_records } ) + # matching zone arpa = Zone('3.2.1.in-addr.arpa.', []) aa.populate(arpa) @@ -211,7 +213,7 @@ class TestAutoArpa(TestCase): aa = AutoArpa('auto-arpa') aa.process_source_zone(zone, []) self.assertEqual( - {'4.3.20.10.in-addr.arpa.': {'a.unit.tests.'}}, aa._records + {'4.3.20.10.in-addr.arpa.': [(999, 'a.unit.tests.')]}, aa._records ) # matching zone @@ -251,7 +253,7 @@ class TestAutoArpa(TestCase): aa = AutoArpa('auto-arpa') aa.process_source_zone(zone, []) self.assertEqual( - {'4.3.2.1.in-addr.arpa.': {'a with spaces.unit.tests.'}}, + {'4.3.2.1.in-addr.arpa.': [(999, 'a with spaces.unit.tests.')]}, aa._records, ) @@ -263,3 +265,26 @@ class TestAutoArpa(TestCase): self.assertEqual('4.3.2.1.in-addr.arpa.', ptr.fqdn) self.assertEqual(record.fqdn, ptr.value) self.assertEqual(3600, ptr.ttl) + + def test_arpa_priority(self): + zone = Zone('unit.tests.', []) + record = Record.new( + zone, + 'a', + {'ttl': 32, 'type': 'A', 'value': '1.2.3.4'}, + ) + zone.add_record(record) + record2 = Record.new( + zone, + 'b', + {'ttl': 32, 'type': 'A', 'value': '1.2.3.4', 'octodns': { 'auto_arpa_priority': 1}}, + ) + zone.add_record(record2) + + aa = AutoArpa('auto-arpa', ttl=1600, populate_should_replace=False, max_auto_arpa=1) + aa.process_source_zone(zone, []) + + arpa = Zone('3.2.1.in-addr.arpa.', []) + aa.populate(arpa) + (ptr), = arpa.records + self.assertEqual(record2.fqdn, ptr.value)