diff --git a/octodns/manager.py b/octodns/manager.py index 0665938..2e1f6df 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -121,6 +121,20 @@ class Manager(object): raise ManagerException('Incorrect provider config for {}' .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 = {} # sort by reversed strings so that parent zones always come first 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) 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, - sub_zones=self.configured_sub_zones(zone_name)) + sub_zones=self.configured_sub_zones(zone_name), + template_zone=template_zone) for source in sources: try: source.populate(zone, lenient=lenient) @@ -269,6 +285,7 @@ class Manager(object): for zone_name, config in zones: self.log.info('sync: zone=%s', zone_name) lenient = config.get('lenient', False) + template_zone = config.get('template_zone', zone_name) try: sources = config['sources'] except KeyError: @@ -318,8 +335,9 @@ class Manager(object): .format(zone_name, target)) 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 & # plan pairs. @@ -413,7 +431,9 @@ class Manager(object): def validate_configs(self): 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: sources = config['sources'] diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index 10add5a..878d7d5 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -139,8 +139,8 @@ class YamlProvider(BaseProvider): filename) 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: # When acting as a target we ignore any existing records so that we @@ -148,7 +148,9 @@ class YamlProvider(BaseProvider): return False 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.log.info('populate: found %s records, exists=False', @@ -243,11 +245,12 @@ class SplitYamlProvider(YamlProvider): super(SplitYamlProvider, self).__init__(id, directory, *args, **kwargs) 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): - 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: # When acting as a target we ignore any existing records so that we diff --git a/octodns/zone.py b/octodns/zone.py index 5f099ac..7a5aaa6 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -35,13 +35,14 @@ def _is_eligible(record): class Zone(object): log = getLogger('Zone') - def __init__(self, name, sub_zones): + def __init__(self, name, sub_zones, template_zone=None): if not name[-1] == '.': raise Exception('Invalid zone name {}, missing ending dot' .format(name)) # Force everything to lowercase just to be safe self.name = text_type(name).lower() if name else name self.sub_zones = sub_zones + self.template_zone = template_zone # We're grouping by node, it allows us to efficiently search for # duplicates and detect when CNAMEs co-exist with other records self._records = defaultdict(set) diff --git a/tests/config/bad-zone-aliases.yaml b/tests/config/bad-zone-aliases.yaml new file mode 100644 index 0000000..4c47e3c --- /dev/null +++ b/tests/config/bad-zone-aliases.yaml @@ -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 diff --git a/tests/config/simple-aliases.yaml b/tests/config/simple-aliases.yaml new file mode 100644 index 0000000..07a2d74 --- /dev/null +++ b/tests/config/simple-aliases.yaml @@ -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 diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 581689a..052238f 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -290,7 +290,8 @@ class TestManager(TestCase): pass # 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): @@ -299,7 +300,16 @@ class TestManager(TestCase): # This will blow up, we don't fallback for source 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):