Browse Source

Move active/eligible zones, sources, targets to the constuctor. Should allow safer and improved flows

zone-config-cleanup
Ross McFarland 2 months ago
parent
commit
aa210982e8
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
2 changed files with 106 additions and 106 deletions
  1. +26
    -28
      octodns/manager.py
  2. +80
    -78
      tests/test_octodns_manager.py

+ 26
- 28
octodns/manager.py View File

@ -98,17 +98,23 @@ class Manager(object):
include_meta=False, include_meta=False,
auto_arpa=False, auto_arpa=False,
enable_checksum=False, enable_checksum=False,
active_zones=None,
active_sources=None, active_sources=None,
active_targets=None,
): ):
version = self._try_version('octodns', version=__version__) version = self._try_version('octodns', version=__version__)
self.log.info( self.log.info(
'__init__: config_file=%s, active_sources=%s (octoDNS %s)',
'__init__: config_file=%s, active_zones=%s, active_sources=%s, active_targets=%s (octoDNS %s)',
config_file, config_file,
active_zones,
active_sources, active_sources,
active_targets,
version, version,
) )
self.active_zones = active_zones
self.active_sources = active_sources self.active_sources = active_sources
self.active_targets = active_targets
self._configured_sub_zones = None self._configured_sub_zones = None
@ -563,7 +569,7 @@ class Manager(object):
# Return the zone as it's the desired state # Return the zone as it's the desired state
return plans, zone return plans, zone
def _get_sources(self, decoded_zone_name, config, eligible_sources):
def _get_sources(self, decoded_zone_name, config):
try: try:
sources = config['sources'] or [] sources = config['sources'] or []
except KeyError: except KeyError:
@ -571,9 +577,13 @@ class Manager(object):
f'Zone {decoded_zone_name} is missing sources' f'Zone {decoded_zone_name} is missing sources'
) )
if eligible_sources and not [
s for s in sources if s in eligible_sources
if self.active_sources and not [
s for s in sources if s in self.active_sources
]: ]:
self.log.warning(
'_get_sources: no active souces configured for %s',
decoded_zone_name,
)
return None return None
self.log.info('sync: sources=%s', sources) self.log.info('sync: sources=%s', sources)
@ -593,7 +603,7 @@ class Manager(object):
return sources return sources
def _preprocess_zones(self, zones, eligible_sources=None, sources=None):
def _preprocess_zones(self, zones, sources=None):
''' '''
This may modify the passed in zone object, it should be ignored after This may modify the passed in zone object, it should be ignored after
the call and the zones returned from this function should be used the call and the zones returned from this function should be used
@ -609,9 +619,7 @@ class Manager(object):
continue continue
# it's dynamic, get a list of zone names from the configured sources # it's dynamic, get a list of zone names from the configured sources
found_sources = sources or self._get_sources(
name, config, eligible_sources
)
found_sources = sources or self._get_sources(name, config)
self.log.info( self.log.info(
'_preprocess_zones: dynamic zone=%s, sources=%s', '_preprocess_zones: dynamic zone=%s, sources=%s',
name, name,
@ -677,18 +685,10 @@ class Manager(object):
return zones return zones
def sync( def sync(
self,
eligible_zones=[],
eligible_targets=[],
dry_run=True,
force=False,
plan_output_fh=stdout,
checksum=None,
self, dry_run=True, force=False, plan_output_fh=stdout, checksum=None
): ):
self.log.info( self.log.info(
'sync: eligible_zones=%s, eligible_targets=%s, dry_run=%s, force=%s, plan_output_fh=%s, checksum=%s',
eligible_zones,
eligible_targets,
'sync: dry_run=%s, force=%s, plan_output_fh=%s, checksum=%s',
dry_run, dry_run,
force, force,
getattr(plan_output_fh, 'name', plan_output_fh.__class__.__name__), getattr(plan_output_fh, 'name', plan_output_fh.__class__.__name__),
@ -699,15 +699,15 @@ class Manager(object):
zones = self._preprocess_zones(zones, self.active_sources) zones = self._preprocess_zones(zones, self.active_sources)
if eligible_zones:
zones = IdnaDict({n: zones.get(n) for n in eligible_zones})
if self.active_zones:
zones = IdnaDict({n: zones.get(n) for n in self.active_zones})
includes_arpa = any(e.endswith('arpa.') for e in zones.keys()) includes_arpa = any(e.endswith('arpa.') for e in zones.keys())
if self.auto_arpa and includes_arpa: if self.auto_arpa and includes_arpa:
# it's not safe to mess with auto_arpa when we don't have a complete # it's not safe to mess with auto_arpa when we don't have a complete
# picture of records, so if any filtering is happening while arpa # picture of records, so if any filtering is happening while arpa
# zones are in play we need to abort # zones are in play we need to abort
if any(e.endswith('arpa.') for e in eligible_zones):
if any(e.endswith('arpa.') for e in (self.active_zones or [])):
raise ManagerException( raise ManagerException(
'ARPA zones cannot be synced during partial runs when auto_arpa is enabled' 'ARPA zones cannot be synced during partial runs when auto_arpa is enabled'
) )
@ -715,9 +715,9 @@ class Manager(object):
raise ManagerException( raise ManagerException(
'active_sources is incompatible with auto_arpa' 'active_sources is incompatible with auto_arpa'
) )
if eligible_targets:
if self.active_targets:
raise ManagerException( raise ManagerException(
'eligible_targets is incompatible with auto_arpa'
'active_targets is incompatible with auto_arpa'
) )
aliased_zones = {} aliased_zones = {}
@ -751,9 +751,7 @@ class Manager(object):
lenient = config.get('lenient', False) lenient = config.get('lenient', False)
sources = self._get_sources(
decoded_zone_name, config, self.active_sources
)
sources = self._get_sources(decoded_zone_name, config)
try: try:
targets = config['targets'] or [] targets = config['targets'] or []
@ -773,8 +771,8 @@ class Manager(object):
self.log.info('sync: no eligible sources, skipping') self.log.info('sync: no eligible sources, skipping')
continue continue
if eligible_targets:
targets = [t for t in targets if t in eligible_targets]
if self.active_targets:
targets = [t for t in targets if t in self.active_targets]
if not targets: if not targets:
# Don't bother planning (and more importantly populating) zones # Don't bother planning (and more importantly populating) zones


+ 80
- 78
tests/test_octodns_manager.py View File

@ -92,30 +92,34 @@ class TestManager(TestCase):
def test_missing_zone(self): def test_missing_zone(self):
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('dynamic-config.yaml')).sync(
['missing.zones.']
)
Manager(
get_config_filename('dynamic-config.yaml'),
active_zones=['missing.zones.'],
).sync()
self.assertTrue('Requested zone ' in str(ctx.exception)) self.assertTrue('Requested zone ' in str(ctx.exception))
def test_missing_targets(self): def test_missing_targets(self):
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')).sync(
['missing.targets.']
)
Manager(
get_config_filename('provider-problems.yaml'),
active_zones=['missing.targets.'],
).sync()
self.assertTrue('missing targets' in str(ctx.exception)) self.assertTrue('missing targets' in str(ctx.exception))
def test_unknown_source(self): def test_unknown_source(self):
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')).sync(
['unknown.source.']
)
Manager(
get_config_filename('provider-problems.yaml'),
active_zones=['unknown.source.'],
).sync()
self.assertTrue('unknown source' in str(ctx.exception)) self.assertTrue('unknown source' in str(ctx.exception))
def test_unknown_target(self): def test_unknown_target(self):
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')).sync(
['unknown.target.']
)
Manager(
get_config_filename('provider-problems.yaml'),
active_zones=['unknown.target.'],
).sync()
self.assertTrue('unknown target' in str(ctx.exception)) self.assertTrue('unknown target' in str(ctx.exception))
def test_bad_plan_output_class(self): def test_bad_plan_output_class(self):
@ -135,9 +139,10 @@ class TestManager(TestCase):
def test_source_only_as_a_target(self): def test_source_only_as_a_target(self):
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('provider-problems.yaml')).sync(
['not.targetable.']
)
Manager(
get_config_filename('provider-problems.yaml'),
active_zones=['not.targetable.'],
).sync()
self.assertTrue('does not support targeting' in str(ctx.exception)) self.assertTrue('does not support targeting' in str(ctx.exception))
def test_always_dry_run(self): def test_always_dry_run(self):
@ -160,23 +165,24 @@ class TestManager(TestCase):
# try with just one of the zones # try with just one of the zones
reset(tmpdir.dirname) reset(tmpdir.dirname)
tc = Manager(get_config_filename('simple.yaml')).sync(
dry_run=False, eligible_zones=['unit.tests.']
)
tc = Manager(
get_config_filename('simple.yaml'), active_zones=['unit.tests.']
).sync(dry_run=False)
self.assertEqual(22, tc) self.assertEqual(22, tc)
# the subzone, with 2 targets # the subzone, with 2 targets
reset(tmpdir.dirname) reset(tmpdir.dirname)
tc = Manager(get_config_filename('simple.yaml')).sync(
dry_run=False, eligible_zones=['subzone.unit.tests.']
)
tc = Manager(
get_config_filename('simple.yaml'),
active_zones=['subzone.unit.tests.'],
).sync(dry_run=False)
self.assertEqual(6, tc) self.assertEqual(6, tc)
# and finally the empty zone # and finally the empty zone
reset(tmpdir.dirname) reset(tmpdir.dirname)
tc = Manager(get_config_filename('simple.yaml')).sync(
dry_run=False, eligible_zones=['empty.']
)
tc = Manager(
get_config_filename('simple.yaml'), active_zones=['empty.']
).sync(dry_run=False)
self.assertEqual(0, tc) self.assertEqual(0, tc)
# Again with force # Again with force
@ -245,30 +251,36 @@ class TestManager(TestCase):
# refer to them with utf-8 # refer to them with utf-8
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(eligible_zones=('déjà.vu.',))
manager.active_zones = ('déjà.vu.',)
manager.sync()
self.assertEqual('Zone déjà.vu. is missing sources', str(ctx.exception)) self.assertEqual('Zone déjà.vu. is missing sources', str(ctx.exception))
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(eligible_zones=('deja.vu.',))
manager.active_zones = ('deja.vu.',)
manager.sync()
self.assertEqual('Zone deja.vu. is missing sources', str(ctx.exception)) self.assertEqual('Zone deja.vu. is missing sources', str(ctx.exception))
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(eligible_zones=('こんにちは.jp.',))
manager.active_zones = ('こんにちは.jp.',)
manager.sync()
self.assertEqual( self.assertEqual(
'Zone こんにちは.jp. is missing sources', str(ctx.exception) 'Zone こんにちは.jp. is missing sources', str(ctx.exception)
) )
# refer to them with idna (exceptions are still utf-8 # refer to them with idna (exceptions are still utf-8
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(eligible_zones=(idna_encode('déjà.vu.'),))
manager.active_zones = (idna_encode('déjà.vu.'),)
manager.sync()
self.assertEqual('Zone déjà.vu. is missing sources', str(ctx.exception)) self.assertEqual('Zone déjà.vu. is missing sources', str(ctx.exception))
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(eligible_zones=(idna_encode('deja.vu.'),))
manager.active_zones = (idna_encode('deja.vu.'),)
manager.sync()
self.assertEqual('Zone deja.vu. is missing sources', str(ctx.exception)) self.assertEqual('Zone deja.vu. is missing sources', str(ctx.exception))
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(eligible_zones=(idna_encode('こんにちは.jp.'),))
manager.active_zones = (idna_encode('こんにちは.jp.'),)
manager.sync()
self.assertEqual( self.assertEqual(
'Zone こんにちは.jp. is missing sources', str(ctx.exception) 'Zone こんにちは.jp. is missing sources', str(ctx.exception)
) )
@ -288,9 +300,9 @@ class TestManager(TestCase):
environ['YAML_TMP_DIR'] = tmpdir.dirname environ['YAML_TMP_DIR'] = tmpdir.dirname
environ['YAML_TMP_DIR2'] = tmpdir.dirname environ['YAML_TMP_DIR2'] = tmpdir.dirname
# Only allow a target that doesn't exist # Only allow a target that doesn't exist
tc = Manager(get_config_filename('simple.yaml')).sync(
eligible_targets=['foo']
)
tc = Manager(
get_config_filename('simple.yaml'), active_targets=['foo']
).sync()
self.assertEqual(0, tc) self.assertEqual(0, tc)
def test_aliases(self): def test_aliases(self):
@ -324,8 +336,9 @@ class TestManager(TestCase):
# Sync an alias without the zone it refers to # Sync an alias without the zone it refers to
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
tc = Manager( tc = Manager(
get_config_filename('simple-alias-zone.yaml')
).sync(eligible_zones=["alias.tests."])
get_config_filename('simple-alias-zone.yaml'),
active_zones=["alias.tests."],
).sync()
self.assertEqual( self.assertEqual(
'Zone alias.tests. cannot be synced without zone ' 'Zone alias.tests. cannot be synced without zone '
'unit.tests. sinced it is aliased', 'unit.tests. sinced it is aliased',
@ -696,13 +709,15 @@ class TestManager(TestCase):
self.assertEqual(['global-counter'], manager.global_processors) self.assertEqual(['global-counter'], manager.global_processors)
self.assertEqual(0, manager.processors['global-counter'].count) self.assertEqual(0, manager.processors['global-counter'].count)
# This zone specifies a valid processor # This zone specifies a valid processor
manager.sync(['unit.tests.'])
manager.active_zones = ['unit.tests.']
manager.sync()
# make sure the global processor ran and counted some records # make sure the global processor ran and counted some records
self.assertTrue(manager.processors['global-counter'].count >= 25) self.assertTrue(manager.processors['global-counter'].count >= 25)
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
# This zone specifies a non-existent processor # This zone specifies a non-existent processor
manager.sync(['bad.unit.tests.'])
manager.active_zones = ['bad.unit.tests.']
manager.sync()
self.assertTrue( self.assertTrue(
'Zone bad.unit.tests., unknown processor: ' 'Zone bad.unit.tests., unknown processor: '
'doesnt-exist' in str(ctx.exception) 'doesnt-exist' in str(ctx.exception)
@ -1028,14 +1043,13 @@ class TestManager(TestCase):
self.assertEqual(1800, manager.processors.get("auto-arpa").ttl) self.assertEqual(1800, manager.processors.get("auto-arpa").ttl)
# we can sync eligible_zones so long as they're not arpa # we can sync eligible_zones so long as they're not arpa
tc = manager.sync(dry_run=False, eligible_zones=['unit.tests.'])
manager.active_zones = ['unit.tests.']
tc = manager.sync(dry_run=False)
self.assertEqual(22, tc) self.assertEqual(22, tc)
# can't do partial syncs that include arpa zones # can't do partial syncs that include arpa zones
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(
dry_run=False,
eligible_zones=['unit.tests.', '3.2.2.in-addr.arpa.'],
)
manager.active_zones = ['unit.tests.', '3.2.2.in-addr.arpa.']
manager.sync(dry_run=False)
self.assertEqual( self.assertEqual(
'ARPA zones cannot be synced during partial runs when auto_arpa is enabled', 'ARPA zones cannot be synced during partial runs when auto_arpa is enabled',
str(ctx.exception), str(ctx.exception),
@ -1043,37 +1057,40 @@ class TestManager(TestCase):
# same for active_sources # same for active_sources
reset(tmpdir.dirname) reset(tmpdir.dirname)
manager.active_zones = ['unit.tests.']
manager.active_sources = ['in'] manager.active_sources = ['in']
tc = manager.sync(dry_run=False, eligible_zones=['unit.tests.'])
tc = manager.sync(dry_run=False)
self.assertEqual(22, tc) self.assertEqual(22, tc)
# can't do partial syncs that include arpa zones # can't do partial syncs that include arpa zones
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.active_zones = None
manager.sync(dry_run=False) manager.sync(dry_run=False)
self.assertEqual( self.assertEqual(
'active_sources is incompatible with auto_arpa', 'active_sources is incompatible with auto_arpa',
str(ctx.exception), str(ctx.exception),
) )
# same for eligible_targets
# same for active_targets
reset(tmpdir.dirname) reset(tmpdir.dirname)
manager.active_zones = ['unit.tests.']
manager.active_sources = None manager.active_sources = None
tc = manager.sync(
dry_run=False,
eligible_zones=['unit.tests.'],
eligible_targets=['dump'],
)
manager.active_targets = ['dump']
tc = manager.sync(dry_run=False)
self.assertEqual(22, tc) self.assertEqual(22, tc)
# can't do partial syncs that include arpa zones # can't do partial syncs that include arpa zones
with self.assertRaises(ManagerException) as ctx: with self.assertRaises(ManagerException) as ctx:
manager.sync(dry_run=False, eligible_targets=['dump'])
manager.active_zones = None
manager.sync(dry_run=False)
self.assertEqual( self.assertEqual(
'eligible_targets is incompatible with auto_arpa',
'active_targets is incompatible with auto_arpa',
str(ctx.exception), str(ctx.exception),
) )
# full sync with arpa is fine, 2 extra records from it # full sync with arpa is fine, 2 extra records from it
reset(tmpdir.dirname) reset(tmpdir.dirname)
manager.active_zones = None
manager.active_sources = None manager.active_sources = None
manager.active_targets = None
tc = manager.sync(dry_run=False) tc = manager.sync(dry_run=False)
self.assertEqual(26, tc) self.assertEqual(26, tc)
@ -1085,21 +1102,12 @@ class TestManager(TestCase):
# two zones which should have been dynamically configured via # two zones which should have been dynamically configured via
# list_zones # list_zones
self.assertEqual(
29,
manager.sync(
eligible_zones=['unit.tests.', 'dynamic.tests.'],
dry_run=False,
),
)
manager.active_zones = ['unit.tests.', 'dynamic.tests.']
self.assertEqual(29, manager.sync(dry_run=False))
# just subzone.unit.tests. which was explicitly configured # just subzone.unit.tests. which was explicitly configured
self.assertEqual(
3,
manager.sync(
eligible_zones=['subzone.unit.tests.'], dry_run=False
),
)
manager.active_zones = ['subzone.unit.tests.']
self.assertEqual(3, manager.sync(dry_run=False))
def test_dynamic_config_all(self): def test_dynamic_config_all(self):
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
@ -1150,9 +1158,8 @@ class TestManager(TestCase):
# Before sync, the dynamic zones haven't been discovered yet # Before sync, the dynamic zones haven't been discovered yet
# But we can verify they will be after preprocessing # But we can verify they will be after preprocessing
zones = dict(manager.config['zones']) zones = dict(manager.config['zones'])
preprocessed = manager._preprocess_zones(
zones, eligible_sources=None
)
manager.active_sources = None
preprocessed = manager._preprocess_zones(zones)
# Verify that both dynamic.tests. and sub.dynamic.tests. were discovered # Verify that both dynamic.tests. and sub.dynamic.tests. were discovered
self.assertIn('dynamic.tests.', preprocessed) self.assertIn('dynamic.tests.', preprocessed)
@ -1170,17 +1177,12 @@ class TestManager(TestCase):
# dynamic.tests. has 7 records, sub.dynamic.tests. has 1 record # dynamic.tests. has 7 records, sub.dynamic.tests. has 1 record
# unit.tests. has 22 records # unit.tests. has 22 records
# Total: 7 + 1 + 22 = 30 records # Total: 7 + 1 + 22 = 30 records
self.assertEqual(
30,
manager.sync(
eligible_zones=[
'unit.tests.',
'dynamic.tests.',
'sub.dynamic.tests.',
],
dry_run=False,
),
)
manager.active_zones = [
'unit.tests.',
'dynamic.tests.',
'sub.dynamic.tests.',
]
self.assertEqual(30, manager.sync(dry_run=False))
def test_build_kwargs(self): def test_build_kwargs(self):
manager = Manager(get_config_filename('simple.yaml')) manager = Manager(get_config_filename('simple.yaml'))


Loading…
Cancel
Save