From 67c75ff146e90c5c4ec85faf101636ffb2c411ab Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 17 Aug 2022 09:58:35 -0700 Subject: [PATCH 01/32] Implement IdnaDict, a dict with case and idna/utf-8 insensitive keys --- octodns/idna.py | 35 ++++++++++++++++ tests/test_octodns_idna.py | 86 +++++++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/octodns/idna.py b/octodns/idna.py index bc91d46..bf30343 100644 --- a/octodns/idna.py +++ b/octodns/idna.py @@ -2,6 +2,8 @@ # # +from collections.abc import MutableMapping + from idna import decode as _decode, encode as _encode # Providers will need to to make calls to these at the appropriate points, @@ -12,6 +14,7 @@ from idna import decode as _decode, encode as _encode def idna_encode(name): # Based on https://github.com/psf/requests/pull/3695/files # #diff-0debbb2447ce5debf2872cb0e17b18babe3566e9d9900739e8581b355bd513f7R39 + name = name.lower() try: name.encode('ascii') # No utf8 chars, just use as-is @@ -34,3 +37,35 @@ def idna_decode(name): return _decode(name) # not idna, just return as-is return name + + +class IdnaDict(MutableMapping): + '''A dict type that is insensitive to case and utf-8/idna encoded strings''' + + def __init__(self, data=None): + self._data = dict() + if data is not None: + self.update(data) + + def __setitem__(self, k, v): + self._data[idna_encode(k)] = v + + def __getitem__(self, k): + return self._data[idna_encode(k)] + + def __delitem__(self, k): + del self._data[idna_encode(k)] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def decoded_keys(self): + for key in self.keys(): + yield idna_decode(key) + + def decoded_items(self): + for key, value in self.items(): + yield (idna_decode(key), value) diff --git a/tests/test_octodns_idna.py b/tests/test_octodns_idna.py index 0c6b125..2e09401 100644 --- a/tests/test_octodns_idna.py +++ b/tests/test_octodns_idna.py @@ -11,7 +11,7 @@ from __future__ import ( from unittest import TestCase -from octodns.idna import idna_decode, idna_encode +from octodns.idna import IdnaDict, idna_decode, idna_encode class TestIdna(TestCase): @@ -56,5 +56,87 @@ class TestIdna(TestCase): self.assertIdna('bleep_bloop.foo_bar.pl.', 'bleep_bloop.foo_bar.pl.') def test_case_insensitivity(self): - # Shouldn't be hit by octoDNS use cases, but checked anyway self.assertEqual('zajęzyk.pl.', idna_decode('XN--ZAJZYK-Y4A.PL.')) + self.assertEqual('xn--zajzyk-y4a.pl.', idna_encode('ZajęzyK.Pl.')) + + +class TestIdnaDict(TestCase): + plain = 'testing.tests.' + almost = 'tésting.tests.' + utf8 = 'déjà.vu.' + + normal = {plain: 42, almost: 43, utf8: 44} + + def test_basics(self): + d = IdnaDict() + + # plain ascii + d[self.plain] = 42 + self.assertEqual(42, d[self.plain]) + + # almost the same, single utf-8 char + d[self.almost] = 43 + # fetch as utf-8 + self.assertEqual(43, d[self.almost]) + # fetch as idna + self.assertEqual(43, d[idna_encode(self.almost)]) + # plain is stil there, unchanged + self.assertEqual(42, d[self.plain]) + + # lots of utf8 + d[self.utf8] = 44 + self.assertEqual(44, d[self.utf8]) + self.assertEqual(44, d[idna_encode(self.utf8)]) + + # setting with idna version replaces something set previously with utf8 + d[idna_encode(self.almost)] = 45 + self.assertEqual(45, d[self.almost]) + self.assertEqual(45, d[idna_encode(self.almost)]) + + # contains + self.assertTrue(self.plain in d) + self.assertTrue(self.almost in d) + self.assertTrue(idna_encode(self.almost) in d) + self.assertTrue(self.utf8 in d) + self.assertTrue(idna_encode(self.utf8) in d) + + # we can delete with either form + del d[self.almost] + self.assertFalse(self.almost in d) + self.assertFalse(idna_encode(self.almost) in d) + del d[idna_encode(self.utf8)] + self.assertFalse(self.utf8 in d) + self.assertFalse(idna_encode(self.utf8) in d) + + def test_keys(self): + d = IdnaDict(self.normal) + + # keys are idna versions by default + self.assertEqual( + (self.plain, idna_encode(self.almost), idna_encode(self.utf8)), + tuple(d.keys()), + ) + + # decoded keys gives the utf8 version + self.assertEqual( + (self.plain, self.almost, self.utf8), tuple(d.decoded_keys()) + ) + + def test_items(self): + d = IdnaDict(self.normal) + + # idna keys in items + self.assertEqual( + ( + (self.plain, 42), + (idna_encode(self.almost), 43), + (idna_encode(self.utf8), 44), + ), + tuple(d.items()), + ) + + # utf8 keys in decoded_items + self.assertEqual( + ((self.plain, 42), (self.almost, 43), (self.utf8, 44)), + tuple(d.decoded_items()), + ) From c1ef45e0fd0e5f9a1e998be9ce219ed2f82786f0 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 17 Aug 2022 10:28:17 -0700 Subject: [PATCH 02/32] Convert Manager.config['zones'] to IdnaDict - print Zone.decoded_name to logs for better readability - Prefer decoded name in YamlProvider --- octodns/manager.py | 15 ++++++++++----- octodns/provider/base.py | 6 +++--- octodns/provider/plan.py | 12 ++++++------ octodns/provider/yaml.py | 20 +++++++++++++++----- octodns/record/__init__.py | 2 +- octodns/zone.py | 4 +++- 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index 40a9c4f..c03398f 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -17,6 +17,7 @@ from sys import stdout import logging from . import __VERSION__ +from .idna import IdnaDict, idna_decode from .provider.base import BaseProvider from .provider.plan import Plan from .provider.yaml import SplitYamlProvider, YamlProvider @@ -114,6 +115,8 @@ class Manager(object): # Read our config file with open(config_file, 'r') as fh: self.config = safe_load(fh, enforce_order=False) + # convert the zones portion of things into an IdnaDict + self.config['zones'] = IdnaDict(self.config['zones']) manager_config = self.config.get('manager', {}) max_workers = ( @@ -341,10 +344,12 @@ class Manager(object): lenient=False, ): + zone = Zone(zone_name, sub_zones=self.configured_sub_zones(zone_name)) self.log.debug( - 'sync: populating, zone=%s, lenient=%s', zone_name, lenient + 'sync: populating, zone=%s, lenient=%s', + zone.decoded_name, + lenient, ) - zone = Zone(zone_name, sub_zones=self.configured_sub_zones(zone_name)) if desired: # This is an alias zone, rather than populate it we'll copy the @@ -368,7 +373,7 @@ class Manager(object): for processor in processors: zone = processor.process_source_zone(zone, sources=sources) - self.log.debug('sync: planning, zone=%s', zone_name) + self.log.debug('sync: planning, zone=%s', zone.decoded_name) plans = [] for target in targets: @@ -431,7 +436,7 @@ class Manager(object): aliased_zones = {} futures = [] for zone_name, config in zones: - self.log.info('sync: zone=%s', zone_name) + self.log.info('sync: zone=%s', idna_decode(zone_name)) if 'alias' in config: source_zone = config['alias'] @@ -602,7 +607,7 @@ class Manager(object): self.log.debug('sync: applying') zones = self.config['zones'] for target, plan in plans: - zone_name = plan.existing.name + zone_name = plan.existing.decoded_name if zones[zone_name].get('always-dry-run', False): self.log.info( 'sync: zone=%s skipping always-dry-run', zone_name diff --git a/octodns/provider/base.py b/octodns/provider/base.py index 78001fa..2a5366c 100644 --- a/octodns/provider/base.py +++ b/octodns/provider/base.py @@ -114,7 +114,7 @@ class BaseProvider(BaseSource): self.log.warning( 'root NS record supported, but no record ' 'is configured for %s', - desired.name, + desired.decoded_name, ) else: if record: @@ -179,7 +179,7 @@ class BaseProvider(BaseSource): self.log.warning('%s; %s', msg, fallback) def plan(self, desired, processors=[]): - self.log.info('plan: desired=%s', desired.name) + self.log.info('plan: desired=%s', desired.decoded_name) existing = Zone(desired.name, desired.sub_zones) exists = self.populate(existing, target=True, lenient=True) @@ -247,7 +247,7 @@ class BaseProvider(BaseSource): self.log.info('apply: disabled') return 0 - zone_name = plan.desired.name + zone_name = plan.desired.decoded_name num_changes = len(plan.changes) self.log.info('apply: making %d changes to %s', num_changes, zone_name) self._apply(plan) diff --git a/octodns/provider/plan.py b/octodns/provider/plan.py index a436483..d9abd92 100644 --- a/octodns/provider/plan.py +++ b/octodns/provider/plan.py @@ -165,8 +165,8 @@ class PlanLogger(_PlanOutput): if plans: current_zone = None for target, plan in plans: - if plan.desired.name != current_zone: - current_zone = plan.desired.name + if plan.desired.decoded_name != current_zone: + current_zone = plan.desired.decoded_name buf.write(hr) buf.write('* ') buf.write(current_zone) @@ -215,8 +215,8 @@ class PlanMarkdown(_PlanOutput): if plans: current_zone = None for target, plan in plans: - if plan.desired.name != current_zone: - current_zone = plan.desired.name + if plan.desired.decoded_name != current_zone: + current_zone = plan.desired.decoded_name fh.write('## ') fh.write(current_zone) fh.write('\n\n') @@ -276,8 +276,8 @@ class PlanHtml(_PlanOutput): if plans: current_zone = None for target, plan in plans: - if plan.desired.name != current_zone: - current_zone = plan.desired.name + if plan.desired.decoded_name != current_zone: + current_zone = plan.desired.decoded_name fh.write('

') fh.write(current_zone) fh.write('

