Browse Source

Add support for zones aliases

This commit adds support for zones aliases. This allows to define one or
multiple zone as aliases of an existing zone without using workarounds
like simlinks and miltiple "zones" entries in the configuration file.

An alias zone is share all of its content with it parent zone, only the
name of the zone is different.

```
zones:
  example.com.:
    aliases:
      - example.net.
      - example.org.
    sources:
      - in
    targets:
      - out
```

Known issues:
- No documentation,
- Only the `octodns-sync` and `octodns-validate` commands supports
aliases zones at this time,

I added a loop in the manager init function which convert all alias
zone to "real" ones during config validation, however I'm not sure
this is the right approach. Comments welcome.
pull/592/head
Jonathan Leroy 5 years ago
parent
commit
b926d78c5c
No known key found for this signature in database GPG Key ID: 7A0BCBE3934842EA
6 changed files with 84 additions and 16 deletions
  1. +27
    -7
      octodns/manager.py
  2. +9
    -6
      octodns/provider/yaml.py
  3. +2
    -1
      octodns/zone.py
  4. +17
    -0
      tests/config/bad-zone-aliases.yaml
  5. +17
    -0
      tests/config/simple-aliases.yaml
  6. +12
    -2
      tests/test_octodns_manager.py

+ 27
- 7
octodns/manager.py View File

@ -121,6 +121,20 @@ class Manager(object):
raise ManagerException('Incorrect provider config for {}' raise ManagerException('Incorrect provider config for {}'
.format(provider_name)) .format(provider_name))
for zone_name, zone_config in self.config['zones'].copy().items():
if 'aliases' in zone_config:
for alias in zone_config['aliases']:
if alias in self.config['zones']:
self.log.exception('Invalid zone alias')
raise ManagerException('Invalid zone alias {}: '
'this zone already exists'
.format(alias))
self.config['zones'][alias] = zone_config
self.config['zones'][alias]['template_zone'] = zone_name
del self.config['zones'][zone_name]['aliases']
zone_tree = {} zone_tree = {}
# sort by reversed strings so that parent zones always come first # sort by reversed strings so that parent zones always come first
for name in sorted(self.config['zones'].keys(), key=lambda s: s[::-1]): for name in sorted(self.config['zones'].keys(), key=lambda s: s[::-1]):
@ -222,12 +236,14 @@ class Manager(object):
self.log.debug('configured_sub_zones: subs=%s', sub_zone_names) self.log.debug('configured_sub_zones: subs=%s', sub_zone_names)
return set(sub_zone_names) return set(sub_zone_names)
def _populate_and_plan(self, zone_name, sources, targets, lenient=False):
def _populate_and_plan(self, zone_name, template_zone, sources, targets,
lenient=False):
self.log.debug('sync: populating, zone=%s, lenient=%s',
zone_name, lenient)
self.log.debug('sync: populating, zone=%s, template=%s, lenient=%s',
zone_name, template_zone, lenient)
zone = Zone(zone_name, zone = Zone(zone_name,
sub_zones=self.configured_sub_zones(zone_name))
sub_zones=self.configured_sub_zones(zone_name),
template_zone=template_zone)
for source in sources: for source in sources:
try: try:
source.populate(zone, lenient=lenient) source.populate(zone, lenient=lenient)
@ -269,6 +285,7 @@ class Manager(object):
for zone_name, config in zones: for zone_name, config in zones:
self.log.info('sync: zone=%s', zone_name) self.log.info('sync: zone=%s', zone_name)
lenient = config.get('lenient', False) lenient = config.get('lenient', False)
template_zone = config.get('template_zone', zone_name)
try: try:
sources = config['sources'] sources = config['sources']
except KeyError: except KeyError:
@ -318,8 +335,9 @@ class Manager(object):
.format(zone_name, target)) .format(zone_name, target))
futures.append(self._executor.submit(self._populate_and_plan, futures.append(self._executor.submit(self._populate_and_plan,
zone_name, sources,
targets, lenient=lenient))
zone_name, template_zone,
sources, targets,
lenient=lenient))
# Wait on all results and unpack/flatten them in to a list of target & # Wait on all results and unpack/flatten them in to a list of target &
# plan pairs. # plan pairs.
@ -413,7 +431,9 @@ class Manager(object):
def validate_configs(self): def validate_configs(self):
for zone_name, config in self.config['zones'].items(): for zone_name, config in self.config['zones'].items():
zone = Zone(zone_name, self.configured_sub_zones(zone_name))
template_zone = config.get('template_zone', zone_name)
zone = Zone(zone_name, self.configured_sub_zones(zone_name),
template_zone)
try: try:
sources = config['sources'] sources = config['sources']


