|
|
|
@ -312,102 +312,111 @@ class Ns1Provider(BaseProvider): |
|
|
|
ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found' |
|
|
|
SHARED_NOTIFYLIST_NAME = 'octoDNS NS1 Notify List' |
|
|
|
|
|
|
|
def _update_filter(self, filter, with_disabled): |
|
|
|
if with_disabled: |
|
|
|
filter['disabled'] = False |
|
|
|
return (dict(sorted(filter.items(), key=lambda t: t[0]))) |
|
|
|
return filter |
|
|
|
def _update_filter(self, filter): |
|
|
|
filter.setdefault('disabled', False) |
|
|
|
return (dict(sorted(filter.items(), key=lambda t: t[0]))) |
|
|
|
|
|
|
|
def _UP_FILTER(self, with_disabled): |
|
|
|
@property |
|
|
|
def _UP_FILTER(self): |
|
|
|
return self._update_filter({ |
|
|
|
'config': {}, |
|
|
|
'filter': 'up' |
|
|
|
}, with_disabled) |
|
|
|
}) |
|
|
|
|
|
|
|
def _REGION_FILTER(self, with_disabled): |
|
|
|
@property |
|
|
|
def _REGION_FILTER(self): |
|
|
|
return self._update_filter({ |
|
|
|
'config': { |
|
|
|
'remove_no_georegion': True |
|
|
|
}, |
|
|
|
'filter': u'geofence_regional' |
|
|
|
}, with_disabled) |
|
|
|
}) |
|
|
|
|
|
|
|
def _COUNTRY_FILTER(self, with_disabled): |
|
|
|
@property |
|
|
|
def _COUNTRY_FILTER(self): |
|
|
|
return self._update_filter({ |
|
|
|
'config': { |
|
|
|
'remove_no_location': True |
|
|
|
}, |
|
|
|
'filter': u'geofence_country' |
|
|
|
}, with_disabled) |
|
|
|
}) |
|
|
|
|
|
|
|
# In the NS1 UI/portal, this filter is called "SELECT FIRST GROUP" though |
|
|
|
# the filter name in the NS1 api is 'select_first_region' |
|
|
|
def _SELECT_FIRST_REGION_FILTER(self, with_disabled): |
|
|
|
@property |
|
|
|
def _SELECT_FIRST_REGION_FILTER(self): |
|
|
|
return self._update_filter({ |
|
|
|
'config': {}, |
|
|
|
'filter': u'select_first_region' |
|
|
|
}, with_disabled) |
|
|
|
}) |
|
|
|
|
|
|
|
def _PRIORITY_FILTER(self, with_disabled): |
|
|
|
@property |
|
|
|
def _PRIORITY_FILTER(self): |
|
|
|
return self._update_filter({ |
|
|
|
'config': { |
|
|
|
'eliminate': u'1' |
|
|
|
}, |
|
|
|
'filter': 'priority' |
|
|
|
}, with_disabled) |
|
|
|
}) |
|
|
|
|
|
|
|
def _WEIGHTED_SHUFFLE_FILTER(self, with_disabled): |
|
|
|
@property |
|
|
|
def _WEIGHTED_SHUFFLE_FILTER(self): |
|
|
|
return self._update_filter({ |
|
|
|
'config': {}, |
|
|
|
'filter': u'weighted_shuffle' |
|
|
|
}, with_disabled) |
|
|
|
}) |
|
|
|
|
|
|
|
def _SELECT_FIRST_N_FILTER(self, with_disabled): |
|
|
|
@property |
|
|
|
def _SELECT_FIRST_N_FILTER(self): |
|
|
|
return self._update_filter({ |
|
|
|
'config': { |
|
|
|
'N': u'1' |
|
|
|
}, |
|
|
|
'filter': u'select_first_n' |
|
|
|
}, with_disabled) |
|
|
|
}) |
|
|
|
|
|
|
|
def _BASIC_FILTER_CHAIN(self, with_disabled): |
|
|
|
@property |
|
|
|
def _BASIC_FILTER_CHAIN(self): |
|
|
|
return [ |
|
|
|
self._UP_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_REGION_FILTER(with_disabled), |
|
|
|
self._PRIORITY_FILTER(with_disabled), |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_N_FILTER(with_disabled) |
|
|
|
self._UP_FILTER, |
|
|
|
self._SELECT_FIRST_REGION_FILTER, |
|
|
|
self._PRIORITY_FILTER, |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER, |
|
|
|
self._SELECT_FIRST_N_FILTER |
|
|
|
] |
|
|
|
|
|
|
|
def _FILTER_CHAIN_WITH_REGION(self, with_disabled): |
|
|
|
@property |
|
|
|
def _FILTER_CHAIN_WITH_REGION(self): |
|
|
|
return [ |
|
|
|
self._UP_FILTER(with_disabled), |
|
|
|
self._REGION_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_REGION_FILTER(with_disabled), |
|
|
|
self._PRIORITY_FILTER(with_disabled), |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_N_FILTER(with_disabled) |
|
|
|
self._UP_FILTER, |
|
|
|
self._REGION_FILTER, |
|
|
|
self._SELECT_FIRST_REGION_FILTER, |
|
|
|
self._PRIORITY_FILTER, |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER, |
|
|
|
self._SELECT_FIRST_N_FILTER |
|
|
|
] |
|
|
|
|
|
|
|
def _FILTER_CHAIN_WITH_COUNTRY(self, with_disabled): |
|
|
|
@property |
|
|
|
def _FILTER_CHAIN_WITH_COUNTRY(self): |
|
|
|
return [ |
|
|
|
self._UP_FILTER(with_disabled), |
|
|
|
self._COUNTRY_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_REGION_FILTER(with_disabled), |
|
|
|
self._PRIORITY_FILTER(with_disabled), |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_N_FILTER(with_disabled) |
|
|
|
self._UP_FILTER, |
|
|
|
self._COUNTRY_FILTER, |
|
|
|
self._SELECT_FIRST_REGION_FILTER, |
|
|
|
self._PRIORITY_FILTER, |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER, |
|
|
|
self._SELECT_FIRST_N_FILTER |
|
|
|
] |
|
|
|
|
|
|
|
def _FILTER_CHAIN_WITH_REGION_AND_COUNTRY(self, with_disabled): |
|
|
|
@property |
|
|
|
def _FILTER_CHAIN_WITH_REGION_AND_COUNTRY(self): |
|
|
|
return [ |
|
|
|
self._UP_FILTER(with_disabled), |
|
|
|
self._REGION_FILTER(with_disabled), |
|
|
|
self._COUNTRY_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_REGION_FILTER(with_disabled), |
|
|
|
self._PRIORITY_FILTER(with_disabled), |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER(with_disabled), |
|
|
|
self._SELECT_FIRST_N_FILTER(with_disabled) |
|
|
|
self._UP_FILTER, |
|
|
|
self._REGION_FILTER, |
|
|
|
self._COUNTRY_FILTER, |
|
|
|
self._SELECT_FIRST_REGION_FILTER, |
|
|
|
self._PRIORITY_FILTER, |
|
|
|
self._WEIGHTED_SHUFFLE_FILTER, |
|
|
|
self._SELECT_FIRST_N_FILTER |
|
|
|
] |
|
|
|
|
|
|
|
_REGION_TO_CONTINENT = { |
|
|
|
@ -452,29 +461,27 @@ class Ns1Provider(BaseProvider): |
|
|
|
super(Ns1Provider, self).__init__(id, *args, **kwargs) |
|
|
|
self.monitor_regions = monitor_regions |
|
|
|
self.shared_notifylist = shared_notifylist |
|
|
|
self.record_filters = dict() |
|
|
|
self._client = Ns1Client(api_key, parallelism, retry_count, |
|
|
|
client_config) |
|
|
|
|
|
|
|
def _valid_filter_config(self, filter_cfg, domain): |
|
|
|
with_disabled = self._disabled_flag_in_filters(filter_cfg, domain) |
|
|
|
has_region = self._REGION_FILTER(with_disabled) in filter_cfg |
|
|
|
has_country = self._COUNTRY_FILTER(with_disabled) in filter_cfg |
|
|
|
def _valid_filter_config(self, filter_cfg): |
|
|
|
self._disabled_flag_in_filters(filter_cfg) |
|
|
|
has_region = self._REGION_FILTER in filter_cfg |
|
|
|
has_country = self._COUNTRY_FILTER in filter_cfg |
|
|
|
expected_filter_cfg = self._get_updated_filter_chain(has_region, |
|
|
|
has_country, |
|
|
|
with_disabled) |
|
|
|
has_country) |
|
|
|
return filter_cfg == expected_filter_cfg |
|
|
|
|
|
|
|
def _get_updated_filter_chain(self, has_region, has_country, |
|
|
|
with_disabled=True): |
|
|
|
def _get_updated_filter_chain(self, has_region, has_country): |
|
|
|
if has_region and has_country: |
|
|
|
filter_chain = self._FILTER_CHAIN_WITH_REGION_AND_COUNTRY( |
|
|
|
with_disabled) |
|
|
|
filter_chain = self._FILTER_CHAIN_WITH_REGION_AND_COUNTRY |
|
|
|
elif has_region: |
|
|
|
filter_chain = self._FILTER_CHAIN_WITH_REGION(with_disabled) |
|
|
|
filter_chain = self._FILTER_CHAIN_WITH_REGION |
|
|
|
elif has_country: |
|
|
|
filter_chain = self._FILTER_CHAIN_WITH_COUNTRY(with_disabled) |
|
|
|
filter_chain = self._FILTER_CHAIN_WITH_COUNTRY |
|
|
|
else: |
|
|
|
filter_chain = self._BASIC_FILTER_CHAIN(with_disabled) |
|
|
|
filter_chain = self._BASIC_FILTER_CHAIN |
|
|
|
|
|
|
|
return filter_chain |
|
|
|
|
|
|
|
@ -546,11 +553,9 @@ class Ns1Provider(BaseProvider): |
|
|
|
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') |
|
|
|
# Cache record filters for later use |
|
|
|
record_filters = self.record_filters.setdefault(record['domain'], {}) |
|
|
|
record_filters[_type] = record['filters'] |
|
|
|
|
|
|
|
# All regions (pools) will include the list of default values |
|
|
|
# (eventually) at higher priorities, we'll just add them to this set to |
|
|
|
@ -1394,41 +1399,12 @@ class Ns1Provider(BaseProvider): |
|
|
|
for v in record.values] |
|
|
|
return {'answers': values, 'ttl': record.ttl}, None |
|
|
|
|
|
|
|
def _get_ns1_filters(self, ns1_zone_name): |
|
|
|
ns1_filters = {} |
|
|
|
ns1_zone = {} |
|
|
|
|
|
|
|
try: |
|
|
|
ns1_zone = self._client.zones_retrieve(ns1_zone_name) |
|
|
|
except ResourceException as e: |
|
|
|
if e.message != self.ZONE_NOT_FOUND_MESSAGE: |
|
|
|
raise |
|
|
|
|
|
|
|
if 'records' in ns1_zone: |
|
|
|
for ns1_record in ns1_zone['records']: |
|
|
|
if ns1_record.get('tier', 1) > 1: |
|
|
|
# Need to get the full record data for geo records |
|
|
|
full_rec = self._client.records_retrieve( |
|
|
|
ns1_zone_name, |
|
|
|
ns1_record['domain'], |
|
|
|
ns1_record['type']) |
|
|
|
if 'filters' in full_rec: |
|
|
|
filter_key = f'{ns1_record["domain"]}.' |
|
|
|
ns1_filters[filter_key] = full_rec['filters'] |
|
|
|
|
|
|
|
return ns1_filters |
|
|
|
|
|
|
|
def _disabled_flag_in_filters(self, filters, domain): |
|
|
|
disabled_count = ['disabled' in f for f in filters].count(True) |
|
|
|
if disabled_count and disabled_count != len(filters): |
|
|
|
# Some filters have the disabled flag, and some don't. Disallow |
|
|
|
exception_msg = f'Mixed disabled flag in filters for {domain}' |
|
|
|
raise Ns1Exception(exception_msg) |
|
|
|
return disabled_count == len(filters) |
|
|
|
def _disabled_flag_in_filters(self, filters): |
|
|
|
# fill up filters with disabled=False flag whenever absent |
|
|
|
return [self._update_filter(f) for f in filters] |
|
|
|
|
|
|
|
def _extra_changes(self, desired, changes, **kwargs): |
|
|
|
self.log.debug('_extra_changes: desired=%s', desired.name) |
|
|
|
ns1_filters = self._get_ns1_filters(desired.name[:-1]) |
|
|
|
changed = set([c.record for c in changes]) |
|
|
|
extra = [] |
|
|
|
for record in desired.records: |
|
|
|
@ -1440,16 +1416,16 @@ class Ns1Provider(BaseProvider): |
|
|
|
# Check if filters for existing domains need an update |
|
|
|
# Needs an explicit check since there might be no change in the |
|
|
|
# config at all. Filters however might still need an update |
|
|
|
domain = f'{record.name}.{record.zone.name}' |
|
|
|
if domain in ns1_filters: |
|
|
|
domain_filters = ns1_filters[domain] |
|
|
|
if not self._disabled_flag_in_filters(domain_filters, domain): |
|
|
|
# 'disabled' entry absent in filter config. Need to update |
|
|
|
# filters. Update record |
|
|
|
self.log.info('_extra_changes: change in filters for %s', |
|
|
|
domain) |
|
|
|
extra.append(Update(record, record)) |
|
|
|
continue |
|
|
|
domain = record.fqdn[:-1] |
|
|
|
_type = record._type |
|
|
|
record_filters = self.record_filters.get(domain, {}).get(_type, []) |
|
|
|
if not self._valid_filter_config(record_filters): |
|
|
|
# unrecognized set of filters, overwrite them by updating the |
|
|
|
# record |
|
|
|
self.log.info('_extra_changes: unrecognized filters in %s, ' |
|
|
|
'will update record', domain) |
|
|
|
extra.append(Update(record, record)) |
|
|
|
continue |
|
|
|
|
|
|
|
for value, have in self._monitors_for(record).items(): |
|
|
|
expected = self._monitor_gen(record, value) |
|
|
|
|