|
|
|
@ -6,8 +6,11 @@ from __future__ import absolute_import, division, print_function, \ |
|
|
|
unicode_literals |
|
|
|
|
|
|
|
from logging import getLogger |
|
|
|
from itertools import chain |
|
|
|
from collections import OrderedDict, defaultdict |
|
|
|
from nsone import NSONE |
|
|
|
from nsone.rest.errors import RateLimitException, ResourceException |
|
|
|
from incf.countryutils import transformations |
|
|
|
from time import sleep |
|
|
|
|
|
|
|
from ..record import Record |
|
|
|
@ -22,9 +25,9 @@ class Ns1Provider(BaseProvider): |
|
|
|
class: octodns.provider.ns1.Ns1Provider |
|
|
|
api_key: env/NS1_API_KEY |
|
|
|
''' |
|
|
|
SUPPORTS_GEO = False |
|
|
|
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', |
|
|
|
'PTR', 'SPF', 'SRV', 'TXT')) |
|
|
|
SUPPORTS_GEO = True |
|
|
|
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', |
|
|
|
'NS', 'PTR', 'SPF', 'SRV', 'TXT')) |
|
|
|
|
|
|
|
ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found' |
|
|
|
|
|
|
|
@ -35,11 +38,50 @@ class Ns1Provider(BaseProvider): |
|
|
|
self._client = NSONE(apiKey=api_key) |
|
|
|
|
|
|
|
def _data_for_A(self, _type, record): |
|
|
|
return { |
|
|
|
# record meta (which would include geo information is only |
|
|
|
# returned when getting a record's detail, not from zone detail |
|
|
|
geo = defaultdict(list) |
|
|
|
data = { |
|
|
|
'ttl': record['ttl'], |
|
|
|
'type': _type, |
|
|
|
'values': record['short_answers'], |
|
|
|
} |
|
|
|
values, codes = [], [] |
|
|
|
if 'answers' not in record: |
|
|
|
values = record['short_answers'] |
|
|
|
for answer in record.get('answers', []): |
|
|
|
meta = answer.get('meta', {}) |
|
|
|
if meta: |
|
|
|
# country + state and country + province are allowed |
|
|
|
# in that case though, supplying a state/province would |
|
|
|
# be redundant since the country would supercede in when |
|
|
|
# resolving the record. it is syntactically valid, however. |
|
|
|
country = meta.get('country', []) |
|
|
|
us_state = meta.get('us_state', []) |
|
|
|
ca_province = meta.get('ca_province', []) |
|
|
|
for cntry in country: |
|
|
|
cn = transformations.cc_to_cn(cntry) |
|
|
|
con = transformations.cn_to_ctca2(cn) |
|
|
|
key = '{}-{}'.format(con, cntry) |
|
|
|
geo[key].extend(answer['answer']) |
|
|
|
for state in us_state: |
|
|
|
key = 'NA-US-{}'.format(state) |
|
|
|
geo[key].extend(answer['answer']) |
|
|
|
for province in ca_province: |
|
|
|
key = 'NA-CA-{}'.format(province) |
|
|
|
geo[key].extend(answer['answer']) |
|
|
|
for code in meta.get('iso_region_code', []): |
|
|
|
key = code |
|
|
|
geo[key].extend(answer['answer']) |
|
|
|
else: |
|
|
|
values.extend(answer['answer']) |
|
|
|
codes.append([]) |
|
|
|
values = [str(x) for x in values] |
|
|
|
geo = OrderedDict( |
|
|
|
{str(k): [str(x) for x in v] for k, v in geo.items()} |
|
|
|
) |
|
|
|
data['values'] = values |
|
|
|
data['geo'] = geo |
|
|
|
return data |
|
|
|
|
|
|
|
_data_for_AAAA = _data_for_A |
|
|
|
|
|
|
|
@ -140,39 +182,79 @@ class Ns1Provider(BaseProvider): |
|
|
|
} |
|
|
|
|
|
|
|
def populate(self, zone, target=False, lenient=False): |
|
|
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, |
|
|
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', |
|
|
|
zone.name, |
|
|
|
target, lenient) |
|
|
|
|
|
|
|
try: |
|
|
|
nsone_zone = self._client.loadZone(zone.name[:-1]) |
|
|
|
records = nsone_zone.data['records'] |
|
|
|
geo_records = nsone_zone.search(has_geo=True) |
|
|
|
except ResourceException as e: |
|
|
|
if e.message != self.ZONE_NOT_FOUND_MESSAGE: |
|
|
|
raise |
|
|
|
records = [] |
|
|
|
geo_records = [] |
|
|
|
|
|
|
|
before = len(zone.records) |
|
|
|
for record in records: |
|
|
|
# geo information isn't returned from the main endpoint, so we need |
|
|
|
# to query for all records with geo information |
|
|
|
zone_hash = {} |
|
|
|
for record in chain(records, geo_records): |
|
|
|
_type = record['type'] |
|
|
|
data_for = getattr(self, '_data_for_{}'.format(_type)) |
|
|
|
name = zone.hostname_from_fqdn(record['domain']) |
|
|
|
record = Record.new(zone, name, data_for(_type, record), |
|
|
|
source=self, lenient=lenient) |
|
|
|
zone.add_record(record) |
|
|
|
|
|
|
|
zone_hash[(_type, name)] = record |
|
|
|
[zone.add_record(r) for r in zone_hash.values()] |
|
|
|
self.log.info('populate: found %s records', |
|
|
|
len(zone.records) - before) |
|
|
|
|
|
|
|
def _params_for_A(self, record): |
|
|
|
return {'answers': record.values, 'ttl': record.ttl} |
|
|
|
params = {'answers': record.values, 'ttl': record.ttl} |
|
|
|
if hasattr(record, 'geo'): |
|
|
|
# purposefully set non-geo answers to have an empty meta, |
|
|
|
# so that we know we did this on purpose if/when troubleshooting |
|
|
|
params['answers'] = [{"answer": [x], "meta": {}} |
|
|
|
for x in record.values] |
|
|
|
has_country = False |
|
|
|
for iso_region, target in record.geo.items(): |
|
|
|
key = 'iso_region_code' |
|
|
|
value = iso_region |
|
|
|
if not has_country and \ |
|
|
|
len(value.split('-')) > 1: # pragma: nocover |
|
|
|
has_country = True |
|
|
|
for answer in target.values: |
|
|
|
params['answers'].append( |
|
|
|
{ |
|
|
|
'answer': [answer], |
|
|
|
'meta': {key: [value]}, |
|
|
|
}, |
|
|
|
) |
|
|
|
params['filters'] = [] |
|
|
|
if len(params['answers']) > 1: |
|
|
|
params['filters'].append( |
|
|
|
{"filter": "shuffle", "config": {}} |
|
|
|
) |
|
|
|
if has_country: |
|
|
|
params['filters'].append( |
|
|
|
{"filter": "geotarget_country", "config": {}} |
|
|
|
) |
|
|
|
params['filters'].append( |
|
|
|
{"filter": "select_first_n", |
|
|
|
"config": {"N": 1}} |
|
|
|
) |
|
|
|
self.log.debug("params for A: %s", params) |
|
|
|
return params |
|
|
|
|
|
|
|
_params_for_AAAA = _params_for_A |
|
|
|
_params_for_NS = _params_for_A |
|
|
|
|
|
|
|
def _params_for_SPF(self, record): |
|
|
|
# NS1 seems to be the only provider that doesn't want things escaped in |
|
|
|
# values so we have to strip them here and add them when going the |
|
|
|
# other way |
|
|
|
# NS1 seems to be the only provider that doesn't want things |
|
|
|
# escaped in values so we have to strip them here and add |
|
|
|
# them when going the other way |
|
|
|
values = [v.replace('\;', ';') for v in record.values] |
|
|
|
return {'answers': values, 'ttl': record.ttl} |
|
|
|
|
|
|
|
@ -264,4 +346,5 @@ class Ns1Provider(BaseProvider): |
|
|
|
|
|
|
|
for change in changes: |
|
|
|
class_name = change.__class__.__name__ |
|
|
|
getattr(self, '_apply_{}'.format(class_name))(nsone_zone, change) |
|
|
|
getattr(self, '_apply_{}'.format(class_name))(nsone_zone, |
|
|
|
change) |