\n') diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index 9ad0934..93b9b12 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -147,7 +147,7 @@ class YamlProvider(BaseProvider): self.log = logging.getLogger(f'{klass}[{id}]') self.log.debug( '__init__: id=%s, directory=%s, default_ttl=%d, ' - 'enforce_order=%d, populate_should_replace=%d', + 'nforce_order=%d, populate_should_replace=%d', id, directory, default_ttl, @@ -196,7 +196,7 @@ class YamlProvider(BaseProvider): def populate(self, zone, target=False, lenient=False): self.log.debug( 'populate: name=%s, target=%s, lenient=%s', - zone.name, + zone.decoded_name, target, lenient, ) @@ -207,7 +207,15 @@ class YamlProvider(BaseProvider): return False before = len(zone.records) - filename = join(self.directory, f'{zone.name}yaml') + filename = join(self.directory, f'{zone.decoded_name}yaml') + if not isfile(filename): + idna_filename = join(self.directory, f'{zone.name}yaml') + self.log.warning( + 'populate: "%s" does not exist, falling back to idna version "%s"', + filename, + idna_filename, + ) + filename = idna_filename self._populate_from_file(filename, zone, lenient) self.log.info( @@ -220,7 +228,9 @@ class YamlProvider(BaseProvider): desired = plan.desired changes = plan.changes self.log.debug( - '_apply: zone=%s, len(changes)=%d', desired.name, len(changes) + '_apply: zone=%s, len(changes)=%d', + desired.decoded_name, + len(changes), ) # Since we don't have existing we'll only see creates records = [c.new for c in changes] @@ -248,7 +258,7 @@ class YamlProvider(BaseProvider): self._do_apply(desired, data) def _do_apply(self, desired, data): - filename = join(self.directory, f'{desired.name}yaml') + filename = join(self.directory, f'{desired.decoded_name}yaml') self.log.debug('_apply: writing filename=%s', filename) with open(filename, 'w') as fh: safe_dump(dict(data), fh) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index d7c6be4..a7db144 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -164,7 +164,7 @@ class Record(EqualityTupleMixin): def __init__(self, zone, name, data, source=None): self.log.debug( '__init__: zone.name=%s, type=%11s, name=%s', - zone.name, + zone.decoded_name, self.__class__.__name__, name, ) diff --git a/octodns/zone.py b/octodns/zone.py index 1bc3724..999a656 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -13,6 +13,7 @@ from collections import defaultdict from logging import getLogger import re +from .idna import idna_decode from .record import Create, Delete @@ -36,6 +37,7 @@ class Zone(object): raise Exception(f'Invalid zone name {name}, missing ending dot') # Force everything to lowercase just to be safe self.name = str(name).lower() if name else name + self.decoded_name = idna_decode(self.name) self.sub_zones = sub_zones # We're grouping by node, it allows us to efficiently search for # duplicates and detect when CNAMEs co-exist with other records @@ -283,4 +285,4 @@ class Zone(object): return copy def __repr__(self): - return f'Zone<{self.name}>' + return f'Zone<{self.decoded_name}>' From bfe4ff3d2eb18a021b07bb9a44f723dd309c0d1f Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 17 Aug 2022 18:10:40 -0700 Subject: [PATCH 03/32] Break up Manager.__init__ configuration bits for easier testing --- octodns/manager.py | 87 +++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index c03398f..32a2802 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -112,32 +112,58 @@ class Manager(object): '__init__: config_file=%s (octoDNS %s)', config_file, version ) + self._configured_sub_zones = None + # Read our config file with open(config_file, 'r') as fh: self.config = safe_load(fh, enforce_order=False) + # convert the zones portion of things into an IdnaDict self.config['zones'] = IdnaDict(self.config['zones']) manager_config = self.config.get('manager', {}) + self._executor = self._config_executor(manager_config, max_workers) + self.include_meta = self._config_include_meta( + manager_config, include_meta + ) + + providers_config = self.config['providers'] + self.providers = self._config_providers(providers_config) + + processors_config = self.config.get('processors', {}) + self.processors = self._config_processors(processors_config) + + plan_outputs_config = manager_config.get( + 'plan_outputs', + { + '_logger': { + 'class': 'octodns.provider.plan.PlanLogger', + 'level': 'info', + } + }, + ) + self.plan_outputs = self._config_plan_outputs(plan_outputs_config) + + def _config_executor(self, manager_config, max_workers=None): max_workers = ( manager_config.get('max_workers', 1) if max_workers is None else max_workers ) - self.log.info('__init__: max_workers=%d', max_workers) + self.log.info('_config_executor: max_workers=%d', max_workers) if max_workers > 1: - self._executor = ThreadPoolExecutor(max_workers=max_workers) - else: - self._executor = MainThreadExecutor() - - self.include_meta = include_meta or manager_config.get( - 'include_meta', False - ) - self.log.info('__init__: include_meta=%s', self.include_meta) - - self.log.debug('__init__: configuring providers') - self.providers = {} - for provider_name, provider_config in self.config['providers'].items(): + return ThreadPoolExecutor(max_workers=max_workers) + return MainThreadExecutor() + + def _config_include_meta(self, manager_config, include_meta=False): + include_meta = include_meta or manager_config.get('include_meta', False) + self.log.info('_config_include_meta: include_meta=%s', include_meta) + return include_meta + + def _config_providers(self, providers_config): + self.log.debug('_config_providers: configuring providers') + providers = {} + for provider_name, provider_config in providers_config.items(): # Get our class and remove it from the provider_config try: _class = provider_config.pop('class') @@ -149,7 +175,7 @@ class Manager(object): _class, module, version = self._get_named_class('provider', _class) kwargs = self._build_kwargs(provider_config) try: - self.providers[provider_name] = _class(provider_name, **kwargs) + providers[provider_name] = _class(provider_name, **kwargs) self.log.info( '__init__: provider=%s (%s %s)', provider_name, @@ -162,10 +188,11 @@ class Manager(object): 'Incorrect provider config for ' + provider_name ) - self.processors = {} - for processor_name, processor_config in self.config.get( - 'processors', {} - ).items(): + return providers + + def _config_processors(self, processors_config): + processors = {} + for processor_name, processor_config in processors_config.items(): try: _class = processor_config.pop('class') except KeyError: @@ -176,9 +203,7 @@ class Manager(object): _class, module, version = self._get_named_class('processor', _class) kwargs = self._build_kwargs(processor_config) try: - self.processors[processor_name] = _class( - processor_name, **kwargs - ) + processors[processor_name] = _class(processor_name, **kwargs) self.log.info( '__init__: processor=%s (%s %s)', processor_name, @@ -190,18 +215,11 @@ class Manager(object): raise ManagerException( 'Incorrect processor config for ' + processor_name ) + return processors - self.plan_outputs = {} - plan_outputs = manager_config.get( - 'plan_outputs', - { - '_logger': { - 'class': 'octodns.provider.plan.PlanLogger', - 'level': 'info', - } - }, - ) - for plan_output_name, plan_output_config in plan_outputs.items(): + def _config_plan_outputs(self, plan_outputs_config): + plan_outputs = {} + for plan_output_name, plan_output_config in plan_outputs_config.items(): try: _class = plan_output_config.pop('class') except KeyError: @@ -214,7 +232,7 @@ class Manager(object): ) kwargs = self._build_kwargs(plan_output_config) try: - self.plan_outputs[plan_output_name] = _class( + plan_outputs[plan_output_name] = _class( plan_output_name, **kwargs ) # Don't print out version info for the default output @@ -230,8 +248,7 @@ class Manager(object): raise ManagerException( 'Incorrect plan_output config for ' + plan_output_name ) - - self._configured_sub_zones = None + return plan_outputs def _try_version(self, module_name, module=None, version=None): try: From a3ceb1f409bb23b75788e74b0c0b3d8a59e6f481 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 17 Aug 2022 18:40:10 -0700 Subject: [PATCH 04/32] Extract Manager zones configuration, add checks for matching utf-u and idna zone names --- octodns/manager.py | 22 +++++++++++++++++++-- tests/test_octodns_manager.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index 32a2802..8b925a1 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -118,8 +118,8 @@ class Manager(object): with open(config_file, 'r') as fh: self.config = safe_load(fh, enforce_order=False) - # convert the zones portion of things into an IdnaDict - self.config['zones'] = IdnaDict(self.config['zones']) + zones = self.config['zones'] + self.config['zones'] = self._config_zones(zones) manager_config = self.config.get('manager', {}) self._executor = self._config_executor(manager_config, max_workers) @@ -144,6 +144,24 @@ class Manager(object): ) self.plan_outputs = self._config_plan_outputs(plan_outputs_config) + def _config_zones(self, zones): + # record the set of configured zones we have as they are + configured_zones = set([z.lower() for z in zones.keys()]) + # walk the configured zones + for name in configured_zones: + if 'xn--' not in name: + continue + # this is an IDNA format zone name + decoded = idna_decode(name) + # do we also have a config for its utf-8 + if decoded in configured_zones: + raise ManagerException( + f'"{decoded}" configured both in utf-8 and idna "{name}"' + ) + + # convert the zones portion of things into an IdnaDict + return IdnaDict(zones) + def _config_executor(self, manager_config, max_workers=None): max_workers = ( manager_config.get('max_workers', 1) diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index bdacb08..c04d724 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -13,6 +13,7 @@ from os import environ from os.path import dirname, isfile, join from octodns import __VERSION__ +from octodns.idna import IdnaDict, idna_encode from octodns.manager import ( _AggregateTarget, MainThreadExecutor, @@ -831,6 +832,41 @@ class TestManager(TestCase): set(), manager.configured_sub_zones('bar.foo.unit.tests.') ) + def test_config_zones(self): + manager = Manager(get_config_filename('simple.yaml')) + + # empty == empty + self.assertEqual({}, manager._config_zones({})) + + # single ascii comes back as-is, but in a IdnaDict + zones = manager._config_zones({'unit.tests.': 42}) + self.assertEqual({'unit.tests.': 42}, zones) + self.assertIsInstance(zones, IdnaDict) + + # single utf-8 comes back idna encoded + self.assertEqual( + {idna_encode('Déjà.vu.'): 42}, + dict(manager._config_zones({'Déjà.vu.': 42})), + ) + + # ascii and non-matching idna as ok + self.assertEqual( + {idna_encode('déjà.vu.'): 42, 'deja.vu.': 43}, + dict( + manager._config_zones( + {idna_encode('déjà.vu.'): 42, 'deja.vu.': 43} + ) + ), + ) + + with self.assertRaises(ManagerException) as ctx: + # zone configured with both utf-8 and idna is an error + manager._config_zones({'Déjà.vu.': 42, idna_encode('Déjà.vu.'): 43}) + self.assertEqual( + '"déjà.vu." configured both in utf-8 and idna "xn--dj-kia8a.vu."', + str(ctx.exception), + ) + class TestMainThreadExecutor(TestCase): def test_success(self): From f00474cca4f157b3b1a086db56cd38fe7c4b7067 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 17 Aug 2022 18:51:00 -0700 Subject: [PATCH 05/32] Record.name/decoded_name pattern implemented --- octodns/record/__init__.py | 30 +++++++++++++++++++++++------- octodns/zone.py | 3 ++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index a7db144..506171b 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -16,6 +16,7 @@ import re from fqdn import FQDN from ..equality import EqualityTupleMixin +from ..idna import idna_decode, idna_encode from .geo import GeoCodes @@ -105,7 +106,11 @@ class Record(EqualityTupleMixin): @classmethod def new(cls, zone, name, data, source=None, lenient=False): name = str(name).lower() - fqdn = f'{name}.{zone.name}' if name else zone.name + fqdn = ( + f'{idna_decode(name)}.{zone.decoded_name}' + if name + else zone.decoded_name + ) try: _type = data['type'] except KeyError: @@ -169,8 +174,13 @@ class Record(EqualityTupleMixin): name, ) self.zone = zone - # force everything lower-case just to be safe - self.name = str(name).lower() if name else name + if name: + # internally everything is idna + self.name = idna_encode(str(name)) + # we'll keep a decoded version around for logs and errors + self.decoded_name = idna_decode(self.name) + else: + self.name = self.decoded_name = name self.source = source self.ttl = int(data['ttl']) @@ -189,6 +199,12 @@ class Record(EqualityTupleMixin): return f'{self.name}.{self.zone.name}' return self.zone.name + @property + def decoded_fqdn(self): + if self.decoded_name: + return f'{self.decoded_name}.{self.zone.decoded_name}' + return self.zone.decoded_name + @property def ignored(self): return self._octodns.get('ignored', False) @@ -350,7 +366,7 @@ class ValuesMixin(object): def __repr__(self): values = "', '".join([str(v) for v in self.values]) klass = self.__class__.__name__ - return f"<{klass} {self._type} {self.ttl}, {self.fqdn}, ['{values}']>" + return f"<{klass} {self._type} {self.ttl}, {self.decoded_fqdn}, ['{values}']>" class _GeoMixin(ValuesMixin): @@ -400,7 +416,7 @@ class _GeoMixin(ValuesMixin): if self.geo: klass = self.__class__.__name__ return ( - f'<{klass} {self._type} {self.ttl}, {self.fqdn}, ' + f'<{klass} {self._type} {self.ttl}, {self.decoded_fqdn}, ' f'{self.values}, {self.geo}>' ) return super(_GeoMixin, self).__repr__() @@ -432,7 +448,7 @@ class ValueMixin(object): def __repr__(self): klass = self.__class__.__name__ - return f'<{klass} {self._type} {self.ttl}, {self.fqdn}, {self.value}>' + return f'<{klass} {self._type} {self.ttl}, {self.decoded_fqdn}, {self.value}>' class _DynamicPool(object): @@ -760,7 +776,7 @@ class _DynamicMixin(object): klass = self.__class__.__name__ return ( - f'<{klass} {self._type} {self.ttl}, {self.fqdn}, ' + f'<{klass} {self._type} {self.ttl}, {self.decoded_fqdn}, ' f'{values}, {self.dynamic}>' ) return super(_DynamicMixin, self).__repr__() diff --git a/octodns/zone.py b/octodns/zone.py index 999a656..d276d19 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -35,8 +35,9 @@ class Zone(object): def __init__(self, name, sub_zones): if not name[-1] == '.': raise Exception(f'Invalid zone name {name}, missing ending dot') - # Force everything to lowercase just to be safe + # internally everything is idna self.name = str(name).lower() if name else name + # we'll keep a decoded version around for logs and errors self.decoded_name = idna_decode(self.name) self.sub_zones = sub_zones # We're grouping by node, it allows us to efficiently search for From 286e2bc94da6c7b9f91b2737f695bac3bfc8d31e Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 07:42:08 -0700 Subject: [PATCH 06/32] Use dict.__repr__ for IdnaDict --- octodns/idna.py | 3 +++ tests/test_octodns_idna.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/octodns/idna.py b/octodns/idna.py index bf30343..42c5fb7 100644 --- a/octodns/idna.py +++ b/octodns/idna.py @@ -69,3 +69,6 @@ class IdnaDict(MutableMapping): def decoded_items(self): for key, value in self.items(): yield (idna_decode(key), value) + + def __repr__(self): + return self._data.__repr__() diff --git a/tests/test_octodns_idna.py b/tests/test_octodns_idna.py index 2e09401..399d1f6 100644 --- a/tests/test_octodns_idna.py +++ b/tests/test_octodns_idna.py @@ -108,6 +108,9 @@ class TestIdnaDict(TestCase): self.assertFalse(self.utf8 in d) self.assertFalse(idna_encode(self.utf8) in d) + # smoke test of repr + d.__repr__() + def test_keys(self): d = IdnaDict(self.normal) From 56faf78c72d11acae3b7e1bd424c15bba300f673 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 07:42:34 -0700 Subject: [PATCH 07/32] Support eligible_zones with idna --- octodns/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index 8b925a1..af559cd 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -17,7 +17,7 @@ from sys import stdout import logging from . import __VERSION__ -from .idna import IdnaDict, idna_decode +from .idna import IdnaDict, idna_decode, idna_encode from .provider.base import BaseProvider from .provider.plan import Plan from .provider.yaml import SplitYamlProvider, YamlProvider @@ -464,13 +464,13 @@ class Manager(object): getattr(plan_output_fh, 'name', plan_output_fh.__class__.__name__), ) - zones = self.config['zones'].items() + zones = self.config['zones'] if eligible_zones: - zones = [z for z in zones if z[0] in eligible_zones] + zones = {idna_encode(n): zones.get(n) for n in eligible_zones} aliased_zones = {} futures = [] - for zone_name, config in zones: + for zone_name, config in zones.items(): self.log.info('sync: zone=%s', idna_decode(zone_name)) if 'alias' in config: source_zone = config['alias'] From 497336e6abd94e023cae3656b0d517c07b50d70b Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 08:10:21 -0700 Subject: [PATCH 08/32] Use IdnaDict for configured_sub_zones, fix get_zone, comments --- octodns/manager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index af559cd..fc50f17 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -341,7 +341,7 @@ class Manager(object): if self._configured_sub_zones is None: # First time through we compute all the sub-zones - configured_sub_zones = {} + configured_sub_zones = IdnaDict() # Get a list of all of our zone names. Sort them from shortest to # longest so that parents will always come before their subzones @@ -758,6 +758,7 @@ class Manager(object): target.apply(plan) def validate_configs(self): + # TODO: this code can probably be shared with stuff in sync for zone_name, config in self.config['zones'].items(): zone = Zone(zone_name, self.configured_sub_zones(zone_name)) @@ -824,8 +825,10 @@ class Manager(object): f'Invalid zone name {zone_name}, missing ' 'ending dot' ) - for name, config in self.config['zones'].items(): - if name == zone_name: - return Zone(name, self.configured_sub_zones(name)) + zone = self.config['zones'].get(zone_name) + if zone: + return Zone( + idna_decode(zone_name), self.configured_sub_zones(zone_name) + ) raise ManagerException(f'Unknown zone name {zone_name}') From 0b0632717d4bd970a4a7b1abc20e3cacbcfbf02b Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 08:11:18 -0700 Subject: [PATCH 09/32] Use decoded_* for human readable stuff --- octodns/cmds/report.py | 2 +- octodns/record/__init__.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/octodns/cmds/report.py b/octodns/cmds/report.py index d13e85c..03a8f11 100755 --- a/octodns/cmds/report.py +++ b/octodns/cmds/report.py @@ -93,7 +93,7 @@ def main(): ] for record, futures in sorted(queries.items(), key=lambda d: d[0]): - stdout.write(record.fqdn) + stdout.write(record.decoded_fqdn) stdout.write(',') stdout.write(record._type) stdout.write(',') diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 506171b..7d6f8e8 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -167,12 +167,6 @@ class Record(EqualityTupleMixin): return reasons def __init__(self, zone, name, data, source=None): - self.log.debug( - '__init__: zone.name=%s, type=%11s, name=%s', - zone.decoded_name, - self.__class__.__name__, - name, - ) self.zone = zone if name: # internally everything is idna @@ -181,6 +175,12 @@ class Record(EqualityTupleMixin): self.decoded_name = idna_decode(self.name) else: self.name = self.decoded_name = name + self.log.debug( + '__init__: zone.name=%s, type=%11s, name=%s', + zone.decoded_name, + self.__class__.__name__, + self.decoded_name, + ) self.source = source self.ttl = int(data['ttl']) From a33235d6085a3b7ef26f50c457cec2d5cebd4b7e Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 08:26:57 -0700 Subject: [PATCH 10/32] YamlProvider idna/utf-8 support, prefers utf8 --- octodns/provider/yaml.py | 23 ++++++++++++------ tests/test_octodns_provider_yaml.py | 36 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index 93b9b12..a284be4 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -17,6 +17,7 @@ import logging from ..record import Record from ..yaml import safe_load, safe_dump from .base import BaseProvider +from . import ProviderException class YamlProvider(BaseProvider): @@ -207,12 +208,20 @@ class YamlProvider(BaseProvider): return False before = len(zone.records) - filename = join(self.directory, f'{zone.decoded_name}yaml') - if not isfile(filename): - idna_filename = join(self.directory, f'{zone.name}yaml') + utf8_filename = join(self.directory, f'{zone.decoded_name}yaml') + idna_filename = join(self.directory, f'{zone.name}yaml') + + # we prefer utf8 + if isfile(utf8_filename): + if utf8_filename != idna_filename and isfile(idna_filename): + raise ProviderException( + f'Both UTF-8 "{utf8_filename}" and IDNA "{idna_filename}" exist for {zone.decoded_name}' + ) + filename = utf8_filename + else: self.log.warning( - 'populate: "%s" does not exist, falling back to idna version "%s"', - filename, + 'populate: "%s" does not exist, falling back to try idna version "%s"', + utf8_filename, idna_filename, ) filename = idna_filename @@ -245,7 +254,7 @@ class YamlProvider(BaseProvider): del d['ttl'] if record._octodns: d['octodns'] = record._octodns - data[record.name].append(d) + data[record.decoded_name].append(d) # Flatten single element lists for k in data.keys(): @@ -261,7 +270,7 @@ class YamlProvider(BaseProvider): filename = join(self.directory, f'{desired.decoded_name}yaml') self.log.debug('_apply: writing filename=%s', filename) with open(filename, 'w') as fh: - safe_dump(dict(data), fh) + safe_dump(dict(data), fh, allow_unicode=True) def _list_all_yaml_files(directory): diff --git a/tests/test_octodns_provider_yaml.py b/tests/test_octodns_provider_yaml.py index e2f55c2..d0a6358 100644 --- a/tests/test_octodns_provider_yaml.py +++ b/tests/test_octodns_provider_yaml.py @@ -15,7 +15,9 @@ from unittest import TestCase from yaml import safe_load from yaml.constructor import ConstructorError +from octodns.idna import idna_encode from octodns.record import Create +from octodns.provider import ProviderException from octodns.provider.base import Plan from octodns.provider.yaml import ( _list_all_yaml_files, @@ -172,6 +174,40 @@ class TestYamlProvider(TestCase): # make sure nothing is left self.assertEqual([], list(data.keys())) + def test_idna_filenames(self): + with TemporaryDirectory() as td: + name = 'déjà.vu.' + filename = f'{name}yaml' + + provider = YamlProvider('test', td.dirname) + zone = Zone(idna_encode(name), []) + + # create a idna named file + with open(join(td.dirname, idna_encode(filename)), 'w') as fh: + pass + + fh.write( + '''--- +'': + type: A + value: 1.2.3.4 +''' + ) + + # populates fine when there's just the idna version (as a fallback) + provider.populate(zone) + self.assertEqual(1, len(zone.records)) + + # create a utf8 named file + with open(join(td.dirname, filename), 'w') as fh: + pass + + # does not allow both idna and utf8 named files + with self.assertRaises(ProviderException) as ctx: + provider.populate(zone) + msg = str(ctx.exception) + self.assertTrue('Both UTF-8' in msg) + def test_empty(self): source = YamlProvider( 'test', join(dirname(__file__), 'config'), supports_root_ns=False From 33284381f894ef89d293c4b04f30e86a7d5d5329 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 12:32:20 -0700 Subject: [PATCH 11/32] Use IdnaDict for eligible zone filtering --- octodns/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index fc50f17..79d201d 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -17,7 +17,7 @@ from sys import stdout import logging from . import __VERSION__ -from .idna import IdnaDict, idna_decode, idna_encode +from .idna import IdnaDict, idna_decode from .provider.base import BaseProvider from .provider.plan import Plan from .provider.yaml import SplitYamlProvider, YamlProvider @@ -466,7 +466,7 @@ class Manager(object): zones = self.config['zones'] if eligible_zones: - zones = {idna_encode(n): zones.get(n) for n in eligible_zones} + zones = IdnaDict({n: zones.get(n) for n in eligible_zones}) aliased_zones = {} futures = [] From 2d0f2ccc5c44999f23f92d9fc44f0ac7c27b7b7f Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 20:10:11 -0700 Subject: [PATCH 12/32] Add idna tests of eligible_zones and fix some messages/bugs --- octodns/manager.py | 44 +++++++++++++++------------------ octodns/provider/yaml.py | 3 ++- tests/test_octodns_manager.py | 46 ++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index 79d201d..cca1cd7 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -471,33 +471,22 @@ class Manager(object): aliased_zones = {} futures = [] for zone_name, config in zones.items(): - self.log.info('sync: zone=%s', idna_decode(zone_name)) + decoded_zone_name = idna_decode(zone_name) + self.log.info('sync: zone=%s', decoded_zone_name) if 'alias' in config: source_zone = config['alias'] # Check that the source zone is defined. if source_zone not in self.config['zones']: - self.log.error( - f'Invalid alias zone {zone_name}, ' - f'target {source_zone} does not exist' - ) - raise ManagerException( - f'Invalid alias zone {zone_name}: ' - f'source zone {source_zone} does ' - 'not exist' - ) + msg = f'Invalid alias zone {decoded_zone_name}: source zone {idna_decode(source_zone)} does not exist' + self.log.error(msg) + raise ManagerException(msg) # Check that the source zone is not an alias zone itself. if 'alias' in self.config['zones'][source_zone]: - self.log.error( - f'Invalid alias zone {zone_name}, ' - f'target {source_zone} is an alias zone' - ) - raise ManagerException( - f'Invalid alias zone {zone_name}: ' - f'source zone {source_zone} is an ' - 'alias zone' - ) + msg = f'Invalid alias zone {decoded_zone_name}: source zone {idna_decode(source_zone)} is an alias zone' + self.log.error(msg) + raise ManagerException(msg) aliased_zones[zone_name] = source_zone continue @@ -506,12 +495,16 @@ class Manager(object): try: sources = config['sources'] except KeyError: - raise ManagerException(f'Zone {zone_name} is missing sources') + raise ManagerException( + f'Zone {decoded_zone_name} is missing sources' + ) try: targets = config['targets'] except KeyError: - raise ManagerException(f'Zone {zone_name} is missing targets') + raise ManagerException( + f'Zone {decoded_zone_name} is missing targets' + ) processors = config.get('processors', []) @@ -540,7 +533,8 @@ class Manager(object): processors = collected except KeyError: raise ManagerException( - f'Zone {zone_name}, unknown ' f'processor: {processor}' + f'Zone {decoded_zone_name}, unknown ' + f'processor: {processor}' ) try: @@ -553,7 +547,7 @@ class Manager(object): sources = collected except KeyError: raise ManagerException( - f'Zone {zone_name}, unknown ' f'source: {source}' + f'Zone {decoded_zone_name}, unknown ' f'source: {source}' ) try: @@ -568,7 +562,7 @@ class Manager(object): targets = trgs except KeyError: raise ManagerException( - f'Zone {zone_name}, unknown ' f'target: {target}' + f'Zone {decoded_zone_name}, unknown ' f'target: {target}' ) futures.append( @@ -600,7 +594,7 @@ class Manager(object): desired_config = desired[zone_source] except KeyError: raise ManagerException( - f'Zone {zone_name} cannot be sync ' + f'Zone {idna_decode(zone_name)} cannot be synced ' f'without zone {zone_source} sinced ' 'it is aliased' ) diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index a284be4..fb58e33 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -148,7 +148,7 @@ class YamlProvider(BaseProvider): self.log = logging.getLogger(f'{klass}[{id}]') self.log.debug( '__init__: id=%s, directory=%s, default_ttl=%d, ' - 'nforce_order=%d, populate_should_replace=%d', + 'enforce_order=%d, populate_should_replace=%d', id, directory, default_ttl, @@ -254,6 +254,7 @@ class YamlProvider(BaseProvider): del d['ttl'] if record._octodns: d['octodns'] = record._octodns + # we want to output the utf-8 version of the name data[record.decoded_name].append(d) # Flatten single element lists diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index c04d724..2a74257 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -183,6 +183,50 @@ class TestManager(TestCase): ).sync(dry_run=False, force=True) self.assertEqual(33, tc) + def test_idna_eligible_zones(self): + # loading w/simple, but we'll be blowing it away and doing some manual + # stuff + manager = Manager(get_config_filename('simple.yaml')) + + # these configs won't be valid, but that's fine we can test what we're + # after based on exceptions raised + manager.config['zones'] = manager._config_zones( + {'déjà.vu.': {}, 'deja.vu.': {}, idna_encode('こんにちは.jp.'): {}} + ) + from pprint import pprint + + pprint(manager.config['zones']) + + # refer to them with utf-8 + with self.assertRaises(ManagerException) as ctx: + manager.sync(eligible_zones=('déjà.vu.',)) + self.assertEqual('Zone déjà.vu. is missing sources', str(ctx.exception)) + + with self.assertRaises(ManagerException) as ctx: + manager.sync(eligible_zones=('deja.vu.',)) + self.assertEqual('Zone deja.vu. is missing sources', str(ctx.exception)) + + with self.assertRaises(ManagerException) as ctx: + manager.sync(eligible_zones=('こんにちは.jp.',)) + self.assertEqual( + 'Zone こんにちは.jp. is missing sources', str(ctx.exception) + ) + + # refer to them with idna (exceptions are still utf-8 + with self.assertRaises(ManagerException) as ctx: + manager.sync(eligible_zones=(idna_encode('déjà.vu.'),)) + self.assertEqual('Zone déjà.vu. is missing sources', str(ctx.exception)) + + with self.assertRaises(ManagerException) as ctx: + manager.sync(eligible_zones=(idna_encode('deja.vu.'),)) + self.assertEqual('Zone deja.vu. is missing sources', str(ctx.exception)) + + with self.assertRaises(ManagerException) as ctx: + manager.sync(eligible_zones=(idna_encode('こんにちは.jp.'),)) + self.assertEqual( + 'Zone こんにちは.jp. is missing sources', str(ctx.exception) + ) + def test_eligible_sources(self): with TemporaryDirectory() as tmpdir: environ['YAML_TMP_DIR'] = tmpdir.dirname @@ -237,7 +281,7 @@ class TestManager(TestCase): get_config_filename('simple-alias-zone.yaml') ).sync(eligible_zones=["alias.tests."]) self.assertEqual( - 'Zone alias.tests. cannot be sync without zone ' + 'Zone alias.tests. cannot be synced without zone ' 'unit.tests. sinced it is aliased', str(ctx.exception), ) From 94317879b44e2cd2da8dd2e8de24c081203cf2ef Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 18 Aug 2022 20:13:36 -0700 Subject: [PATCH 13/32] few more utf-8 prints/exceptions --- octodns/manager.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index cca1cd7..d9e0180 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -17,7 +17,7 @@ from sys import stdout import logging from . import __VERSION__ -from .idna import IdnaDict, idna_decode +from .idna import IdnaDict, idna_decode, idna_encode from .provider.base import BaseProvider from .provider.plan import Plan from .provider.yaml import SplitYamlProvider, YamlProvider @@ -754,6 +754,7 @@ class Manager(object): def validate_configs(self): # TODO: this code can probably be shared with stuff in sync for zone_name, config in self.config['zones'].items(): + decoded_zone_name = idna_decode(zone_name) zone = Zone(zone_name, self.configured_sub_zones(zone_name)) source_zone = config.get('alias') @@ -761,7 +762,7 @@ class Manager(object): if source_zone not in self.config['zones']: self.log.exception('Invalid alias zone') raise ManagerException( - f'Invalid alias zone {zone_name}: ' + f'Invalid alias zone {decoded_zone_name}: ' f'source zone {source_zone} does ' 'not exist' ) @@ -769,7 +770,7 @@ class Manager(object): if 'alias' in self.config['zones'][source_zone]: self.log.exception('Invalid alias zone') raise ManagerException( - f'Invalid alias zone {zone_name}: ' + f'Invalid alias zone {decoded_zone_name}: ' 'source zone {source_zone} is an ' 'alias zone' ) @@ -783,7 +784,9 @@ class Manager(object): try: sources = config['sources'] except KeyError: - raise ManagerException(f'Zone {zone_name} is missing sources') + raise ManagerException( + f'Zone {decoded_zone_name} is missing sources' + ) try: # rather than using a list comprehension, we break this @@ -795,7 +798,7 @@ class Manager(object): sources = collected except KeyError: raise ManagerException( - f'Zone {zone_name}, unknown source: ' + source + f'Zone {decoded_zone_name}, unknown source: ' + source ) for source in sources: @@ -810,19 +813,21 @@ class Manager(object): collected.append(self.processors[processor]) except KeyError: raise ManagerException( - f'Zone {zone_name}, unknown ' f'processor: {processor}' + f'Zone {decoded_zone_name}, unknown ' + f'processor: {processor}' ) def get_zone(self, zone_name): if not zone_name[-1] == '.': raise ManagerException( - f'Invalid zone name {zone_name}, missing ' 'ending dot' + f'Invalid zone name {idna_decode(zone_name)}, missing ' + 'ending dot' ) zone = self.config['zones'].get(zone_name) if zone: return Zone( - idna_decode(zone_name), self.configured_sub_zones(zone_name) + idna_encode(zone_name), self.configured_sub_zones(zone_name) ) - raise ManagerException(f'Unknown zone name {zone_name}') + raise ManagerException(f'Unknown zone name {idna_decode(zone_name)}') From 799e1232b3939b1204c85d7bc86e68a10797c4da Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 20 Aug 2022 08:11:24 -0700 Subject: [PATCH 14/32] Record should work with encoded same as everything else --- octodns/record/__init__.py | 14 +++++--------- tests/test_octodns_idna.py | 8 ++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 7d6f8e8..36fef2d 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -78,7 +78,7 @@ class ValidationError(RecordException): @classmethod def build_message(cls, fqdn, reasons): reasons = '\n - '.join(reasons) - return f'Invalid record {fqdn}\n - {reasons}' + return f'Invalid record {idna_decode(fqdn)}\n - {reasons}' def __init__(self, fqdn, reasons): super(Exception, self).__init__(self.build_message(fqdn, reasons)) @@ -105,16 +105,12 @@ class Record(EqualityTupleMixin): @classmethod def new(cls, zone, name, data, source=None, lenient=False): - name = str(name).lower() - fqdn = ( - f'{idna_decode(name)}.{zone.decoded_name}' - if name - else zone.decoded_name - ) + name = idna_encode(str(name)) + fqdn = f'{name}.{zone.name}' if name else zone.name try: _type = data['type'] except KeyError: - raise Exception(f'Invalid record {fqdn}, missing type') + raise Exception(f'Invalid record {idna_decode(fqdn)}, missing type') try: _class = cls._CLASSES[_type] except KeyError: @@ -139,7 +135,7 @@ class Record(EqualityTupleMixin): n = len(fqdn) if n > 253: reasons.append( - f'invalid fqdn, "{fqdn}" is too long at {n} ' + f'invalid fqdn, "{idna_decode(fqdn)}" is too long at {n} ' 'chars, max is 253' ) for label in name.split('.'): diff --git a/tests/test_octodns_idna.py b/tests/test_octodns_idna.py index 399d1f6..3742439 100644 --- a/tests/test_octodns_idna.py +++ b/tests/test_octodns_idna.py @@ -59,6 +59,14 @@ class TestIdna(TestCase): self.assertEqual('zajęzyk.pl.', idna_decode('XN--ZAJZYK-Y4A.PL.')) self.assertEqual('xn--zajzyk-y4a.pl.', idna_encode('ZajęzyK.Pl.')) + def test_repeated_encode_decoded(self): + self.assertEqual( + 'zajęzyk.pl.', idna_decode(idna_decode('xn--zajzyk-y4a.pl.')) + ) + self.assertEqual( + 'xn--zajzyk-y4a.pl.', idna_encode(idna_encode('zajęzyk.pl.')) + ) + class TestIdnaDict(TestCase): plain = 'testing.tests.' From 55b970183734f8c4004a862b0c3ed15d8ff83a3c Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 20 Aug 2022 09:40:54 -0700 Subject: [PATCH 15/32] IdnaError exception handling, ensure validation happens on encoded names --- octodns/idna.py | 33 +++++++++++++++++--------- octodns/record/__init__.py | 15 +++++++++--- tests/test_octodns_idna.py | 11 ++++++++- tests/test_octodns_record.py | 45 ++++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 15 deletions(-) diff --git a/octodns/idna.py b/octodns/idna.py index 42c5fb7..9a079ee 100644 --- a/octodns/idna.py +++ b/octodns/idna.py @@ -4,13 +4,18 @@ from collections.abc import MutableMapping -from idna import decode as _decode, encode as _encode +from idna import IDNAError as _IDNAError, decode as _decode, encode as _encode # Providers will need to to make calls to these at the appropriate points, # generally right before they pass names off to api calls. For an example of # usage see https://github.com/octodns/octodns-ns1/pull/20 +class IdnaError(Exception): + def __init__(self, idna_error): + super().__init__(str(idna_error)) + + def idna_encode(name): # Based on https://github.com/psf/requests/pull/3695/files # #diff-0debbb2447ce5debf2872cb0e17b18babe3566e9d9900739e8581b355bd513f7R39 @@ -20,21 +25,27 @@ def idna_encode(name): # No utf8 chars, just use as-is return name except UnicodeEncodeError: - if name.startswith('*'): - # idna.encode doesn't like the * - name = _encode(name[2:]).decode('utf-8') - return f'*.{name}' - return _encode(name).decode('utf-8') + try: + if name.startswith('*'): + # idna.encode doesn't like the * + name = _encode(name[2:]).decode('utf-8') + return f'*.{name}' + return _encode(name).decode('utf-8') + except _IDNAError as e: + raise IdnaError(e) def idna_decode(name): pieces = name.lower().split('.') if any(p.startswith('xn--') for p in pieces): - # it's idna - if name.startswith('*'): - # idna.decode doesn't like the * - return f'*.{_decode(name[2:])}' - return _decode(name) + try: + # it's idna + if name.startswith('*'): + # idna.decode doesn't like the * + return f'*.{_decode(name[2:])}' + return _decode(name) + except _IDNAError as e: + raise IdnaError(e) # not idna, just return as-is return name diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 36fef2d..28f2451 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -16,7 +16,7 @@ import re from fqdn import FQDN from ..equality import EqualityTupleMixin -from ..idna import idna_decode, idna_encode +from ..idna import IdnaError, idna_decode, idna_encode from .geo import GeoCodes @@ -105,7 +105,13 @@ class Record(EqualityTupleMixin): @classmethod def new(cls, zone, name, data, source=None, lenient=False): - name = idna_encode(str(name)) + reasons = [] + try: + name = idna_encode(str(name)) + except IdnaError as e: + # convert the error into a reason + reasons.append(str(e)) + name = str(name) fqdn = f'{name}.{zone.name}' if name else zone.name try: _type = data['type'] @@ -115,7 +121,7 @@ class Record(EqualityTupleMixin): _class = cls._CLASSES[_type] except KeyError: raise Exception(f'Unknown record type: "{_type}"') - reasons = _class.validate(name, fqdn, data) + reasons.extend(_class.validate(name, fqdn, data)) try: lenient |= data['octodns']['lenient'] except KeyError: @@ -145,6 +151,7 @@ class Record(EqualityTupleMixin): f'invalid label, "{label}" is too long at {n}' ' chars, max is 63' ) + # TODO: look at the idna lib for a lot more potential validations... try: ttl = int(data['ttl']) if ttl < 0: @@ -191,6 +198,8 @@ class Record(EqualityTupleMixin): @property def fqdn(self): + # TODO: these should be calculated and set in __init__ rather than on + # each use if self.name: return f'{self.name}.{self.zone.name}' return self.zone.name diff --git a/tests/test_octodns_idna.py b/tests/test_octodns_idna.py index 3742439..7e1eb68 100644 --- a/tests/test_octodns_idna.py +++ b/tests/test_octodns_idna.py @@ -11,7 +11,7 @@ from __future__ import ( from unittest import TestCase -from octodns.idna import IdnaDict, idna_decode, idna_encode +from octodns.idna import IdnaDict, IdnaError, idna_decode, idna_encode class TestIdna(TestCase): @@ -67,6 +67,15 @@ class TestIdna(TestCase): 'xn--zajzyk-y4a.pl.', idna_encode(idna_encode('zajęzyk.pl.')) ) + def test_exception_translation(self): + with self.assertRaises(IdnaError) as ctx: + idna_encode('déjà..vu.') + self.assertEqual('Empty Label', str(ctx.exception)) + + with self.assertRaises(IdnaError) as ctx: + idna_decode('xn--djvu-1na6c..com.') + self.assertEqual('Empty Label', str(ctx.exception)) + class TestIdnaDict(TestCase): plain = 'testing.tests.' diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 2bbea5d..7ad2f82 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -1889,6 +1889,51 @@ class TestRecordValidation(TestCase): self.zone, name, {'ttl': 300, 'type': 'A', 'value': '1.2.3.4'} ) + # make sure we're validating with encoded fqdns + utf8 = 'déjà-vu' + padding = ('.' + ('x' * 57)) * 4 + utf8_name = f'{utf8}{padding}' + # make sure our test is valid here, we're under 253 chars long as utf8 + self.assertEqual(251, len(f'{utf8_name}.{self.zone.name}')) + with self.assertRaises(ValidationError) as ctx: + Record.new( + self.zone, + utf8_name, + {'ttl': 300, 'type': 'A', 'value': '1.2.3.4'}, + ) + reason = ctx.exception.reasons[0] + self.assertTrue(reason.startswith('invalid fqdn, "déjà-vu')) + self.assertTrue( + reason.endswith( + '.unit.tests." is too long at 259' ' chars, max is 253' + ) + ) + + # same, but with ascii version of things + plain = 'deja-vu' + plain_name = f'{plain}{padding}' + self.assertEqual(251, len(f'{plain_name}.{self.zone.name}')) + Record.new( + self.zone, plain_name, {'ttl': 300, 'type': 'A', 'value': '1.2.3.4'} + ) + + # check that we're validating encoded labels + padding = 'x' * (60 - len(utf8)) + utf8_name = f'{utf8}{padding}' + # make sure the test is valid, we're at 63 chars + self.assertEqual(60, len(utf8_name)) + with self.assertRaises(ValidationError) as ctx: + Record.new( + self.zone, + utf8_name, + {'ttl': 300, 'type': 'A', 'value': '1.2.3.4'}, + ) + reason = ctx.exception.reasons[0] + # Unfortunately this is a translated IDNAError so we don't have much + # control over the exact message :-/ (doesn't give context like octoDNS + # does) + self.assertEqual('Label too long', reason) + # no ttl with self.assertRaises(ValidationError) as ctx: Record.new(self.zone, '', {'type': 'A', 'value': '1.2.3.4'}) From 27fc734c2abf67691ea404481fd399ca11e9fc60 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 20 Aug 2022 11:39:00 -0700 Subject: [PATCH 16/32] Test YamlProvider handling of non-ascii record names --- octodns/zone.py | 4 +++- tests/test_octodns_provider_yaml.py | 34 ++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/octodns/zone.py b/octodns/zone.py index d276d19..726373c 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -41,7 +41,9 @@ class Zone(object): self.decoded_name = idna_decode(self.name) self.sub_zones = sub_zones # We're grouping by node, it allows us to efficiently search for - # duplicates and detect when CNAMEs co-exist with other records + # duplicates and detect when CNAMEs co-exist with other records. Also + # node that we always store things with Record.name which will be idna + # encoded thus we don't have to deal with idna/utf8 collisions self._records = defaultdict(set) self._root_ns = None # optional leading . to match empty hostname diff --git a/tests/test_octodns_provider_yaml.py b/tests/test_octodns_provider_yaml.py index d0a6358..c0f07a2 100644 --- a/tests/test_octodns_provider_yaml.py +++ b/tests/test_octodns_provider_yaml.py @@ -174,7 +174,7 @@ class TestYamlProvider(TestCase): # make sure nothing is left self.assertEqual([], list(data.keys())) - def test_idna_filenames(self): + def test_idna(self): with TemporaryDirectory() as td: name = 'déjà.vu.' filename = f'{name}yaml' @@ -184,23 +184,41 @@ class TestYamlProvider(TestCase): # create a idna named file with open(join(td.dirname, idna_encode(filename)), 'w') as fh: - pass - fh.write( '''--- '': type: A value: 1.2.3.4 +# something in idna notation +xn--dj-kia8a: + type: A + value: 2.3.4.5 +# something with utf-8 +これはテストです: + type: A + value: 3.4.5.6 ''' ) # populates fine when there's just the idna version (as a fallback) provider.populate(zone) - self.assertEqual(1, len(zone.records)) - - # create a utf8 named file - with open(join(td.dirname, filename), 'w') as fh: - pass + d = {r.name: r for r in zone.records} + self.assertEqual(3, len(d)) + # verify that we loaded the expected records, including idna/utf-8 + # named ones + self.assertEqual(['1.2.3.4'], d[''].values) + self.assertEqual(['2.3.4.5'], d['xn--dj-kia8a'].values) + self.assertEqual(['3.4.5.6'], d['xn--28jm5b5a8k5k8cra'].values) + + # create a utf8 named file (provider always writes utf-8 filenames + plan = provider.plan(zone) + provider.apply(plan) + + with open(join(td.dirname, filename), 'r') as fh: + content = fh.read() + # verify that the non-ascii records were written out in utf-8 + self.assertTrue('déjà:' in content) + self.assertTrue('これはテストです:' in content) # does not allow both idna and utf8 named files with self.assertRaises(ProviderException) as ctx: From 16e0bd06751e2f0179be06fd1812faced4be662d Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 20 Aug 2022 11:55:19 -0700 Subject: [PATCH 17/32] Testing of Zone and Record name/decoded_name handling --- octodns/zone.py | 4 ++-- tests/test_octodns_record.py | 13 +++++++++++++ tests/test_octodns_zone.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/octodns/zone.py b/octodns/zone.py index 726373c..6ec54fb 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -13,7 +13,7 @@ from collections import defaultdict from logging import getLogger import re -from .idna import idna_decode +from .idna import idna_decode, idna_encode from .record import Create, Delete @@ -36,7 +36,7 @@ class Zone(object): if not name[-1] == '.': raise Exception(f'Invalid zone name {name}, missing ending dot') # internally everything is idna - self.name = str(name).lower() if name else name + self.name = idna_encode(str(name)) if name else name # we'll keep a decoded version around for logs and errors self.decoded_name = idna_decode(self.name) self.sub_zones = sub_zones diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 7ad2f82..75b4675 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -11,6 +11,7 @@ from __future__ import ( from unittest import TestCase +from octodns.idna import idna_encode from octodns.record import ( ARecord, AaaaRecord, @@ -83,6 +84,18 @@ class TestRecord(TestCase): ) self.assertEqual('mixedcase', record.name) + def test_utf8(self): + zone = Zone('natación.mx.', []) + utf8 = 'niño' + encoded = idna_encode(utf8) + record = ARecord( + zone, utf8, {'ttl': 30, 'type': 'A', 'value': '1.2.3.4'} + ) + self.assertEqual(encoded, record.name) + self.assertEqual(utf8, record.decoded_name) + self.assertTrue(f'{encoded}.{zone.name}', record.fqdn) + self.assertTrue(f'{utf8}.{zone.decoded_name}', record.decoded_fqdn) + def test_alias_lowering_value(self): upper_record = AliasRecord( self.zone, diff --git a/tests/test_octodns_zone.py b/tests/test_octodns_zone.py index 37e893f..5245735 100644 --- a/tests/test_octodns_zone.py +++ b/tests/test_octodns_zone.py @@ -11,6 +11,7 @@ from __future__ import ( from unittest import TestCase +from octodns.idna import idna_encode from octodns.record import ( ARecord, AaaaRecord, @@ -35,6 +36,13 @@ class TestZone(TestCase): zone = Zone('UniT.TEsTs.', []) self.assertEqual('unit.tests.', zone.name) + def test_utf8(self): + utf8 = 'grüßen.de.' + encoded = idna_encode(utf8) + zone = Zone(utf8, []) + self.assertEqual(encoded, zone.name) + self.assertEqual(utf8, zone.decoded_name) + def test_hostname_from_fqdn(self): zone = Zone('unit.tests.', []) for hostname, fqdn in ( @@ -46,6 +54,27 @@ class TestZone(TestCase): ('foo.bar', 'foo.bar.unit.tests'), ('foo.unit.tests', 'foo.unit.tests.unit.tests.'), ('foo.unit.tests', 'foo.unit.tests.unit.tests'), + ('déjà', 'déjà.unit.tests'), + ('déjà.foo', 'déjà.foo.unit.tests'), + ('bar.déjà', 'bar.déjà.unit.tests'), + ('bar.déjà.foo', 'bar.déjà.foo.unit.tests'), + ): + self.assertEqual(hostname, zone.hostname_from_fqdn(fqdn)) + + zone = Zone('grüßen.de.', []) + for hostname, fqdn in ( + ('', 'grüßen.de.'), + ('', 'grüßen.de'), + ('foo', 'foo.grüßen.de.'), + ('foo', 'foo.grüßen.de'), + ('foo.bar', 'foo.bar.grüßen.de.'), + ('foo.bar', 'foo.bar.grüßen.de'), + ('foo.grüßen.de', 'foo.grüßen.de.grüßen.de.'), + ('foo.grüßen.de', 'foo.grüßen.de.grüßen.de'), + ('déjà', 'déjà.grüßen.de'), + ('déjà.foo', 'déjà.foo.grüßen.de'), + ('bar.déjà', 'bar.déjà.grüßen.de'), + ('bar.déjà.foo', 'bar.déjà.foo.grüßen.de'), ): self.assertEqual(hostname, zone.hostname_from_fqdn(fqdn)) From 3bd79627c352c2e846fb0e2a5dd07f3ac3cd1090 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 27 Aug 2022 16:32:06 -0700 Subject: [PATCH 18/32] Doc Zone.configured_sub_zones --- octodns/manager.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/octodns/manager.py b/octodns/manager.py index d9e0180..7ee2057 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -338,6 +338,17 @@ class Manager(object): return kwargs def configured_sub_zones(self, zone_name): + ''' + Accepts either UTF-8 or IDNA encoded zone name and returns the list of + any configured sub-zones in IDNA form. E.g. for the following + configured zones: + some.com. + other.some.com. + deep.thing.some.com. + It would return + other + deep.thing + ''' if self._configured_sub_zones is None: # First time through we compute all the sub-zones From d9d3209ef6e23458fa86b4778c96ab600f799588 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 7 Sep 2022 13:51:40 -0700 Subject: [PATCH 19/32] Update octodns/record/__init__.py --- octodns/record/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 58169d5..287ebe2 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -1978,7 +1978,7 @@ class UrlfwdValue(EqualityTupleMixin, dict): @classmethod def process(cls, values): - return [UrlfwdValue(v) for v in values] + return [cls(v) for v in values] def __init__(self, value): super().__init__( From c4d9362664dc12ec25f66f87de275389f32ad1c5 Mon Sep 17 00:00:00 2001 From: Lukasz Polanski Date: Sat, 10 Sep 2022 23:23:54 +0200 Subject: [PATCH 20/32] Update README.md Adding links to the noteworthy NetBox-DNS plugin and its OctoDNS provider --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 666e4b9..788619f 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,7 @@ If you have a problem or suggestion, please [open an issue](https://github.com/o - [`doddo/octodns-lexicon`](https://github.com/doddo/octodns-lexicon): Use [Lexicon](https://github.com/AnalogJ/lexicon) providers as octoDNS providers. - [`asyncon/octoblox`](https://github.com/asyncon/octoblox): [Infoblox](https://www.infoblox.com/) provider. - [`sukiyaki/octodns-netbox`](https://github.com/sukiyaki/octodns-netbox): [NetBox](https://github.com/netbox-community/netbox) source. + - [`jcollie/octodns-netbox-dns`](https://github.com/jcollie/octodns-netbox-dns): [NetBox-DNS Plugin](https://github.com/auroraresearchlab/netbox-dns) provider. - [`kompetenzbolzen/octodns-custom-provider`](https://github.com/kompetenzbolzen/octodns-custom-provider): zonefile provider & phpIPAM source. - **Resources.** - Article: [Visualising DNS records with Neo4j](https://medium.com/@costask/querying-and-visualising-octodns-records-with-neo4j-f4f72ab2d474) + code From 47b72225dcee80e527878ead602c7c85519bf6df Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 11 Sep 2022 14:36:35 -0700 Subject: [PATCH 21/32] Processor impl that filters on name --- octodns/processor/filter.py | 121 ++++++++++++++++++++++++- tests/test_octodns_processor_filter.py | 87 +++++++++++++++++- 2 files changed, 203 insertions(+), 5 deletions(-) diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index f3aabf5..5bfaa36 100644 --- a/octodns/processor/filter.py +++ b/octodns/processor/filter.py @@ -9,6 +9,8 @@ from __future__ import ( unicode_literals, ) +from re import compile as re_compile + from .base import BaseProcessor @@ -19,8 +21,8 @@ class TypeAllowlistFilter(BaseProcessor): processors: only-a-and-aaaa: - class: octodns.processor.filter.TypeRejectlistFilter - rejectlist: + class: octodns.processor.filter.TypeAllowlistFilter + allowlist: - A - AAAA @@ -35,7 +37,7 @@ class TypeAllowlistFilter(BaseProcessor): ''' def __init__(self, name, allowlist): - super(TypeAllowlistFilter, self).__init__(name) + super().__init__(name) self.allowlist = set(allowlist) def _process(self, zone, *args, **kwargs): @@ -71,7 +73,7 @@ class TypeRejectlistFilter(BaseProcessor): ''' def __init__(self, name, rejectlist): - super(TypeRejectlistFilter, self).__init__(name) + super().__init__(name) self.rejectlist = set(rejectlist) def _process(self, zone, *args, **kwargs): @@ -83,3 +85,114 @@ class TypeRejectlistFilter(BaseProcessor): process_source_zone = _process process_target_zone = _process + + +class _NameBaseFilter(BaseProcessor): + def __init__(self, name, _list): + super().__init__(name) + exact = set() + regex = [] + for pattern in _list: + if pattern.startswith('/'): + regex.append(re_compile(pattern[1:-1])) + else: + exact.add(pattern) + self.exact = exact + self.regex = regex + + +class NameAllowlistFilter(_NameBaseFilter): + '''Only manage records with names that match the provider patterns + + Example usage: + + processors: + only-these: + class: octodns.processor.filter.NameAllowlistFilter + allowlist: + # exact string match + - www + # contains/substring match + - /substring/ + # regex pattern match + - /some-pattern-\\d\\+/ + # regex - anchored so has to match start to end + - /^start-.+-end$/ + + zones: + exxampled.com.: + sources: + - config + processors: + - only-these + targets: + - route53 + ''' + + def __init__(self, name, allowlist): + super().__init__(name, allowlist) + + def _process(self, zone, *args, **kwargs): + for record in zone.records: + name = record.name + if name in self.exact: + continue + + if any(r.search(name) for r in self.regex): + continue + + zone.remove_record(record) + + return zone + + process_source_zone = _process + process_target_zone = _process + + +class NameRejectlistFilter(_NameBaseFilter): + '''Reject managing records with names that match the provider patterns + + Example usage: + + processors: + not-these: + class: octodns.processor.filter.NameRejectlistFilter + rejectlist: + # exact string match + - www + # contains/substring match + - /substring/ + # regex pattern match + - /some-pattern-\\d\\+/ + # regex - anchored so has to match start to end + - /^start-.+-end$/ + + zones: + exxampled.com.: + sources: + - config + processors: + - not-these + targets: + - route53 + ''' + + def __init__(self, name, rejectlist): + super().__init__(name, rejectlist) + + def _process(self, zone, *args, **kwargs): + for record in zone.records: + name = record.name + if name in self.exact: + zone.remove_record(record) + continue + + for regex in self.regex: + if regex.search(name): + zone.remove_record(record) + break + + return zone + + process_source_zone = _process + process_target_zone = _process diff --git a/tests/test_octodns_processor_filter.py b/tests/test_octodns_processor_filter.py index 859677d..aa6f5ff 100644 --- a/tests/test_octodns_processor_filter.py +++ b/tests/test_octodns_processor_filter.py @@ -11,7 +11,12 @@ from __future__ import ( from unittest import TestCase -from octodns.processor.filter import TypeAllowlistFilter, TypeRejectlistFilter +from octodns.processor.filter import ( + NameAllowlistFilter, + NameRejectlistFilter, + TypeAllowlistFilter, + TypeRejectlistFilter, +) from octodns.record import Record from octodns.zone import Zone @@ -76,3 +81,83 @@ class TestTypeRejectListFilter(TestCase): filter_a_aaaa = TypeRejectlistFilter('not-a-aaaa', set(('A', 'AAAA'))) got = filter_a_aaaa.process_target_zone(zone.copy()) self.assertEqual(['txt', 'txt2'], sorted([r.name for r in got.records])) + + +class TestNameAllowListFilter(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_exact(self): + allows = NameAllowlistFilter('exact', ('matches',)) + + self.assertEqual(4, len(self.zone.records)) + filtered = allows.process_source_zone(self.zone.copy()) + self.assertEqual(1, len(filtered.records)) + self.assertEqual(['matches'], [r.name for r in filtered.records]) + + def test_regex(self): + allows = NameAllowlistFilter('exact', ('/^start-.+-end$/',)) + + self.assertEqual(4, len(self.zone.records)) + filtered = allows.process_source_zone(self.zone.copy()) + self.assertEqual(2, len(filtered.records)) + self.assertEqual( + ['start-a3b444c-end', 'start-f43ad96-end'], + sorted([r.name for r in filtered.records]), + ) + + +class TestNameRejectListFilter(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_exact(self): + rejects = NameRejectlistFilter('exact', ('matches',)) + + self.assertEqual(4, len(self.zone.records)) + filtered = rejects.process_source_zone(self.zone.copy()) + self.assertEqual(3, len(filtered.records)) + self.assertEqual( + ['doesnt', 'start-a3b444c-end', 'start-f43ad96-end'], + sorted([r.name for r in filtered.records]), + ) + + def test_regex(self): + rejects = NameRejectlistFilter('exact', ('/^start-.+-end$/',)) + + self.assertEqual(4, len(self.zone.records)) + filtered = rejects.process_source_zone(self.zone.copy()) + self.assertEqual(2, len(filtered.records)) + self.assertEqual( + ['doesnt', 'matches'], sorted([r.name for r in filtered.records]) + ) From 92ecb545645002a502d3a3b7871d1c9f32f59d8f Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 11 Sep 2022 15:02:09 -0700 Subject: [PATCH 22/32] Add a note about name reject/allow filter to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78685f6..360dd86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * 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. +* NameAllowlistFilter & NameRejectlistFilter implementations to support + filtering on record names to include/exclude records from management. ## v0.9.18 - 2022-08-14 - Subzone handling From 8ff83b8ed9e41e7d36e60bffbd11e4088b09a82b Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 12 Sep 2022 14:18:20 -0700 Subject: [PATCH 23/32] 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 24/32] 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 0f57e6c63e2b1cf269ce9536f79ba6aa861f30f8 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 12 Sep 2022 15:28:51 -0700 Subject: [PATCH 25/32] Implement manager.processors for configuring global processors --- octodns/manager.py | 5 ++++- tests/config/processors.yaml | 6 ++++++ tests/helpers.py | 10 ++++++++++ tests/test_octodns_manager.py | 9 ++++++++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index c58074a..d33c960 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -127,6 +127,9 @@ class Manager(object): manager_config, include_meta ) + self.global_processors = manager_config.get('processors', []) + self.log.info('__init__: global_processors=%s', self.global_processors) + providers_config = self.config['providers'] self.providers = self._config_providers(providers_config) @@ -539,7 +542,7 @@ class Manager(object): try: collected = [] - for processor in processors: + for processor in self.global_processors + processors: collected.append(self.processors[processor]) processors = collected except KeyError: diff --git a/tests/config/processors.yaml b/tests/config/processors.yaml index ec50fb3..6fa9e92 100644 --- a/tests/config/processors.yaml +++ b/tests/config/processors.yaml @@ -1,3 +1,7 @@ +manager: + processors: + - global-counter + providers: config: # This helps us get coverage when printing out provider versions @@ -19,6 +23,8 @@ processors: test: # This helps us get coverage when printing out processor versions class: helpers.TestBaseProcessor + global-counter: + class: helpers.CountingProcessor zones: unit.tests.: diff --git a/tests/helpers.py b/tests/helpers.py index 5bb0a86..5eee380 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -131,3 +131,13 @@ class TestYamlProvider(YamlProvider): class TestBaseProcessor(BaseProcessor): pass + + +class CountingProcessor(BaseProcessor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.count = 0 + + def process_source_zone(self, zone, *args, **kwargs): + self.count += len(zone.records) + return zone diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index c0cbfca..2d9e16a 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -643,9 +643,16 @@ class TestManager(TestCase): def test_processor_config(self): # Smoke test loading a valid config manager = Manager(get_config_filename('processors.yaml')) - self.assertEqual(['noop', 'test'], list(manager.processors.keys())) + self.assertEqual( + ['noop', 'test', 'global-counter'], list(manager.processors.keys()) + ) + # make sure we got the global processor and that it's count is 0 now + self.assertEqual(['global-counter'], manager.global_processors) + self.assertEqual(0, manager.processors['global-counter'].count) # This zone specifies a valid processor manager.sync(['unit.tests.']) + # make sure the global processor ran and counted some records + self.assertTrue(manager.processors['global-counter'].count >= 25) with self.assertRaises(ManagerException) as ctx: # This zone specifies a non-existent processor From 5498a3b9c9b64286eabb7461abceed5929f0dffa Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 12 Sep 2022 16:37:19 -0700 Subject: [PATCH 26/32] CHANGELOG entry for global processors --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78685f6..aaa9128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * 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. +* Support for configuring global processors that apply to all zones with + `manager.processors` ## v0.9.18 - 2022-08-14 - Subzone handling From 48831659e5896e4354f4affec71f8bdf5fe082bd Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 14 Sep 2022 06:46:13 -0700 Subject: [PATCH 27/32] 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), + ) From 5cb42d6d73db1a3ef3b8dc45e832abe776d394ba Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 14 Sep 2022 07:00:33 -0700 Subject: [PATCH 28/32] elif trivial improvement --- octodns/processor/filter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index 5bfaa36..fdbd8ec 100644 --- a/octodns/processor/filter.py +++ b/octodns/processor/filter.py @@ -137,8 +137,7 @@ class NameAllowlistFilter(_NameBaseFilter): name = record.name if name in self.exact: continue - - if any(r.search(name) for r in self.regex): + elif any(r.search(name) for r in self.regex): continue zone.remove_record(record) From 2982733c70eff068fe29aa12c5b00a44142fed5d Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 14 Sep 2022 09:46:25 -0700 Subject: [PATCH 29/32] CHANGELOG entry about IDNA support --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fba38e..a84f0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## v0.9.19 - 2022-??-?? - ??? +#### Noteworthy changes + +* Added support for automatic handling of IDNA (utf-8) zones. Everything is + stored IDNA encoded internally. For ASCII zones that's a noop. For zones with + utf-8 chars they will be converted and all internals/providers will see the + encoded version and work with it without any knowledge of it having been + converted. This means that all providers will automatically support IDNA as of + this version. IDNA zones will generally be displayed in the logs in their + decoded form. Both forms should be accepted in command line arguments. + Providers may need to be updated to display the decoded form in their logs, + until then they'd display the IDNA version. + +#### Stuff + * Addressed shortcomings with YamlProvider.SUPPORTS in that it didn't include dynamically registered types, was a static list that could have drifted over time even ignoring 3rd party types. From d7880c084d0e4d4c134afd1be600faf8e57b2070 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 15 Sep 2022 14:10:21 -0700 Subject: [PATCH 30/32] Remove from __futures__ bits, all are now manditory --- octodns/__init__.py | 7 ------- octodns/cmds/__init__.py | 7 ------- octodns/cmds/args.py | 7 ------- octodns/cmds/compare.py | 7 ------- octodns/cmds/dump.py | 7 ------- octodns/cmds/report.py | 7 ------- octodns/cmds/sync.py | 7 ------- octodns/cmds/validate.py | 7 ------- octodns/cmds/versions.py | 7 ------- octodns/equality.py | 7 ------- octodns/manager.py | 7 ------- octodns/processor/__init__.py | 7 ------- octodns/processor/acme.py | 7 ------- octodns/processor/awsacm.py | 7 ------- octodns/processor/base.py | 7 ------- octodns/processor/filter.py | 7 ------- octodns/processor/ownership.py | 7 ------- octodns/processor/restrict.py | 7 ------- octodns/provider/__init__.py | 7 ------- octodns/provider/azuredns.py | 7 ------- octodns/provider/base.py | 7 ------- octodns/provider/cloudflare.py | 7 ------- octodns/provider/constellix.py | 7 ------- octodns/provider/digitalocean.py | 7 ------- octodns/provider/dnsimple.py | 7 ------- octodns/provider/dnsmadeeasy.py | 7 ------- octodns/provider/dyn.py | 7 ------- octodns/provider/easydns.py | 7 ------- octodns/provider/edgedns.py | 7 ------- octodns/provider/etc_hosts.py | 7 ------- octodns/provider/fastdns.py | 7 ------- octodns/provider/gandi.py | 7 ------- octodns/provider/gcore.py | 7 ------- octodns/provider/googlecloud.py | 7 ------- octodns/provider/hetzner.py | 7 ------- octodns/provider/mythicbeasts.py | 7 ------- octodns/provider/ns1.py | 7 ------- octodns/provider/ovh.py | 7 ------- octodns/provider/plan.py | 7 ------- octodns/provider/powerdns.py | 7 ------- octodns/provider/rackspace.py | 7 ------- octodns/provider/route53.py | 7 ------- octodns/provider/selectel.py | 7 ------- octodns/provider/transip.py | 7 ------- octodns/provider/ultra.py | 7 ------- octodns/provider/yaml.py | 7 ------- octodns/record/__init__.py | 7 ------- octodns/source/__init__.py | 7 ------- octodns/source/axfr.py | 7 ------- octodns/source/base.py | 7 ------- octodns/source/tinydns.py | 7 ------- octodns/yaml.py | 7 ------- octodns/zone.py | 7 ------- tests/helpers.py | 7 ------- tests/test_octodns_equality.py | 7 ------- tests/test_octodns_idna.py | 7 ------- tests/test_octodns_manager.py | 7 ------- tests/test_octodns_plan.py | 7 ------- tests/test_octodns_processor_acme.py | 7 ------- tests/test_octodns_processor_awsacm.py | 7 ------- tests/test_octodns_processor_filter.py | 7 ------- tests/test_octodns_processor_ownership.py | 7 ------- tests/test_octodns_provider_azuredns.py | 7 ------- tests/test_octodns_provider_base.py | 7 ------- tests/test_octodns_provider_cloudflare.py | 7 ------- tests/test_octodns_provider_constellix.py | 7 ------- tests/test_octodns_provider_digitalocean.py | 7 ------- tests/test_octodns_provider_dnsimple.py | 7 ------- tests/test_octodns_provider_dnsmadeeasy.py | 7 ------- tests/test_octodns_provider_dyn.py | 7 ------- tests/test_octodns_provider_easydns.py | 7 ------- tests/test_octodns_provider_edgedns.py | 7 ------- tests/test_octodns_provider_etc_hosts.py | 7 ------- tests/test_octodns_provider_gandi.py | 7 ------- tests/test_octodns_provider_gcore.py | 7 ------- tests/test_octodns_provider_googlecloud.py | 7 ------- tests/test_octodns_provider_hetzner.py | 7 ------- tests/test_octodns_provider_mythicbeasts.py | 7 ------- tests/test_octodns_provider_ns1.py | 7 ------- tests/test_octodns_provider_ovh.py | 7 ------- tests/test_octodns_provider_powerdns.py | 7 ------- tests/test_octodns_provider_rackspace.py | 7 ------- tests/test_octodns_provider_route53.py | 7 ------- tests/test_octodns_provider_selectel.py | 7 ------- tests/test_octodns_provider_transip.py | 7 ------- tests/test_octodns_provider_ultra.py | 7 ------- tests/test_octodns_provider_yaml.py | 7 ------- tests/test_octodns_record.py | 7 ------- tests/test_octodns_record_geo.py | 7 ------- tests/test_octodns_source_axfr.py | 7 ------- tests/test_octodns_source_tinydns.py | 7 ------- tests/test_octodns_yaml.py | 7 ------- tests/test_octodns_zone.py | 7 ------- 93 files changed, 651 deletions(-) diff --git a/octodns/__init__.py b/octodns/__init__.py index f08da5c..4225567 100644 --- a/octodns/__init__.py +++ b/octodns/__init__.py @@ -1,10 +1,3 @@ 'OctoDNS: DNS as code - Tools for managing DNS across multiple providers' -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - __VERSION__ = '0.9.19' diff --git a/octodns/cmds/__init__.py b/octodns/cmds/__init__.py index 16a8eb0..407eb4e 100644 --- a/octodns/cmds/__init__.py +++ b/octodns/cmds/__init__.py @@ -1,10 +1,3 @@ # # # - -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) diff --git a/octodns/cmds/args.py b/octodns/cmds/args.py index 7f04f43..ac00079 100644 --- a/octodns/cmds/args.py +++ b/octodns/cmds/args.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from argparse import ArgumentParser as _Base from logging import DEBUG, INFO, WARN, Formatter, StreamHandler, getLogger from logging.handlers import SysLogHandler diff --git a/octodns/cmds/compare.py b/octodns/cmds/compare.py index 818a436..a25bd68 100755 --- a/octodns/cmds/compare.py +++ b/octodns/cmds/compare.py @@ -3,13 +3,6 @@ Octo-DNS Comparator ''' -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from pprint import pprint import sys diff --git a/octodns/cmds/dump.py b/octodns/cmds/dump.py index 5629342..38eaeb9 100755 --- a/octodns/cmds/dump.py +++ b/octodns/cmds/dump.py @@ -3,13 +3,6 @@ Octo-DNS Dumper ''' -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from octodns.cmds.args import ArgumentParser from octodns.manager import Manager diff --git a/octodns/cmds/report.py b/octodns/cmds/report.py index 03a8f11..2bc5fa3 100755 --- a/octodns/cmds/report.py +++ b/octodns/cmds/report.py @@ -3,13 +3,6 @@ Octo-DNS Reporter ''' -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from concurrent.futures import ThreadPoolExecutor from dns.exception import Timeout from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, Resolver, query diff --git a/octodns/cmds/sync.py b/octodns/cmds/sync.py index 05bc146..6efa4f5 100755 --- a/octodns/cmds/sync.py +++ b/octodns/cmds/sync.py @@ -3,13 +3,6 @@ Octo-DNS Multiplexer ''' -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from octodns.cmds.args import ArgumentParser from octodns.manager import Manager diff --git a/octodns/cmds/validate.py b/octodns/cmds/validate.py index f69c02b..b69f856 100755 --- a/octodns/cmds/validate.py +++ b/octodns/cmds/validate.py @@ -3,13 +3,6 @@ Octo-DNS Validator ''' -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import WARN from octodns.cmds.args import ArgumentParser diff --git a/octodns/cmds/versions.py b/octodns/cmds/versions.py index 2794ee4..59a009e 100755 --- a/octodns/cmds/versions.py +++ b/octodns/cmds/versions.py @@ -3,13 +3,6 @@ octoDNS Versions ''' -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from octodns.cmds.args import ArgumentParser from octodns.manager import Manager diff --git a/octodns/equality.py b/octodns/equality.py index db08a2a..2514d27 100644 --- a/octodns/equality.py +++ b/octodns/equality.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - class EqualityTupleMixin(object): def _equality_tuple(self): diff --git a/octodns/manager.py b/octodns/manager.py index d33c960..5677e6d 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from collections import deque from concurrent.futures import ThreadPoolExecutor from importlib import import_module diff --git a/octodns/processor/__init__.py b/octodns/processor/__init__.py index 16a8eb0..407eb4e 100644 --- a/octodns/processor/__init__.py +++ b/octodns/processor/__init__.py @@ -1,10 +1,3 @@ # # # - -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) diff --git a/octodns/processor/acme.py b/octodns/processor/acme.py index 2797dba..cab3f16 100644 --- a/octodns/processor/acme.py +++ b/octodns/processor/acme.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger from .base import BaseProcessor diff --git a/octodns/processor/awsacm.py b/octodns/processor/awsacm.py index dcd53f9..68280fe 100644 --- a/octodns/processor/awsacm.py +++ b/octodns/processor/awsacm.py @@ -2,13 +2,6 @@ # Ignores AWS ACM validation CNAME records. # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Route53') diff --git a/octodns/processor/base.py b/octodns/processor/base.py index c6c368e..5279af2 100644 --- a/octodns/processor/base.py +++ b/octodns/processor/base.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - class ProcessorException(Exception): pass diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index fdbd8ec..256d9f4 100644 --- a/octodns/processor/filter.py +++ b/octodns/processor/filter.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from re import compile as re_compile from .base import BaseProcessor diff --git a/octodns/processor/ownership.py b/octodns/processor/ownership.py index 68b7430..083a583 100644 --- a/octodns/processor/ownership.py +++ b/octodns/processor/ownership.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from collections import defaultdict from ..provider.plan import Plan diff --git a/octodns/processor/restrict.py b/octodns/processor/restrict.py index 1903995..e585eeb 100644 --- a/octodns/processor/restrict.py +++ b/octodns/processor/restrict.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from .base import BaseProcessor, ProcessorException diff --git a/octodns/provider/__init__.py b/octodns/provider/__init__.py index 4acfe88..3fa1627 100644 --- a/octodns/provider/__init__.py +++ b/octodns/provider/__init__.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - class ProviderException(Exception): pass diff --git a/octodns/provider/azuredns.py b/octodns/provider/azuredns.py index 269df2f..a9bd960 100644 --- a/octodns/provider/azuredns.py +++ b/octodns/provider/azuredns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Azure') diff --git a/octodns/provider/base.py b/octodns/provider/base.py index 0ef52b4..ae9c018 100644 --- a/octodns/provider/base.py +++ b/octodns/provider/base.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from ..source.base import BaseSource from ..zone import Zone from .plan import Plan diff --git a/octodns/provider/cloudflare.py b/octodns/provider/cloudflare.py index 0014620..c64b20c 100644 --- a/octodns/provider/cloudflare.py +++ b/octodns/provider/cloudflare.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Cloudflare') diff --git a/octodns/provider/constellix.py b/octodns/provider/constellix.py index 5747e9a..e89b810 100644 --- a/octodns/provider/constellix.py +++ b/octodns/provider/constellix.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Constellix') diff --git a/octodns/provider/digitalocean.py b/octodns/provider/digitalocean.py index 3e1f75e..c8ca249 100644 --- a/octodns/provider/digitalocean.py +++ b/octodns/provider/digitalocean.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('DigitalOcean') diff --git a/octodns/provider/dnsimple.py b/octodns/provider/dnsimple.py index e34a87f..d8600b5 100644 --- a/octodns/provider/dnsimple.py +++ b/octodns/provider/dnsimple.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Dnsimple') diff --git a/octodns/provider/dnsmadeeasy.py b/octodns/provider/dnsmadeeasy.py index cbb28f0..6080552 100644 --- a/octodns/provider/dnsmadeeasy.py +++ b/octodns/provider/dnsmadeeasy.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('DnsMadeEasy') diff --git a/octodns/provider/dyn.py b/octodns/provider/dyn.py index e7c1120..f8a7699 100644 --- a/octodns/provider/dyn.py +++ b/octodns/provider/dyn.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Dyn') diff --git a/octodns/provider/easydns.py b/octodns/provider/easydns.py index 6dc5c8b..a81e7d7 100644 --- a/octodns/provider/easydns.py +++ b/octodns/provider/easydns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('EasyDns') diff --git a/octodns/provider/edgedns.py b/octodns/provider/edgedns.py index ca439ee..b2692db 100644 --- a/octodns/provider/edgedns.py +++ b/octodns/provider/edgedns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Akamai') diff --git a/octodns/provider/etc_hosts.py b/octodns/provider/etc_hosts.py index e903dd2..858177d 100644 --- a/octodns/provider/etc_hosts.py +++ b/octodns/provider/etc_hosts.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('EtcHosts') diff --git a/octodns/provider/fastdns.py b/octodns/provider/fastdns.py index 0164be5..7428864 100644 --- a/octodns/provider/fastdns.py +++ b/octodns/provider/fastdns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Akamai') diff --git a/octodns/provider/gandi.py b/octodns/provider/gandi.py index 4e08be0..28652ed 100644 --- a/octodns/provider/gandi.py +++ b/octodns/provider/gandi.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Gandi') diff --git a/octodns/provider/gcore.py b/octodns/provider/gcore.py index 983ff1d..bdd3b41 100644 --- a/octodns/provider/gcore.py +++ b/octodns/provider/gcore.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('GCore') diff --git a/octodns/provider/googlecloud.py b/octodns/provider/googlecloud.py index b772f36..bf9754b 100644 --- a/octodns/provider/googlecloud.py +++ b/octodns/provider/googlecloud.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('GoogleCloud') diff --git a/octodns/provider/hetzner.py b/octodns/provider/hetzner.py index b0f362e..7ccdecf 100644 --- a/octodns/provider/hetzner.py +++ b/octodns/provider/hetzner.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Hetzner') diff --git a/octodns/provider/mythicbeasts.py b/octodns/provider/mythicbeasts.py index 708124a..df67ed6 100644 --- a/octodns/provider/mythicbeasts.py +++ b/octodns/provider/mythicbeasts.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('MythicBeasts') diff --git a/octodns/provider/ns1.py b/octodns/provider/ns1.py index 9d60570..2d67a8f 100644 --- a/octodns/provider/ns1.py +++ b/octodns/provider/ns1.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Ns1') diff --git a/octodns/provider/ovh.py b/octodns/provider/ovh.py index de6f045..b18b144 100644 --- a/octodns/provider/ovh.py +++ b/octodns/provider/ovh.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Ovh') diff --git a/octodns/provider/plan.py b/octodns/provider/plan.py index 2e2fc17..ceacb25 100644 --- a/octodns/provider/plan.py +++ b/octodns/provider/plan.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import DEBUG, ERROR, INFO, WARN, getLogger from sys import stdout diff --git a/octodns/provider/powerdns.py b/octodns/provider/powerdns.py index fd76abf..e7ae2b0 100644 --- a/octodns/provider/powerdns.py +++ b/octodns/provider/powerdns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('PowerDns') diff --git a/octodns/provider/rackspace.py b/octodns/provider/rackspace.py index 58b3443..6373add 100644 --- a/octodns/provider/rackspace.py +++ b/octodns/provider/rackspace.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Rackspace') diff --git a/octodns/provider/route53.py b/octodns/provider/route53.py index f5937c0..c2a1cdb 100644 --- a/octodns/provider/route53.py +++ b/octodns/provider/route53.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Route53') diff --git a/octodns/provider/selectel.py b/octodns/provider/selectel.py index 04fcf7b..55710e1 100644 --- a/octodns/provider/selectel.py +++ b/octodns/provider/selectel.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Selectel') diff --git a/octodns/provider/transip.py b/octodns/provider/transip.py index c6c1e8c..9e28a4c 100644 --- a/octodns/provider/transip.py +++ b/octodns/provider/transip.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Transip') diff --git a/octodns/provider/ultra.py b/octodns/provider/ultra.py index 271621b..c79178f 100644 --- a/octodns/provider/ultra.py +++ b/octodns/provider/ultra.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger logger = getLogger('Ultra') diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index da148db..3a3252b 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from collections import defaultdict from os import listdir, makedirs from os.path import isdir, isfile, join diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 1e79327..b3663fd 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from ipaddress import IPv4Address, IPv6Address from logging import getLogger import re diff --git a/octodns/source/__init__.py b/octodns/source/__init__.py index 16a8eb0..407eb4e 100644 --- a/octodns/source/__init__.py +++ b/octodns/source/__init__.py @@ -1,10 +1,3 @@ # # # - -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) diff --git a/octodns/source/axfr.py b/octodns/source/axfr.py index e761575..164a466 100644 --- a/octodns/source/axfr.py +++ b/octodns/source/axfr.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - import dns.name import dns.query import dns.zone diff --git a/octodns/source/base.py b/octodns/source/base.py index f7a2925..30d3190 100644 --- a/octodns/source/base.py +++ b/octodns/source/base.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - class BaseSource(object): diff --git a/octodns/source/tinydns.py b/octodns/source/tinydns.py index 19cf9a5..30189f7 100755 --- a/octodns/source/tinydns.py +++ b/octodns/source/tinydns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from collections import defaultdict from ipaddress import ip_address from os import listdir diff --git a/octodns/yaml.py b/octodns/yaml.py index 5b02431..fbd625a 100644 --- a/octodns/yaml.py +++ b/octodns/yaml.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from natsort import natsort_keygen from yaml import SafeDumper, SafeLoader, load, dump from yaml.constructor import ConstructorError diff --git a/octodns/zone.py b/octodns/zone.py index 32daa4b..f0df959 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from collections import defaultdict from logging import getLogger import re diff --git a/tests/helpers.py b/tests/helpers.py index 5eee380..6efd604 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from shutil import rmtree from tempfile import mkdtemp from logging import getLogger diff --git a/tests/test_octodns_equality.py b/tests/test_octodns_equality.py index 71bfaaa..6b75ec6 100644 --- a/tests/test_octodns_equality.py +++ b/tests/test_octodns_equality.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.equality import EqualityTupleMixin diff --git a/tests/test_octodns_idna.py b/tests/test_octodns_idna.py index 7e1eb68..4571065 100644 --- a/tests/test_octodns_idna.py +++ b/tests/test_octodns_idna.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.idna import IdnaDict, IdnaError, idna_decode, idna_encode diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 2d9e16a..1a36723 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from os import environ from os.path import dirname, isfile, join diff --git a/tests/test_octodns_plan.py b/tests/test_octodns_plan.py index 3b614c7..74d2915 100644 --- a/tests/test_octodns_plan.py +++ b/tests/test_octodns_plan.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from io import StringIO from logging import getLogger from unittest import TestCase diff --git a/tests/test_octodns_processor_acme.py b/tests/test_octodns_processor_acme.py index 78a4f5d..38d4e9d 100644 --- a/tests/test_octodns_processor_acme.py +++ b/tests/test_octodns_processor_acme.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.processor.acme import AcmeMangingProcessor diff --git a/tests/test_octodns_processor_awsacm.py b/tests/test_octodns_processor_awsacm.py index 8d9e071..dbb05fd 100644 --- a/tests/test_octodns_processor_awsacm.py +++ b/tests/test_octodns_processor_awsacm.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_processor_filter.py b/tests/test_octodns_processor_filter.py index aa6f5ff..54224e7 100644 --- a/tests/test_octodns_processor_filter.py +++ b/tests/test_octodns_processor_filter.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.processor.filter import ( diff --git a/tests/test_octodns_processor_ownership.py b/tests/test_octodns_processor_ownership.py index debfacc..f5216d7 100644 --- a/tests/test_octodns_processor_ownership.py +++ b/tests/test_octodns_processor_ownership.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.processor.ownership import OwnershipProcessor diff --git a/tests/test_octodns_provider_azuredns.py b/tests/test_octodns_provider_azuredns.py index f220756..59347b4 100644 --- a/tests/test_octodns_provider_azuredns.py +++ b/tests/test_octodns_provider_azuredns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_base.py b/tests/test_octodns_provider_base.py index b47cc3c..0a78ff7 100644 --- a/tests/test_octodns_provider_base.py +++ b/tests/test_octodns_provider_base.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from logging import getLogger from unittest import TestCase from unittest.mock import MagicMock, call diff --git a/tests/test_octodns_provider_cloudflare.py b/tests/test_octodns_provider_cloudflare.py index 3b5f1ff..0e90eb6 100644 --- a/tests/test_octodns_provider_cloudflare.py +++ b/tests/test_octodns_provider_cloudflare.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_constellix.py b/tests/test_octodns_provider_constellix.py index 97528d2..b795de7 100644 --- a/tests/test_octodns_provider_constellix.py +++ b/tests/test_octodns_provider_constellix.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_digitalocean.py b/tests/test_octodns_provider_digitalocean.py index 0b36a4c..cdb9328 100644 --- a/tests/test_octodns_provider_digitalocean.py +++ b/tests/test_octodns_provider_digitalocean.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_dnsimple.py b/tests/test_octodns_provider_dnsimple.py index 33a2430..8acccc7 100644 --- a/tests/test_octodns_provider_dnsimple.py +++ b/tests/test_octodns_provider_dnsimple.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_dnsmadeeasy.py b/tests/test_octodns_provider_dnsmadeeasy.py index 18fa968..6dd70be 100644 --- a/tests/test_octodns_provider_dnsmadeeasy.py +++ b/tests/test_octodns_provider_dnsmadeeasy.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_dyn.py b/tests/test_octodns_provider_dyn.py index ad5735f..2a326a0 100644 --- a/tests/test_octodns_provider_dyn.py +++ b/tests/test_octodns_provider_dyn.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_easydns.py b/tests/test_octodns_provider_easydns.py index 151a6d7..38aa501 100644 --- a/tests/test_octodns_provider_easydns.py +++ b/tests/test_octodns_provider_easydns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_edgedns.py b/tests/test_octodns_provider_edgedns.py index 1df1fdc..cbe43a3 100644 --- a/tests/test_octodns_provider_edgedns.py +++ b/tests/test_octodns_provider_edgedns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase # Just for coverage diff --git a/tests/test_octodns_provider_etc_hosts.py b/tests/test_octodns_provider_etc_hosts.py index 9a08106..d8c34f6 100644 --- a/tests/test_octodns_provider_etc_hosts.py +++ b/tests/test_octodns_provider_etc_hosts.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_gandi.py b/tests/test_octodns_provider_gandi.py index 00f2c30..1221c50 100644 --- a/tests/test_octodns_provider_gandi.py +++ b/tests/test_octodns_provider_gandi.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase # Just for coverage diff --git a/tests/test_octodns_provider_gcore.py b/tests/test_octodns_provider_gcore.py index dd54268..0cfd650 100644 --- a/tests/test_octodns_provider_gcore.py +++ b/tests/test_octodns_provider_gcore.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_googlecloud.py b/tests/test_octodns_provider_googlecloud.py index 5188cd8..2e651bc 100644 --- a/tests/test_octodns_provider_googlecloud.py +++ b/tests/test_octodns_provider_googlecloud.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_hetzner.py b/tests/test_octodns_provider_hetzner.py index 7f28345..527ef12 100644 --- a/tests/test_octodns_provider_hetzner.py +++ b/tests/test_octodns_provider_hetzner.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_mythicbeasts.py b/tests/test_octodns_provider_mythicbeasts.py index 7ae74b8..3e8aab6 100644 --- a/tests/test_octodns_provider_mythicbeasts.py +++ b/tests/test_octodns_provider_mythicbeasts.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_ns1.py b/tests/test_octodns_provider_ns1.py index 09ab1e3..3d1d07e 100644 --- a/tests/test_octodns_provider_ns1.py +++ b/tests/test_octodns_provider_ns1.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_ovh.py b/tests/test_octodns_provider_ovh.py index 8973af7..9aedd0e 100644 --- a/tests/test_octodns_provider_ovh.py +++ b/tests/test_octodns_provider_ovh.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_powerdns.py b/tests/test_octodns_provider_powerdns.py index 9698c8c..4d86750 100644 --- a/tests/test_octodns_provider_powerdns.py +++ b/tests/test_octodns_provider_powerdns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_rackspace.py b/tests/test_octodns_provider_rackspace.py index cb30d56..d4ee15d 100644 --- a/tests/test_octodns_provider_rackspace.py +++ b/tests/test_octodns_provider_rackspace.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_route53.py b/tests/test_octodns_provider_route53.py index 2486adc..c2aeab5 100644 --- a/tests/test_octodns_provider_route53.py +++ b/tests/test_octodns_provider_route53.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_selectel.py b/tests/test_octodns_provider_selectel.py index 5c429d9..6c54268 100644 --- a/tests/test_octodns_provider_selectel.py +++ b/tests/test_octodns_provider_selectel.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_transip.py b/tests/test_octodns_provider_transip.py index eddddb0..90488bf 100644 --- a/tests/test_octodns_provider_transip.py +++ b/tests/test_octodns_provider_transip.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_ultra.py b/tests/test_octodns_provider_ultra.py index 0e6d9f2..4c6442e 100644 --- a/tests/test_octodns_provider_ultra.py +++ b/tests/test_octodns_provider_ultra.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase diff --git a/tests/test_octodns_provider_yaml.py b/tests/test_octodns_provider_yaml.py index f5b823f..dd2121c 100644 --- a/tests/test_octodns_provider_yaml.py +++ b/tests/test_octodns_provider_yaml.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from os import makedirs from os.path import basename, dirname, isdir, isfile, join from unittest import TestCase diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 9a3ed71..5a625ed 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.idna import idna_encode diff --git a/tests/test_octodns_record_geo.py b/tests/test_octodns_record_geo.py index 16745ac..c7b15a3 100644 --- a/tests/test_octodns_record_geo.py +++ b/tests/test_octodns_record_geo.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.record.geo import GeoCodes diff --git a/tests/test_octodns_source_axfr.py b/tests/test_octodns_source_axfr.py index 356491a..792eacf 100644 --- a/tests/test_octodns_source_axfr.py +++ b/tests/test_octodns_source_axfr.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - import dns.zone from dns.exception import DNSException diff --git a/tests/test_octodns_source_tinydns.py b/tests/test_octodns_source_tinydns.py index 140ae42..3eb4292 100644 --- a/tests/test_octodns_source_tinydns.py +++ b/tests/test_octodns_source_tinydns.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.record import Record diff --git a/tests/test_octodns_yaml.py b/tests/test_octodns_yaml.py index 58b4aae..c3783a3 100644 --- a/tests/test_octodns_yaml.py +++ b/tests/test_octodns_yaml.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from io import StringIO from unittest import TestCase from yaml.constructor import ConstructorError diff --git a/tests/test_octodns_zone.py b/tests/test_octodns_zone.py index 5245735..708862f 100644 --- a/tests/test_octodns_zone.py +++ b/tests/test_octodns_zone.py @@ -2,13 +2,6 @@ # # -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - from unittest import TestCase from octodns.idna import idna_encode From 496183bce07f99f2a676efc4944244919a749a84 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 15 Sep 2022 19:10:17 -0700 Subject: [PATCH 31/32] Fix version type-os in CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9b785..4e835fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## v0.9.19 - 2022-??-?? - ??? +## v0.9.20 - 2022-??-?? - ??? #### Noteworthy changes @@ -31,7 +31,7 @@ * NameAllowlistFilter & NameRejectlistFilter implementations to support filtering on record names to include/exclude records from management. -## v0.9.18 - 2022-08-14 - Subzone handling +## v0.9.19 - 2022-08-14 - Subzone handling * Fixed issue with sub-zone handling introduced in 0.9.18 From 000541eea460bd68ce642bf3295a4f803a0b9237 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 21 Sep 2022 09:14:29 -0700 Subject: [PATCH 32/32] CHANGELOG entry for values as objects --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e835fa..84be125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ * Add TtlRestrictionFilter processor for adding ttl restriction/checking * NameAllowlistFilter & NameRejectlistFilter implementations to support filtering on record names to include/exclude records from management. +* All Record values are now first class objects. This shouldn't be an externally + visible change, but will enable future improvements. ## v0.9.19 - 2022-08-14 - Subzone handling