diff --git a/octodns/provider/ns1.py b/octodns/provider/ns1.py index a023df2..ff2ba04 100644 --- a/octodns/provider/ns1.py +++ b/octodns/provider/ns1.py @@ -545,21 +545,16 @@ class Ns1Provider(BaseProvider): pass return pool_name - def _data_for_dynamic(self, _type, record): - # First make sure we have the expected filters config - if not self._valid_filter_config(record['filters'], record['domain']): - self.log.error('_data_for_dynamic: %s %s has unsupported ' - 'filters', record['domain'], _type) - raise Ns1Exception('Unrecognized advanced record') - + def _parse_pools(self, answers): # All regions (pools) will include the list of default values # (eventually) at higher priorities, we'll just add them to this set to # we'll have the complete collection. default = set() + # Fill out the pools by walking the answers and looking at their - # region. + # region (< v0.9.11) or notes (> v0.9.11). pools = defaultdict(lambda: {'fallback': None, 'values': []}) - for answer in record['answers']: + for answer in answers: meta = answer['meta'] notes = self._parse_notes(meta.get('note', '')) @@ -579,7 +574,7 @@ class Ns1Provider(BaseProvider): if meta['priority'] != 1: # Ignore all but priority 1 continue - # And use region's pool name as the pool name + # And use region's name as the pool name pool_name = self._parse_dynamic_pool_name(answer['region']) else: # > v0.9.11, use the notes-based name and consider all values @@ -603,18 +598,83 @@ class Ns1Provider(BaseProvider): if fallback is not None: pool['fallback'] = fallback + # Order and convert to a list + default = sorted(default) + + return default, pools + + def _parse_rule_geos(self, meta): + geos = set() + + for georegion in meta.get('georegion', []): + geos.add(self._REGION_TO_CONTINENT[georegion]) + + # Countries are easy enough to map, we just have to find their + # continent + # + # NOTE: Some continents need special handling since NS1 + # does not supprt them as regions. These are defined under + # _CONTINENT_TO_LIST_OF_COUNTRIES. So the countries for these + # regions will be present in meta['country']. If all the countries + # in _CONTINENT_TO_LIST_OF_COUNTRIES[] list are found, + # set the continent as the region and remove individual countries + + special_continents = dict() + for country in meta.get('country', []): + # country_alpha2_to_continent_code fails for Pitcairn ('PN'), + # United States Minor Outlying Islands ('UM') and + # Sint Maarten ('SX') + if country == 'PN': + con = 'OC' + elif country in ['SX', 'UM']: + con = 'NA' + else: + con = country_alpha2_to_continent_code(country) + + if con in self._CONTINENT_TO_LIST_OF_COUNTRIES: + special_continents.setdefault(con, set()).add(country) + else: + geos.add(f'{con}-{country}') + + for continent, countries in special_continents.items(): + if countries == self._CONTINENT_TO_LIST_OF_COUNTRIES[ + continent]: + # All countries found, so add it to geos + geos.add(continent) + else: + # Partial countries found, so just add them as-is to geos + for c in countries: + geos.add(f'{continent}-{c}') + + # States and provinces are easy too, + # just assume NA-US or NA-CA + for state in meta.get('us_state', []): + geos.add(f'NA-US-{state}') + + for province in meta.get('ca_province', []): + geos.add(f'NA-CA-{province}') + + return geos + + def _parse_rules(self, pools, regions): # The regions objects map to rules, but it's a bit fuzzy since they're # tied to pools on the NS1 side, e.g. we can only have 1 rule per pool, # that may eventually run into problems, but I don't have any use-cases # examples currently where it would rules = {} - for pool_name, region in sorted(record['regions'].items()): + for pool_name, region in sorted(regions.items()): # Get the actual pool name by removing the type pool_name = self._parse_dynamic_pool_name(pool_name) meta = region['meta'] notes = self._parse_notes(meta.get('note', '')) + # The group notes field in the UI is a `note` on the region here, + # that's where we can find our pool's fallback in < v0.9.11 anyway + if 'fallback' in notes: + # set the fallback pool name + pools[pool_name]['fallback'] = notes['fallback'] + rule_order = notes['rule-order'] try: rule = rules[rule_order] @@ -625,72 +685,26 @@ class Ns1Provider(BaseProvider): } rules[rule_order] = rule - # The group notes field in the UI is a `note` on the region here, - # that's where we can find our pool's fallback in < v0.9.11 anyway - if 'fallback' in notes: - # set the fallback pool name - pools[pool_name]['fallback'] = notes['fallback'] - - geos = set() - - for georegion in meta.get('georegion', []): - geos.add(self._REGION_TO_CONTINENT[georegion]) - - # Countries are easy enough to map, we just have to find their - # continent - # - # NOTE: Some continents need special handling since NS1 - # does not supprt them as regions. These are defined under - # _CONTINENT_TO_LIST_OF_COUNTRIES. So the countries for these - # regions will be present in meta['country']. If all the countries - # in _CONTINENT_TO_LIST_OF_COUNTRIES[] list are found, - # set the continent as the region and remove individual countries - - special_continents = dict() - for country in meta.get('country', []): - # country_alpha2_to_continent_code fails for Pitcairn ('PN'), - # United States Minor Outlying Islands ('UM') and - # Sint Maarten ('SX') - if country == 'PN': - con = 'OC' - elif country in ['SX', 'UM']: - con = 'NA' - else: - con = country_alpha2_to_continent_code(country) - - if con in self._CONTINENT_TO_LIST_OF_COUNTRIES: - special_continents.setdefault(con, set()).add(country) - else: - geos.add(f'{con}-{country}') - - for continent, countries in special_continents.items(): - if countries == self._CONTINENT_TO_LIST_OF_COUNTRIES[ - continent]: - # All countries found, so add it to geos - geos.add(continent) - else: - # Partial countries found, so just add them as-is to geos - for c in countries: - geos.add(f'{continent}-{c}') - - # States and provinces are easy too, - # just assume NA-US or NA-CA - for state in meta.get('us_state', []): - geos.add(f'NA-US-{state}') - - for province in meta.get('ca_province', []): - geos.add(f'NA-CA-{province}') - + geos = self._parse_rule_geos(meta) if geos: # There are geos, combine them with any existing geos for this # pool and recorded the sorted unique set of them rule['geos'] = sorted(set(rule.get('geos', [])) | geos) - # Order and convert to a list - default = sorted(default) # Convert to list and order - rules = list(rules.values()) - rules.sort(key=lambda r: (r['_order'], r['pool'])) + rules = sorted(rules.values(), key=lambda r: (r['_order'], r['pool'])) + + return rules + + def _data_for_dynamic(self, _type, record): + # First make sure we have the expected filters config + if not self._valid_filter_config(record['filters'], record['domain']): + self.log.error('_data_for_dynamic: %s %s has unsupported ' + 'filters', record['domain'], _type) + raise Ns1Exception('Unrecognized advanced record') + + default, pools = self._parse_pools(record['answers']) + rules = self._parse_rules(pools, record['regions']) data = { 'dynamic': { @@ -1182,10 +1196,8 @@ class Ns1Provider(BaseProvider): } answers.append(answer) - def _params_for_dynamic(self, record): + def _generate_regions(self, record): pools = record.dynamic.pools - - # Convert rules to regions has_country = False has_region = False regions = {} @@ -1267,6 +1279,10 @@ class Ns1Provider(BaseProvider): 'meta': meta, } + return has_country, has_region, regions + + def _generate_answers(self, record, regions): + pools = record.dynamic.pools existing_monitors = self._monitors_for(record) active_monitors = set() @@ -1324,6 +1340,15 @@ class Ns1Provider(BaseProvider): pool_label, pool_answers, pools, priority) + return active_monitors, answers + + def _params_for_dynamic(self, record): + # Convert rules to regions + has_country, has_region, regions = self._generate_regions(record) + + # Convert pools to answers + active_monitors, answers = self._generate_answers(record, regions) + # Update filters as necessary filters = self._get_updated_filter_chain(has_region, has_country)