+ 9
- 6
octodns/provider/yaml.py View File

@ -139,8 +139,8 @@ class YamlProvider(BaseProvider):
filename) filename)
def populate(self, zone, target=False, lenient=False): def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
self.log.debug('populate: name=%s, template=%s, target=%s, lenient=%s',
zone.name, zone.template_zone, target, lenient)
if target: if target:
# When acting as a target we ignore any existing records so that we # When acting as a target we ignore any existing records so that we
@ -148,7 +148,9 @@ class YamlProvider(BaseProvider):
return False return False
before = len(zone.records) before = len(zone.records)
filename = join(self.directory, '{}yaml'.format(zone.name))
filename = join(self.directory, '{}yaml'.format(zone.template_zone
if zone.template_zone
else zone.name))
self._populate_from_file(filename, zone, lenient) self._populate_from_file(filename, zone, lenient)
self.log.info('populate: found %s records, exists=False', self.log.info('populate: found %s records, exists=False',
@ -243,11 +245,12 @@ class SplitYamlProvider(YamlProvider):
super(SplitYamlProvider, self).__init__(id, directory, *args, **kwargs) super(SplitYamlProvider, self).__init__(id, directory, *args, **kwargs)
def _zone_directory(self, zone): def _zone_directory(self, zone):
return join(self.directory, zone.name)
return join(self.directory, zone.template_zone if zone.template_zone
else zone.name)
def populate(self, zone, target=False, lenient=False): def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
self.log.debug('populate: name=%s, template=%s, target=%s, lenient=%s',
zone.name, zone.template_zone, target, lenient)
if target: if target:
# When acting as a target we ignore any existing records so that we # When acting as a target we ignore any existing records so that we


+ 2
- 1
octodns/zone.py View File

@ -35,13 +35,14 @@ def _is_eligible(record):
class Zone(object): class Zone(object):
log = getLogger('Zone') log = getLogger('Zone')
def __init__(self, name, sub_zones):
def __init__(self, name, sub_zones, template_zone=None):
if not name[-1] == '.': if not name[-1] == '.':
raise Exception('Invalid zone name {}, missing ending dot' raise Exception('Invalid zone name {}, missing ending dot'
.format(name)) .format(name))
# Force everything to lowercase just to be safe # Force everything to lowercase just to be safe
self.name = text_type(name).lower() if name else name self.name = text_type(name).lower() if name else name
self.sub_zones = sub_zones self.sub_zones = sub_zones
self.template_zone = template_zone
# We're grouping by node, it allows us to efficiently search for # 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
self._records = defaultdict(set) self._records = defaultdict(set)


+ 17
- 0
tests/config/bad-zone-aliases.yaml View File

@ -0,0 +1,17 @@
manager:
max_workers: 2
providers:
in:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
zones:
unit.tests.:
aliases:
- unit.tests.
sources:
- in
targets:
- dump

+ 17
- 0
tests/config/simple-aliases.yaml View File

@ -0,0 +1,17 @@
manager:
max_workers: 2
providers:
in:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
zones:
unit.tests.:
aliases:
- unit-alias.tests.
sources:
- in
targets:
- dump

+ 12
- 2
tests/test_octodns_manager.py View File

@ -290,7 +290,8 @@ class TestManager(TestCase):
pass pass
# This should be ok, we'll fall back to not passing it # This should be ok, we'll fall back to not passing it
manager._populate_and_plan('unit.tests.', [NoLenient()], [])
manager._populate_and_plan('unit.tests.', 'unit.tests.',
[NoLenient()], [])
class NoZone(SimpleProvider): class NoZone(SimpleProvider):
@ -299,7 +300,16 @@ class TestManager(TestCase):
# This will blow up, we don't fallback for source # This will blow up, we don't fallback for source
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
manager._populate_and_plan('unit.tests.', [NoZone()], [])
manager._populate_and_plan('unit.tests.', 'unit.tests.',
[NoZone()], [])
def test_zone_aliases(self):
Manager(get_config_filename('simple-aliases.yaml')).validate_configs()
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('bad-zone-aliases.yaml')) \
.validate_configs()
self.assertTrue('Invalid zone alias' in text_type(ctx.exception))
class TestMainThreadExecutor(TestCase): class TestMainThreadExecutor(TestCase):


Loading…
Cancel
Save