def import_zone_data(filename, api_key, **kwargs): nsone_obj = NSONE(apiKey=api_key) extension = os.path.splitext(filename)[1] with open(filename, 'rb') as f: if extension == '.csv': reader = csv.DictReader(f) data = transform_csv(reader) elif extension == '.json': data = transform_json(json.loads(f)) if kwargs.get('delete'): delete_zone_data(nsone_obj, data) return for k, v in data.iteritems(): try: zone = nsone_obj.createZone(k) print 'Added Zone {}'.format(zone) except AuthException as e: # Invalid api key passed in print e.message return except ResourceException as e: # zone already exists print '{} {}'.format(k, e.message) zone = nsone_obj.loadZone(k) for rec in v: answers = rec['Data'].split() try: # determine which record type to add using types provided in the file method_name = 'add_{}'.format(rec['Type']) add_method = getattr(zone, method_name) # data corresponds to the answer and it might have priority values record = add_method(k, [answers], ttl=rec['TTL']) print 'Successfully Added record {}'.format(record) except ResourceException as e: # record already exists, so add answers to it # Or Invalid add method print '{} {}'.format(rec, e.message) try: record = nsone_obj.loadRecord(k, rec['Type'], k) print 'Successfully loaded record {}'.format(k, rec['Type']) recordAnswers = {answer['answer'][0] for answer in record.data['answers']} if recordAnswers.intersection(answers[0]): record.addAnswers(answers) print 'Added answer: {}'.format(answers[0]) record.addAnswers([answers]) print 'Successfully Processed Answers {}'.format(answers) except ResourceException as e: # Invalid format for answer, so ignore this answer, or invalid record type print e.message continue
class Ns1Provider(BaseProvider): ''' Ns1 provider nsone: class: octodns.provider.ns1.Ns1Provider api_key: env/NS1_API_KEY ''' 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' def __init__(self, id, api_key, *args, **kwargs): self.log = getLogger('Ns1Provider[{}]'.format(id)) self.log.debug('__init__: id=%s, api_key=***', id) super(Ns1Provider, self).__init__(id, *args, **kwargs) self._client = NSONE(apiKey=api_key) def _data_for_A(self, _type, record): # 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, 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 = [unicode(x) for x in values] geo = OrderedDict( {unicode(k): [unicode(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 def _data_for_SPF(self, _type, record): values = [v.replace(';', '\\;') for v in record['short_answers']] return {'ttl': record['ttl'], 'type': _type, 'values': values} _data_for_TXT = _data_for_SPF def _data_for_CAA(self, _type, record): values = [] for answer in record['short_answers']: flags, tag, value = answer.split(' ', 2) values.append({ 'flags': flags, 'tag': tag, 'value': value, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_CNAME(self, _type, record): try: value = record['short_answers'][0] except IndexError: value = None return { 'ttl': record['ttl'], 'type': _type, 'value': value, } _data_for_ALIAS = _data_for_CNAME _data_for_PTR = _data_for_CNAME def _data_for_MX(self, _type, record): values = [] for answer in record['short_answers']: preference, exchange = answer.split(' ', 1) values.append({ 'preference': preference, 'exchange': exchange, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_NAPTR(self, _type, record): values = [] for answer in record['short_answers']: order, preference, flags, service, regexp, replacement = \ answer.split(' ', 5) values.append({ 'flags': flags, 'order': order, 'preference': preference, 'regexp': regexp, 'replacement': replacement, 'service': service, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_NS(self, _type, record): return { 'ttl': record['ttl'], 'type': _type, 'values': [ a if a.endswith('.') else '{}.'.format(a) for a in record['short_answers'] ], } def _data_for_SRV(self, _type, record): values = [] for answer in record['short_answers']: priority, weight, port, target = answer.split(' ', 3) values.append({ 'priority': priority, 'weight': weight, 'port': port, 'target': target, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def populate(self, zone, target=False, lenient=False): 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) exists = True except ResourceException as e: if e.message != self.ZONE_NOT_FOUND_MESSAGE: raise records = [] geo_records = [] exists = False before = len(zone.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'] if _type not in self.SUPPORTS: continue 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_hash[(_type, name)] = record [zone.add_record(r) for r in zone_hash.values()] self.log.info('populate: found %s records, exists=%s', len(zone.records) - before, exists) return exists def _params_for_A(self, record): 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 has_country: params['filters'].append({"filter": "shuffle", "config": {}}) 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 values = [v.replace('\\;', ';') for v in record.values] return {'answers': values, 'ttl': record.ttl} _params_for_TXT = _params_for_SPF def _params_for_CAA(self, record): values = [(v.flags, v.tag, v.value) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_CNAME(self, record): return {'answers': [record.value], 'ttl': record.ttl} _params_for_ALIAS = _params_for_CNAME _params_for_PTR = _params_for_CNAME def _params_for_MX(self, record): values = [(v.preference, v.exchange) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_NAPTR(self, record): values = [(v.order, v.preference, v.flags, v.service, v.regexp, v.replacement) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_SRV(self, record): values = [(v.priority, v.weight, v.port, v.target) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _get_name(self, record): return record.fqdn[:-1] if record.name == '' else record.name def _apply_Create(self, nsone_zone, change): new = change.new name = self._get_name(new) _type = new._type params = getattr(self, '_params_for_{}'.format(_type))(new) meth = getattr(nsone_zone, 'add_{}'.format(_type)) try: meth(name, **params) except RateLimitException as e: period = float(e.period) self.log.warn( '_apply_Create: rate limit encountered, pausing ' 'for %ds and trying again', period) sleep(period) meth(name, **params) def _apply_Update(self, nsone_zone, change): existing = change.existing name = self._get_name(existing) _type = existing._type record = nsone_zone.loadRecord(name, _type) new = change.new params = getattr(self, '_params_for_{}'.format(_type))(new) try: record.update(**params) except RateLimitException as e: period = float(e.period) self.log.warn( '_apply_Update: rate limit encountered, pausing ' 'for %ds and trying again', period) sleep(period) record.update(**params) def _apply_Delete(self, nsone_zone, change): existing = change.existing name = self._get_name(existing) _type = existing._type record = nsone_zone.loadRecord(name, _type) try: record.delete() except RateLimitException as e: period = float(e.period) self.log.warn( '_apply_Delete: rate limit encountered, pausing ' 'for %ds and trying again', period) sleep(period) record.delete() def _apply(self, plan): desired = plan.desired changes = plan.changes self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, len(changes)) domain_name = desired.name[:-1] try: nsone_zone = self._client.loadZone(domain_name) except ResourceException as e: if e.message != self.ZONE_NOT_FOUND_MESSAGE: raise self.log.debug('_apply: no matching zone, creating') nsone_zone = self._client.createZone(domain_name) for change in changes: class_name = change.__class__.__name__ getattr(self, '_apply_{}'.format(class_name))(nsone_zone, change)
# from nsone import NSONE # NSONE will use config in ~/.nsone by default nsone = NSONE() # to specify an apikey here instead, use: # from nsone import Config # config = Config() # config.createFromAPIKey('qACMD09OJXBxT7XOuRs8') # nsone = NSONE(config=config) # create a zone to play in zone = nsone.createZone('testzone.com') # create an NSONE API data source sourceAPI = nsone.datasource() s = sourceAPI.create('my api source', 'nsone_v1') sourceID = s['id'] # create feeds which will drive the meta data for each answer # we'll use the id of these feeds when we connect the feeds to the # answer meta below feedAPI = nsone.datafeed() feed1 = feedAPI.create(sourceID, 'feed to server1', config={'label': 'server1'}) feed2 = feedAPI.create(sourceID,
# # Copyright (c) 2014 NSONE, Inc. # # License under The MIT License (MIT). See LICENSE in project root. # from nsone import NSONE # NSONE will use config in ~/.nsone by default nsone = NSONE() # to specify an apikey here instead, use: # nsone = NSONE(apiKey='qACMD09OJXBxT7XOuRs8') # to load an alternate configuration file: # nsone = NSONE(configFile='/etc/nsone/api.json') # import a zone from the included example zone definition zone = nsone.createZone('example2.com', zoneFile='./importzone.db') print(zone) # delete a whole zone, including all records, data feeds, etc. this is # immediate and irreversible, so be careful! zone.delete()
# to specify an apikey here instead, use: # nsone = NSONE(apiKey='qACMD09OJXBxT7XOuRs8') # to load an alternate configuration file: # nsone = NSONE(configFile='/etc/nsone/api.json') ###################### # LOAD / CREATE ZONE # ###################### # to load an existing zone, get a Zone object back test_zone = nsone.loadZone('test.com') # or create a new zone, get a Zone object back # you can specify options like retry, refresh, expiry, nx_ttl, etc zone = nsone.createZone('example.com', nx_ttl=3600) print(zone) # once you have a Zone, you can access all the zone information via the # data property print(zone.data['dns_servers']) ############### # ADD RECORDS # ############### # in general, use add_XXXX to add a new record to a zone, where XXXX represents # the record type. all of these take optional named parameters like # ttl, use_csubnet, feed, networks, meta, regions, filters # all of these return Record objects
class Ns1Provider(BaseProvider): ''' Ns1 provider nsone: class: octodns.provider.ns1.Ns1Provider api_key: env/NS1_API_KEY ''' SUPPORTS_GEO = False ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found' def __init__(self, id, api_key, *args, **kwargs): self.log = getLogger('Ns1Provider[{}]'.format(id)) self.log.debug('__init__: id=%s, api_key=***', id) super(Ns1Provider, self).__init__(id, *args, **kwargs) self._client = NSONE(apiKey=api_key) def _data_for_A(self, _type, record): return { 'ttl': record['ttl'], 'type': _type, 'values': record['short_answers'], } _data_for_AAAA = _data_for_A _data_for_SPF = _data_for_A _data_for_TXT = _data_for_A def _data_for_CNAME(self, _type, record): return { 'ttl': record['ttl'], 'type': _type, 'value': record['short_answers'][0], } _data_for_PTR = _data_for_CNAME def _data_for_MX(self, _type, record): values = [] for answer in record['short_answers']: priority, value = answer.split(' ', 1) values.append({ 'priority': priority, 'value': value, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_NAPTR(self, _type, record): values = [] for answer in record['short_answers']: order, preference, flags, service, regexp, replacement = \ answer.split(' ', 5) values.append({ 'flags': flags, 'order': order, 'preference': preference, 'regexp': regexp, 'replacement': replacement, 'service': service, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_NS(self, _type, record): return { 'ttl': record['ttl'], 'type': _type, 'values': [ a if a.endswith('.') else '{}.'.format(a) for a in record['short_answers'] ], } def _data_for_SRV(self, _type, record): values = [] for answer in record['short_answers']: priority, weight, port, target = answer.split(' ', 3) values.append({ 'priority': priority, 'weight': weight, 'port': port, 'target': target, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def populate(self, zone, target=False): self.log.debug('populate: name=%s', zone.name) try: nsone_zone = self._client.loadZone(zone.name[:-1]) records = nsone_zone.data['records'] except ResourceException as e: if e.message != self.ZONE_NOT_FOUND_MESSAGE: raise records = [] before = len(zone.records) for record in 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)) zone.add_record(record) 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_for_AAAA = _params_for_A _params_for_NS = _params_for_A _params_for_SPF = _params_for_A _params_for_TXT = _params_for_A def _params_for_CNAME(self, record): return {'answers': [record.value], 'ttl': record.ttl} _params_for_PTR = _params_for_CNAME def _params_for_MX(self, record): values = [(v.priority, v.value) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_NAPTR(self, record): values = [(v.order, v.preference, v.flags, v.service, v.regexp, v.replacement) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_SRV(self, record): values = [(v.priority, v.weight, v.port, v.target) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _get_name(self, record): return record.fqdn[:-1] if record.name == '' else record.name def _apply_Create(self, nsone_zone, change): new = change.new name = self._get_name(new) _type = new._type params = getattr(self, '_params_for_{}'.format(_type))(new) getattr(nsone_zone, 'add_{}'.format(_type))(name, **params) def _apply_Update(self, nsone_zone, change): existing = change.existing name = self._get_name(existing) _type = existing._type record = nsone_zone.loadRecord(name, _type) new = change.new params = getattr(self, '_params_for_{}'.format(_type))(new) record.update(**params) def _apply_Delete(self, nsone_zone, change): existing = change.existing name = self._get_name(existing) _type = existing._type record = nsone_zone.loadRecord(name, _type) record.delete() def _apply(self, plan): desired = plan.desired changes = plan.changes self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, len(changes)) domain_name = desired.name[:-1] try: nsone_zone = self._client.loadZone(domain_name) except ResourceException as e: if e.message != self.ZONE_NOT_FOUND_MESSAGE: raise self.log.debug('_apply: no matching zone, creating') nsone_zone = self._client.createZone(domain_name) for change in changes: class_name = change.__class__.__name__ getattr(self, '_apply_{}'.format(class_name))(nsone_zone, change)
class Ns1Provider(BaseProvider): ''' Ns1 provider nsone: 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')) ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found' def __init__(self, id, api_key, *args, **kwargs): self.log = getLogger('Ns1Provider[{}]'.format(id)) self.log.debug('__init__: id=%s, api_key=***', id) super(Ns1Provider, self).__init__(id, *args, **kwargs) self._client = NSONE(apiKey=api_key) def _data_for_A(self, _type, record): return { 'ttl': record['ttl'], 'type': _type, 'values': record['short_answers'], } _data_for_AAAA = _data_for_A def _data_for_SPF(self, _type, record): values = [v.replace(';', '\;') for v in record['short_answers']] return {'ttl': record['ttl'], 'type': _type, 'values': values} _data_for_TXT = _data_for_SPF def _data_for_CAA(self, _type, record): values = [] for answer in record['short_answers']: flags, tag, value = answer.split(' ', 2) values.append({ 'flags': flags, 'tag': tag, 'value': value, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_CNAME(self, _type, record): return { 'ttl': record['ttl'], 'type': _type, 'value': record['short_answers'][0], } _data_for_ALIAS = _data_for_CNAME _data_for_PTR = _data_for_CNAME def _data_for_MX(self, _type, record): values = [] for answer in record['short_answers']: preference, exchange = answer.split(' ', 1) values.append({ 'preference': preference, 'exchange': exchange, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_NAPTR(self, _type, record): values = [] for answer in record['short_answers']: order, preference, flags, service, regexp, replacement = \ answer.split(' ', 5) values.append({ 'flags': flags, 'order': order, 'preference': preference, 'regexp': regexp, 'replacement': replacement, 'service': service, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def _data_for_NS(self, _type, record): return { 'ttl': record['ttl'], 'type': _type, 'values': [ a if a.endswith('.') else '{}.'.format(a) for a in record['short_answers'] ], } def _data_for_SRV(self, _type, record): values = [] for answer in record['short_answers']: priority, weight, port, target = answer.split(' ', 3) values.append({ 'priority': priority, 'weight': weight, 'port': port, 'target': target, }) return { 'ttl': record['ttl'], 'type': _type, 'values': values, } def populate(self, zone, target=False, lenient=False): 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'] except ResourceException as e: if e.message != self.ZONE_NOT_FOUND_MESSAGE: raise records = [] before = len(zone.records) for record in 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) 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_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 values = [v.replace('\;', ';') for v in record.values] return {'answers': values, 'ttl': record.ttl} _params_for_TXT = _params_for_SPF def _params_for_CAA(self, record): values = [(v.flags, v.tag, v.value) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_CNAME(self, record): return {'answers': [record.value], 'ttl': record.ttl} _params_for_ALIAS = _params_for_CNAME _params_for_PTR = _params_for_CNAME def _params_for_MX(self, record): values = [(v.preference, v.exchange) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_NAPTR(self, record): values = [(v.order, v.preference, v.flags, v.service, v.regexp, v.replacement) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _params_for_SRV(self, record): values = [(v.priority, v.weight, v.port, v.target) for v in record.values] return {'answers': values, 'ttl': record.ttl} def _get_name(self, record): return record.fqdn[:-1] if record.name == '' else record.name def _apply_Create(self, nsone_zone, change): new = change.new name = self._get_name(new) _type = new._type params = getattr(self, '_params_for_{}'.format(_type))(new) meth = getattr(nsone_zone, 'add_{}'.format(_type)) try: meth(name, **params) except RateLimitException as e: self.log.warn( '_apply_Create: rate limit encountered, pausing ' 'for %ds and trying again', e.period) sleep(e.period) meth(name, **params) def _apply_Update(self, nsone_zone, change): existing = change.existing name = self._get_name(existing) _type = existing._type record = nsone_zone.loadRecord(name, _type) new = change.new params = getattr(self, '_params_for_{}'.format(_type))(new) try: record.update(**params) except RateLimitException as e: self.log.warn( '_apply_Update: rate limit encountered, pausing ' 'for %ds and trying again', e.period) sleep(e.period) record.update(**params) def _apply_Delete(self, nsone_zone, change): existing = change.existing name = self._get_name(existing) _type = existing._type record = nsone_zone.loadRecord(name, _type) try: record.delete() except RateLimitException as e: self.log.warn( '_apply_Delete: rate limit encountered, pausing ' 'for %ds and trying again', e.period) sleep(e.period) record.delete() def _apply(self, plan): desired = plan.desired changes = plan.changes self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, len(changes)) domain_name = desired.name[:-1] try: nsone_zone = self._client.loadZone(domain_name) except ResourceException as e: if e.message != self.ZONE_NOT_FOUND_MESSAGE: raise self.log.debug('_apply: no matching zone, creating') nsone_zone = self._client.createZone(domain_name) for change in changes: class_name = change.__class__.__name__ getattr(self, '_apply_{}'.format(class_name))(nsone_zone, change)
class NsoneImporter(object): """ Attributes: config (nsone.Config): The configuration for the nsone requests. nsoneObj (nsone.NSONE): Instance of the nsone object used for http requests data (dict): Dictionary containing the zone data used by all methods for importing deleteData (bool): Attribute used to call deletion endpoints instead of importing """ config = Config() def __init__(self, apiKey, data, delete): """ Args: apiKey (str): The Nsone Api Key data (Dict): Zone Data Dict delete (bool): Delete Flag, defaults to false from argument parser """ self.config.createFromAPIKey(apiKey) self.config['transport'] = 'twisted' self.nsoneObj = NSONE(config=self.config) self.data = data self.deleteData = delete def _deleteZoneData(self): """ Parent method that triggers callback chain for deleting all zones. Makes call to deleteZonesandRecords method and depending on the response, the success or error callbacks are fired. Each call to deleteZonesandRecords is an instance of a deferred object and each object is collected for graceful termination """ dl = [] for zoneName, records in self.data: deleteZoneRes = self._deleteZonesAndRecords(zoneName, records, self.nsoneObj) deleteZoneRes.addCallback(self._deleteZoneSuccess, zoneName) deleteZoneRes.addErrback(self._deleteZoneFailure, zoneName) dl.append(deleteZoneRes) return defer.DeferredList(dl, fireOnOneErrback=True) @defer.inlineCallbacks def _deleteZonesAndRecords(self, zoneName, records, nsoneObj): """ Returns a deferred object that calls the delete method of the nsone object. Args: zoneName (str): The zone name from the data dictionary records (Dict): a list of records belonging to this zone nsoneObj (nsone.NSONE): Instance of the nsone object """ zone = yield nsoneObj.loadZone(zoneName) yield zone.delete() def _deleteZoneSuccess(self, response, zoneName): """ Success callback for deleteZonesAndRecords deferred object. Triggered if there are no errors when calling the api Args: response (None): Upon success, the delete method returns None zoneName (str): The zone name """ print 'Successfully Deleted Zone: {}'.format(zoneName) def _deleteZoneFailure(self, failure, zoneName): """ Error callback for deleteZonesAndRecords deferred object. Triggered if there are errors when calling the api Args: failure (twisted.python.failure) zoneName (str): The zone name """ print '{}: {}'.format(zoneName, failure.getErrorMessage()) def _importZoneData(self): """ The parent method that triggers all of the callback chains for importing zone data. Loops through all of the zone data and creates a deferred object for every zone and adds Success and Error callbacks for each deferred instance. Tracks all of the deferreds by appending them to a deferred list. If no errors occur, the success callback is triggered. If even a single error occurs, the fireOnOneErrback parameter is set to true and the error callback is triggered and the program exits. NOTE: Auth exceptions are not explicitely checked since if an nsone.rest.errors.AuthException is raised, the deferred list error back will fire and the program will exit Returns: defer.DeferredList """ dl = [] for zoneName, records in self.data: zone = self._createZone(zoneName) zone.addCallback(self._createZoneSuccess, zoneName, records, self.nsoneObj) zone.addErrback(self._createZoneFailure, zoneName, records, self.nsoneObj) dl.append(zone) return defer.DeferredList(dl, fireOnOneErrback=True) @defer.inlineCallbacks def _createZone(self, zoneName): """ Returns the result of the deferred zone api call when available. Args: zoneName (str): The zone name in the data dictionary Returns: nsone.zones.Zone """ zone = yield self.nsoneObj.createZone(zoneName) defer.returnValue(zone) def _createZoneSuccess(self, response, zoneName, records, nsoneObj): """ Success callback for _createZone deferred object. Triggered if there are no errors when calling the api. If a zone is created successfully, then the next logical step during the import process is to add all of the records for that zone This method calls _createRecords which creates deferred objects for all of the records in the record list and adde both success and error callbacks for each instance. Each deferred record is tracked with the DeferredList object and the error back is fired as soon as one error back is fired Args: response (nsone.zones.Zone): The zone returned from createZone zoneName (str): The zone name records (list): The list of records belonging to the zone nsoneObj (nsone.NSONE): Instance of the nsone object Returns: defer.DeferredList """ return self._createRecords(response, zoneName, records, nsoneObj) @defer.inlineCallbacks def _createZoneFailure(self, failure, zoneName, records, nsoneObj): """ Failure Error callback if a zone cannot be created If a zone fails to be created, it is likely due to the fact that it exists already. The next logical step is to try to load the existing zone. A deferred zone is created with the _loadZone method. If a zone is loaded successfully, the _loadZoneSuccess callback is fired. Othewise, the error callback is fired A deferred zone object is yielded and the response from _loadZone is returned to the generator when it is available. Args: failure (twisted.python.failure) zoneName (str): The zone name records (list): The list of records belonging to the zone nsoneObj (nsone.NSONE): Instance of the nsone object Yields: twisted.internet.defer """ f = failure.trap(ResourceException) print failure.getErrorMessage() zone = self._loadZone(zoneName, nsoneObj) zone.addCallback(self._loadZoneSuccess, zoneName, records, nsoneObj) zone.addErrback(self._loadZoneFailure, zoneName) yield zone @defer.inlineCallbacks def _loadZone(self, zoneName, nsoneObj): """ Gets the result of the deferred load zone api call when available. Args: zoneName (str): The zone name in the data dictionary nsoneObj (nsone.NSONE): Instance of the nsone object Returns: nsone.zones.Zone """ zone = yield nsoneObj.loadZone(zoneName) defer.returnValue(zone) def _loadZoneSuccess(self, response, zoneName, records, nsoneObj): """ Success callback for _loadZone deferred object. Triggered if there are no errors when calling the api. If a zone is loaded successfully, then the next logical step during the import process is to add all of the records for that zone similar to creating a zone This method calls a helper method that creates deferred objects for all of the records in the record list and addes both success and error callbacks for each instance. Each deferred record is tracked with the DeferredList object and the error back is fired as soon as one error back is fired Args: response (nsone.zones.Zone): The zone returned from createZone zoneName (str): The zone name records (list): The list of records belonging to the zone nsoneObj (nsone.NSONE): Instance of the nsone object Returns: defer.DeferredList """ print 'Successfully Loaded Zone: {}'.format(zoneName) return self._createRecords(response, zoneName, records, nsoneObj) def _loadZoneFailure(self, failure, zoneName): """ Triggered when a zone cannot be loaded Prints the error message from the failure Args: failure (twisted.python.failure): zoneName (str): The zone name """ print '{}: {}'.format(zoneName, failure.getErrorMessage()) def _createRecords(self, response, zoneName, records, nsoneObj): """ This method reates deferred objects for all of the records in the record list and addes both success and error callbacks for each instance. Records are logically created either after creating a zone or loading a zone successfully which is why this functionality is modularized into this function to remove duplicate logic. Each deferred record is tracked with the DeferredList object and the error back is fired as soon as one error back is fired Args: response (nsone.zones.Zone): The zone returned from createZone zoneName (str): The zone name records (list): The list of records belonging to the zone nsoneObj (nsone.NSONE): Instance of the nsone object Returns: defer.DeferredList """ dl = [] zone = response for rec in records: answers = rec['Data'].split() methodName = 'add_{}'.format(rec['Type']) addMethod = getattr(zone, methodName) record = self._createRecord(addMethod, zoneName, [answers], rec['TTL']) record.addCallback(self._createRecordSuccess) record.addErrback(self._createRecordFailure, zoneName, rec['Type'], answers, nsoneObj) dl.append(record) return defer.DeferredList(dl, fireOnOneErrback=True) @defer.inlineCallbacks def _createRecord(self, addMethod, zoneName, answers, ttl): """ Calls the add_X method on zones for creating records and returns the value when it's available Args: addMethod (function) : add_X method of the zone where X is dynamic zoneName (str): The zone name from the data dict answers (list): The answers for the record in list form ttl (str): The TTL value for the record Return: nsone.records.Record """ record = yield addMethod(zoneName, answers, ttl=ttl) defer.returnValue(record) def _createRecordSuccess(self, response): """ Triggered when a record is created successfully If a record is created successfully, an nsone record instance is returned Args: response (nsone.records.Record): an instance of an nsone record object """ print 'Created record: {}'.format(response) @defer.inlineCallbacks def _createRecordFailure(self, failure, zoneName, recType, answers, nsoneObj): """ Triggered when a record cannot be created. Logically if a record can't be created, an attempt at loading it is made. A deferred record object is created and both success and error callbacks are chained onto it. Args: failure (twisted.python.failure): the failure object Yields: nsone.records.Record """ f = failure.trap(ResourceException) if f == ResourceException: record = self._loadRecord(zoneName, recType, nsoneObj) record.addCallback(self._loadRecordSuccess, answers) record.addErrback(self._loadRecordFailure) yield record @defer.inlineCallbacks def _loadRecord(self, zoneName, recType, nsoneObj): """ Calls the loadRecord method on nsoneObj returns the value of the record when it's available Args: zoneName (str): The zone name from the data dict recType (str): The record type nsoneObj (nsone.NSONE): Instance of the nsone object Return: nsone.records.Record """ record = yield nsoneObj.loadRecord(zoneName, recType, zoneName) defer.returnValue(record) @defer.inlineCallbacks def _loadRecordSuccess(self, response, answers): """ Triggered when a record is successfully loaded Logically if a record is loaded the next step would be to try to add answers to it. A deferred object is yielded and the response from the addAnswers method on the record is returned to the generator when available. The response from the method triggers either the succes or error callback on the deferred object. Args: response (nsone.records.Record): the record instance answers (list): The answers to be added to the record Yields: twisted.internet.defer """ print 'Successfully loaded Record: {}'.format(response) record = response addRecordAnswersRes = self._addRecordAnswers(record, answers) addRecordAnswersRes.addCallback(self._addRecordAnswersSuccess, answers) addRecordAnswersRes.addErrback(self._addRecordAnswersFailure) yield addRecordAnswersRes def _loadRecordFailure(self, failure): """ Prints the failure message if a record fails to load Args: failure (twisted.python.failure): The failure object """ print failure.getErrorMessage() @defer.inlineCallbacks def _addRecordAnswers(self, record, answers): """ Calls the addAnswers method on the nsone.records.Record object If the record answers already contain the answers passed into this method, then nothing is done. Otherwise, the addAnswers method is called and the answers are added to the record Returns a deferred object with the response from the addAnswers method on the record object when it is available or returns None if the record answers intersect with what's passed in here. Args: record (nsone.records.Record): The nsone record object answers (list): The record answers Yields: None """ recordData = yield record.data recordAnswers = {answer['answer'][0] for answer in recordData['answers']} if not recordAnswers.intersection(answers): print 'Adding answer: {}'.format(answers) yield record.addAnswers(answers) else: print 'Answer already exists: {}'.format(answers[0]) def _addRecordAnswersSuccess(self, response, answers): """ Prints a success message to show that the answers were added successfully. Args: response (None): the addAnswers method on the record returns None on success answers (list): The record answers """ print 'Successfully processed answers: {}'.format(answers) def _addRecordAnswersFailure(self, failure): """ Prints a failure message to show that the answers weren't added Args: failure (twisted.python.failure): the twisted failure object """ print failure.getErrorMessage() def _startRequests(self, reactor): """ This method initializes either the zone data import or deletion. All of the api requests are triggered here Args: reactor (twisted.internet.reactor) Return: function """ if self.deleteData: return self._deleteZoneData() return self._importZoneData() def run(self): """ Schedules the startRequests method and gracefully exits the program when either all of the deferred objects fire successfully or fail """ task.react(self._startRequests)