| @ -0,0 +1,447 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| from collections import defaultdict | |||
| from requests import Session | |||
| from base64 import b64encode | |||
| from ipaddress import ip_address | |||
| import hashlib | |||
| import hmac | |||
| import logging | |||
| import time | |||
| from ..record import Record | |||
| from .base import BaseProvider | |||
| class ConstellixClientException(Exception): | |||
| pass | |||
| class ConstellixClientBadRequest(ConstellixClientException): | |||
| def __init__(self, resp): | |||
| errors = resp.json()['errors'] | |||
| super(ConstellixClientBadRequest, self).__init__( | |||
| '\n - {}'.format('\n - '.join(errors))) | |||
| class ConstellixClientUnauthorized(ConstellixClientException): | |||
| def __init__(self): | |||
| super(ConstellixClientUnauthorized, self).__init__('Unauthorized') | |||
| class ConstellixClientNotFound(ConstellixClientException): | |||
| def __init__(self): | |||
| super(ConstellixClientNotFound, self).__init__('Not Found') | |||
| class ConstellixClient(object): | |||
| BASE = 'https://api.dns.constellix.com/v1/domains' | |||
| def __init__(self, api_key, secret_key, ratelimit_delay=0.0): | |||
| self.api_key = api_key | |||
| self.secret_key = secret_key | |||
| self.ratelimit_delay = ratelimit_delay | |||
| self._sess = Session() | |||
| self._sess.headers.update({'x-cnsdns-apiKey': self.api_key}) | |||
| self._domains = None | |||
| def _current_time(self): | |||
| return str(int(time.time() * 1000)) | |||
| def _hmac_hash(self, now): | |||
| return hmac.new(self.secret_key.encode('utf-8'), now.encode('utf-8'), | |||
| digestmod=hashlib.sha1).digest() | |||
| def _request(self, method, path, params=None, data=None): | |||
| now = self._current_time() | |||
| hmac_hash = self._hmac_hash(now) | |||
| headers = { | |||
| 'x-cnsdns-hmac': b64encode(hmac_hash), | |||
| 'x-cnsdns-requestDate': now | |||
| } | |||
| url = '{}{}'.format(self.BASE, path) | |||
| resp = self._sess.request(method, url, headers=headers, | |||
| params=params, json=data) | |||
| if resp.status_code == 400: | |||
| raise ConstellixClientBadRequest(resp) | |||
| if resp.status_code == 401: | |||
| raise ConstellixClientUnauthorized() | |||
| if resp.status_code == 404: | |||
| raise ConstellixClientNotFound() | |||
| resp.raise_for_status() | |||
| time.sleep(self.ratelimit_delay) | |||
| return resp | |||
| @property | |||
| def domains(self): | |||
| if self._domains is None: | |||
| zones = [] | |||
| resp = self._request('GET', '/').json() | |||
| zones += resp | |||
| self._domains = {'{}.'.format(z['name']): z['id'] for z in zones} | |||
| return self._domains | |||
| def domain(self, name): | |||
| path = '/{}'.format(self.domains.get(name)) | |||
| return self._request('GET', path).json() | |||
| def domain_create(self, name): | |||
| self._request('POST', '/', data={'names': [name]}) | |||
| def _absolutize_value(self, value, zone_name): | |||
| if value == '': | |||
| value = zone_name | |||
| elif not value.endswith('.'): | |||
| value = '{}.{}'.format(value, zone_name) | |||
| return value | |||
| def records(self, zone_name): | |||
| zone_id = self.domains.get(zone_name, False) | |||
| path = '/{}/records'.format(zone_id) | |||
| resp = self._request('GET', path).json() | |||
| for record in resp: | |||
| # change ANAME records to ALIAS | |||
| if record['type'] == 'ANAME': | |||
| record['type'] = 'ALIAS' | |||
| # change relative values to absolute | |||
| value = record['value'] | |||
| if record['type'] in ['ALIAS', 'CNAME', 'MX', 'NS', 'SRV']: | |||
| if isinstance(value, unicode): | |||
| record['value'] = self._absolutize_value(value, | |||
| zone_name) | |||
| if isinstance(value, list): | |||
| for v in value: | |||
| v['value'] = self._absolutize_value(v['value'], | |||
| zone_name) | |||
| # compress IPv6 addresses | |||
| if record['type'] == 'AAAA': | |||
| for i, v in enumerate(value): | |||
| value[i] = str(ip_address(v)) | |||
| return resp | |||
| def record_create(self, zone_name, record_type, params): | |||
| # change ALIAS records to ANAME | |||
| if record_type == 'ALIAS': | |||
| record_type = 'ANAME' | |||
| zone_id = self.domains.get(zone_name, False) | |||
| path = '/{}/records/{}'.format(zone_id, record_type) | |||
| self._request('POST', path, data=params) | |||
| def record_delete(self, zone_name, record_type, record_id): | |||
| zone_id = self.domains.get(zone_name, False) | |||
| path = '/{}/records/{}/{}'.format(zone_id, record_type, record_id) | |||
| self._request('DELETE', path) | |||
| class ConstellixProvider(BaseProvider): | |||
| ''' | |||
| Constellix DNS provider | |||
| constellix: | |||
| class: octodns.provider.constellix.ConstellixProvider | |||
| # Your Contellix api key (required) | |||
| api_key: env/CONSTELLIX_API_KEY | |||
| # Your Constellix secret key (required) | |||
| secret_key: env/CONSTELLIX_SECRET_KEY | |||
| # Amount of time to wait between requests to avoid | |||
| # ratelimit (optional) | |||
| ratelimit_delay: 0.0 | |||
| ''' | |||
| SUPPORTS_GEO = False | |||
| SUPPORTS_DYNAMIC = False | |||
| SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', | |||
| 'NS', 'PTR', 'SPF', 'SRV', 'TXT')) | |||
| def __init__(self, id, api_key, secret_key, ratelimit_delay=0.0, | |||
| *args, **kwargs): | |||
| self.log = logging.getLogger('ConstellixProvider[{}]'.format(id)) | |||
| self.log.debug('__init__: id=%s, api_key=***, secret_key=***', id) | |||
| super(ConstellixProvider, self).__init__(id, *args, **kwargs) | |||
| self._client = ConstellixClient(api_key, secret_key, ratelimit_delay) | |||
| self._zone_records = {} | |||
| def _data_for_multiple(self, _type, records): | |||
| record = records[0] | |||
| return { | |||
| 'ttl': record['ttl'], | |||
| 'type': _type, | |||
| 'values': record['value'] | |||
| } | |||
| _data_for_A = _data_for_multiple | |||
| _data_for_AAAA = _data_for_multiple | |||
| def _data_for_CAA(self, _type, records): | |||
| values = [] | |||
| record = records[0] | |||
| for value in record['value']: | |||
| values.append({ | |||
| 'flags': value['flag'], | |||
| 'tag': value['tag'], | |||
| 'value': value['data'] | |||
| }) | |||
| return { | |||
| 'ttl': records[0]['ttl'], | |||
| 'type': _type, | |||
| 'values': values | |||
| } | |||
| def _data_for_NS(self, _type, records): | |||
| record = records[0] | |||
| return { | |||
| 'ttl': record['ttl'], | |||
| 'type': _type, | |||
| 'values': [value['value'] for value in record['value']] | |||
| } | |||
| def _data_for_ALIAS(self, _type, records): | |||
| record = records[0] | |||
| return { | |||
| 'ttl': record['ttl'], | |||
| 'type': _type, | |||
| 'value': record['value'][0]['value'] | |||
| } | |||
| _data_for_PTR = _data_for_ALIAS | |||
| def _data_for_TXT(self, _type, records): | |||
| values = [value['value'].replace(';', '\\;') | |||
| for value in records[0]['value']] | |||
| return { | |||
| 'ttl': records[0]['ttl'], | |||
| 'type': _type, | |||
| 'values': values | |||
| } | |||
| _data_for_SPF = _data_for_TXT | |||
| def _data_for_MX(self, _type, records): | |||
| values = [] | |||
| record = records[0] | |||
| for value in record['value']: | |||
| values.append({ | |||
| 'preference': value['level'], | |||
| 'exchange': value['value'] | |||
| }) | |||
| return { | |||
| 'ttl': records[0]['ttl'], | |||
| 'type': _type, | |||
| 'values': values | |||
| } | |||
| def _data_for_single(self, _type, records): | |||
| record = records[0] | |||
| return { | |||
| 'ttl': record['ttl'], | |||
| 'type': _type, | |||
| 'value': record['value'] | |||
| } | |||
| _data_for_CNAME = _data_for_single | |||
| def _data_for_SRV(self, _type, records): | |||
| values = [] | |||
| record = records[0] | |||
| for value in record['value']: | |||
| values.append({ | |||
| 'port': value['port'], | |||
| 'priority': value['priority'], | |||
| 'target': value['value'], | |||
| 'weight': value['weight'] | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': records[0]['ttl'], | |||
| 'values': values | |||
| } | |||
| def zone_records(self, zone): | |||
| if zone.name not in self._zone_records: | |||
| try: | |||
| self._zone_records[zone.name] = \ | |||
| self._client.records(zone.name) | |||
| except ConstellixClientNotFound: | |||
| return [] | |||
| return self._zone_records[zone.name] | |||
| def populate(self, zone, target=False, lenient=False): | |||
| self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, | |||
| target, lenient) | |||
| values = defaultdict(lambda: defaultdict(list)) | |||
| for record in self.zone_records(zone): | |||
| _type = record['type'] | |||
| if _type not in self.SUPPORTS: | |||
| self.log.warning('populate: skipping unsupported %s record', | |||
| _type) | |||
| continue | |||
| values[record['name']][record['type']].append(record) | |||
| before = len(zone.records) | |||
| for name, types in values.items(): | |||
| for _type, records in types.items(): | |||
| data_for = getattr(self, '_data_for_{}'.format(_type)) | |||
| record = Record.new(zone, name, data_for(_type, records), | |||
| source=self, lenient=lenient) | |||
| zone.add_record(record, lenient=lenient) | |||
| exists = zone.name in self._zone_records | |||
| self.log.info('populate: found %s records, exists=%s', | |||
| len(zone.records) - before, exists) | |||
| return exists | |||
| def _params_for_multiple(self, record): | |||
| yield { | |||
| 'name': record.name, | |||
| 'ttl': record.ttl, | |||
| 'roundRobin': [{ | |||
| 'value': value | |||
| } for value in record.values] | |||
| } | |||
| _params_for_A = _params_for_multiple | |||
| _params_for_AAAA = _params_for_multiple | |||
| # An A record with this name must exist in this domain for | |||
| # this NS record to be valid. Need to handle checking if | |||
| # there is an A record before creating NS | |||
| _params_for_NS = _params_for_multiple | |||
| def _params_for_single(self, record): | |||
| yield { | |||
| 'name': record.name, | |||
| 'ttl': record.ttl, | |||
| 'host': record.value, | |||
| } | |||
| _params_for_CNAME = _params_for_single | |||
| def _params_for_ALIAS(self, record): | |||
| yield { | |||
| 'name': record.name, | |||
| 'ttl': record.ttl, | |||
| 'roundRobin': [{ | |||
| 'value': record.value, | |||
| 'disableFlag': False | |||
| }] | |||
| } | |||
| _params_for_PTR = _params_for_ALIAS | |||
| def _params_for_MX(self, record): | |||
| values = [] | |||
| for value in record.values: | |||
| values.append({ | |||
| 'value': value.exchange, | |||
| 'level': value.preference | |||
| }) | |||
| yield { | |||
| 'value': value.exchange, | |||
| 'name': record.name, | |||
| 'ttl': record.ttl, | |||
| 'roundRobin': values | |||
| } | |||
| def _params_for_SRV(self, record): | |||
| values = [] | |||
| for value in record.values: | |||
| values.append({ | |||
| 'value': value.target, | |||
| 'priority': value.priority, | |||
| 'weight': value.weight, | |||
| 'port': value.port | |||
| }) | |||
| for value in record.values: | |||
| yield { | |||
| 'name': record.name, | |||
| 'ttl': record.ttl, | |||
| 'roundRobin': values | |||
| } | |||
| def _params_for_TXT(self, record): | |||
| # Constellix does not want values escaped | |||
| values = [] | |||
| for value in record.chunked_values: | |||
| values.append({ | |||
| 'value': value.replace('\\;', ';') | |||
| }) | |||
| yield { | |||
| 'name': record.name, | |||
| 'ttl': record.ttl, | |||
| 'roundRobin': values | |||
| } | |||
| _params_for_SPF = _params_for_TXT | |||
| def _params_for_CAA(self, record): | |||
| values = [] | |||
| for value in record.values: | |||
| values.append({ | |||
| 'tag': value.tag, | |||
| 'data': value.value, | |||
| 'flag': value.flags, | |||
| }) | |||
| yield { | |||
| 'name': record.name, | |||
| 'ttl': record.ttl, | |||
| 'roundRobin': values | |||
| } | |||
| def _apply_Create(self, change): | |||
| new = change.new | |||
| params_for = getattr(self, '_params_for_{}'.format(new._type)) | |||
| for params in params_for(new): | |||
| self._client.record_create(new.zone.name, new._type, params) | |||
| def _apply_Update(self, change): | |||
| self._apply_Delete(change) | |||
| self._apply_Create(change) | |||
| def _apply_Delete(self, change): | |||
| existing = change.existing | |||
| zone = existing.zone | |||
| for record in self.zone_records(zone): | |||
| if existing.name == record['name'] and \ | |||
| existing._type == record['type']: | |||
| self._client.record_delete(zone.name, record['type'], | |||
| record['id']) | |||
| def _apply(self, plan): | |||
| desired = plan.desired | |||
| changes = plan.changes | |||
| self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, | |||
| len(changes)) | |||
| try: | |||
| self._client.domain(desired.name) | |||
| except ConstellixClientNotFound: | |||
| self.log.debug('_apply: no matching zone, creating domain') | |||
| self._client.domain_create(desired.name[:-1]) | |||
| for change in changes: | |||
| class_name = change.__class__.__name__ | |||
| getattr(self, '_apply_{}'.format(class_name))(change) | |||
| # Clear out the cache if any | |||
| self._zone_records.pop(desired.name, None) | |||
| @ -0,0 +1,525 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| from requests import Session | |||
| from akamai.edgegrid import EdgeGridAuth | |||
| from urlparse import urljoin | |||
| from collections import defaultdict | |||
| from logging import getLogger | |||
| from ..record import Record | |||
| from .base import BaseProvider | |||
| class AkamaiClientNotFound(Exception): | |||
| def __init__(self, resp): | |||
| message = "404: Resource not found" | |||
| super(AkamaiClientNotFound, self).__init__(message) | |||
| class AkamaiClient(object): | |||
| ''' | |||
| Client for making calls to Akamai Fast DNS API using Python Requests | |||
| Fast DNS Zone Management API V2, found here: | |||
| developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html | |||
| Info on Python Requests library: | |||
| https://2.python-requests.org/en/master/ | |||
| ''' | |||
| def __init__(self, client_secret, host, access_token, client_token): | |||
| self.base = "https://" + host + "/config-dns/v2/" | |||
| sess = Session() | |||
| sess.auth = EdgeGridAuth( | |||
| client_token=client_token, | |||
| client_secret=client_secret, | |||
| access_token=access_token | |||
| ) | |||
| self._sess = sess | |||
| def _request(self, method, path, params=None, data=None, v1=False): | |||
| url = urljoin(self.base, path) | |||
| resp = self._sess.request(method, url, params=params, json=data) | |||
| if resp.status_code == 404: | |||
| raise AkamaiClientNotFound(resp) | |||
| resp.raise_for_status() | |||
| return resp | |||
| def record_create(self, zone, name, record_type, content): | |||
| path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type) | |||
| result = self._request('POST', path, data=content) | |||
| return result | |||
| def record_delete(self, zone, name, record_type): | |||
| path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type) | |||
| result = self._request('DELETE', path) | |||
| return result | |||
| def record_replace(self, zone, name, record_type, content): | |||
| path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type) | |||
| result = self._request('PUT', path, data=content) | |||
| return result | |||
| def zone_get(self, zone): | |||
| path = 'zones/{}'.format(zone) | |||
| result = self._request('GET', path) | |||
| return result | |||
| def zone_create(self, contractId, params, gid=None): | |||
| path = 'zones?contractId={}'.format(contractId) | |||
| if gid is not None: | |||
| path += '&gid={}'.format(gid) | |||
| result = self._request('POST', path, data=params) | |||
| return result | |||
| def zone_recordset_get(self, zone, page=None, pageSize=None, search=None, | |||
| showAll="true", sortBy="name", types=None): | |||
| params = { | |||
| 'page': page, | |||
| 'pageSize': pageSize, | |||
| 'search': search, | |||
| 'showAll': showAll, | |||
| 'sortBy': sortBy, | |||
| 'types': types | |||
| } | |||
| path = 'zones/{}/recordsets'.format(zone) | |||
| result = self._request('GET', path, params=params) | |||
| return result | |||
| class AkamaiProvider(BaseProvider): | |||
| ''' | |||
| Akamai Fast DNS Provider | |||
| fastdns.py: | |||
| Example config file with variables: | |||
| " | |||
| --- | |||
| providers: | |||
| config: | |||
| class: octodns.provider.yaml.YamlProvider | |||
| directory: ./config (example path to directory of zone files) | |||
| fastdns: | |||
| class: octodns.provider.fastdns.AkamaiProvider | |||
| client_secret: env/AKAMAI_CLIENT_SECRET | |||
| host: env/AKAMAI_HOST | |||
| access_token: env/AKAMAI_ACCESS_TOKEN | |||
| client_token: env/AKAMAI_CLIENT_TOKEN | |||
| contract_id: env/AKAMAI_CONTRACT_ID (optional) | |||
| zones: | |||
| example.com.: | |||
| sources: | |||
| - config | |||
| targets: | |||
| - fastdns | |||
| " | |||
| The first four variables above can be hidden in environment variables | |||
| and octoDNS will automatically search for them in the shell. It is | |||
| possible to also hard-code into the config file: eg, contract_id. | |||
| The first four values can be found by generating credentials: | |||
| https://control.akamai.com/ | |||
| Configure > Organization > Manage APIs > New API Client for me | |||
| Select appropriate group, and fill relevant fields. | |||
| For API Service Name, select DNS-Zone Record Management | |||
| and then set appropriate Access level (Read-Write to make changes). | |||
| Then select the "New Credential" button to generate values for above | |||
| The contract_id paramater is optional, and only required for creating | |||
| a new zone. If the zone being managed already exists in Akamai for the | |||
| user in question, then this paramater is not needed. | |||
| ''' | |||
| SUPPORTS_GEO = False | |||
| SUPPORTS_DYNAMIC = False | |||
| SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SPF', | |||
| 'SRV', 'SSHFP', 'TXT')) | |||
| def __init__(self, id, client_secret, host, access_token, client_token, | |||
| contract_id=None, gid=None, *args, **kwargs): | |||
| self.log = getLogger('AkamaiProvider[{}]'.format(id)) | |||
| self.log.debug('__init__: id=%s, ') | |||
| super(AkamaiProvider, self).__init__(id, *args, **kwargs) | |||
| self._dns_client = AkamaiClient(client_secret, host, access_token, | |||
| client_token) | |||
| self._zone_records = {} | |||
| self._contractId = contract_id | |||
| self._gid = gid | |||
| def zone_records(self, zone): | |||
| """ returns records for a zone, looks for it if not present, or | |||
| returns empty [] if can't find a match | |||
| """ | |||
| if zone.name not in self._zone_records: | |||
| try: | |||
| name = zone.name[:-1] | |||
| response = self._dns_client.zone_recordset_get(name) | |||
| self._zone_records[zone.name] = response.json()["recordsets"] | |||
| except (AkamaiClientNotFound, KeyError): | |||
| return [] | |||
| return self._zone_records[zone.name] | |||
| def populate(self, zone, target=False, lenient=False): | |||
| self.log.debug('populate: name=%s', zone.name) | |||
| values = defaultdict(lambda: defaultdict(list)) | |||
| for record in self.zone_records(zone): | |||
| _type = record.get('type') | |||
| # Akamai sends down prefix.zonename., while octodns expects prefix | |||
| _name = record.get('name').split("." + zone.name[:-1], 1)[0] | |||
| if _name == zone.name[:-1]: | |||
| _name = '' # root / @ | |||
| if _type not in self.SUPPORTS: | |||
| continue | |||
| values[_name][_type].append(record) | |||
| before = len(zone.records) | |||
| for name, types in values.items(): | |||
| for _type, records in types.items(): | |||
| data_for = getattr(self, '_data_for_{}'.format(_type)) | |||
| record = Record.new(zone, name, data_for(_type, records[0]), | |||
| source=self, lenient=lenient) | |||
| zone.add_record(record, lenient=lenient) | |||
| exists = zone.name in self._zone_records | |||
| found = len(zone.records) - before | |||
| self.log.info('populate: found %s records, exists=%s', found, exists) | |||
| return exists | |||
| def _apply(self, plan): | |||
| desired = plan.desired | |||
| changes = plan.changes | |||
| self.log.debug('apply: zone=%s, chnges=%d', desired.name, len(changes)) | |||
| zone_name = desired.name[:-1] | |||
| try: | |||
| self._dns_client.zone_get(zone_name) | |||
| except AkamaiClientNotFound: | |||
| self.log.info("zone not found, creating zone") | |||
| params = self._build_zone_config(zone_name) | |||
| self._dns_client.zone_create(self._contractId, params, self._gid) | |||
| for change in changes: | |||
| class_name = change.__class__.__name__ | |||
| getattr(self, '_apply_{}'.format(class_name))(change) | |||
| # Clear out the cache if any | |||
| self._zone_records.pop(desired.name, None) | |||
| def _apply_Create(self, change): | |||
| new = change.new | |||
| record_type = new._type | |||
| params_for = getattr(self, '_params_for_{}'.format(record_type)) | |||
| values = self._get_values(new.data) | |||
| rdata = params_for(values) | |||
| zone = new.zone.name[:-1] | |||
| name = self._set_full_name(new.name, zone) | |||
| content = { | |||
| "name": name, | |||
| "type": record_type, | |||
| "ttl": new.ttl, | |||
| "rdata": rdata | |||
| } | |||
| self._dns_client.record_create(zone, name, record_type, content) | |||
| return | |||
| def _apply_Delete(self, change): | |||
| zone = change.existing.zone.name[:-1] | |||
| name = self._set_full_name(change.existing.name, zone) | |||
| record_type = change.existing._type | |||
| self._dns_client.record_delete(zone, name, record_type) | |||
| return | |||
| def _apply_Update(self, change): | |||
| new = change.new | |||
| record_type = new._type | |||
| params_for = getattr(self, '_params_for_{}'.format(record_type)) | |||
| values = self._get_values(new.data) | |||
| rdata = params_for(values) | |||
| zone = new.zone.name[:-1] | |||
| name = self._set_full_name(new.name, zone) | |||
| content = { | |||
| "name": name, | |||
| "type": record_type, | |||
| "ttl": new.ttl, | |||
| "rdata": rdata | |||
| } | |||
| self._dns_client.record_replace(zone, name, record_type, content) | |||
| return | |||
| def _data_for_multiple(self, _type, records): | |||
| return { | |||
| 'ttl': records['ttl'], | |||
| 'type': _type, | |||
| 'values': [r for r in records['rdata']] | |||
| } | |||
| _data_for_A = _data_for_multiple | |||
| _data_for_AAAA = _data_for_multiple | |||
| _data_for_NS = _data_for_multiple | |||
| _data_for_SPF = _data_for_multiple | |||
| def _data_for_CNAME(self, _type, records): | |||
| value = records['rdata'][0] | |||
| if (value[-1] != '.'): | |||
| value = '{}.'.format(value) | |||
| return { | |||
| 'ttl': records['ttl'], | |||
| 'type': _type, | |||
| 'value': value | |||
| } | |||
| def _data_for_MX(self, _type, records): | |||
| values = [] | |||
| for r in records['rdata']: | |||
| preference, exchange = r.split(" ", 1) | |||
| values.append({ | |||
| 'preference': preference, | |||
| 'exchange': exchange | |||
| }) | |||
| return { | |||
| 'ttl': records['ttl'], | |||
| 'type': _type, | |||
| 'values': values | |||
| } | |||
| def _data_for_NAPTR(self, _type, records): | |||
| values = [] | |||
| for r in records['rdata']: | |||
| order, preference, flags, service, regexp, repl = r.split(' ', 5) | |||
| values.append({ | |||
| 'flags': flags[1:-1], | |||
| 'order': order, | |||
| 'preference': preference, | |||
| 'regexp': regexp[1:-1], | |||
| 'replacement': repl, | |||
| 'service': service[1:-1] | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': records['ttl'], | |||
| 'values': values | |||
| } | |||
| def _data_for_PTR(self, _type, records): | |||
| return { | |||
| 'ttl': records['ttl'], | |||
| 'type': _type, | |||
| 'value': records['rdata'][0] | |||
| } | |||
| def _data_for_SRV(self, _type, records): | |||
| values = [] | |||
| for r in records['rdata']: | |||
| priority, weight, port, target = r.split(' ', 3) | |||
| values.append({ | |||
| 'port': port, | |||
| 'priority': priority, | |||
| 'target': target, | |||
| 'weight': weight | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': records['ttl'], | |||
| 'values': values | |||
| } | |||
| def _data_for_SSHFP(self, _type, records): | |||
| values = [] | |||
| for r in records['rdata']: | |||
| algorithm, fp_type, fingerprint = r.split(' ', 2) | |||
| values.append({ | |||
| 'algorithm': algorithm, | |||
| 'fingerprint': fingerprint.lower(), | |||
| 'fingerprint_type': fp_type | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': records['ttl'], | |||
| 'values': values | |||
| } | |||
| def _data_for_TXT(self, _type, records): | |||
| values = [] | |||
| for r in records['rdata']: | |||
| r = r[1:-1] | |||
| values.append(r.replace(';', '\\;')) | |||
| return { | |||
| 'ttl': records['ttl'], | |||
| 'type': _type, | |||
| 'values': values | |||
| } | |||
| def _params_for_multiple(self, values): | |||
| return [r for r in values] | |||
| def _params_for_single(self, values): | |||
| return values | |||
| _params_for_A = _params_for_multiple | |||
| _params_for_AAAA = _params_for_multiple | |||
| _params_for_NS = _params_for_multiple | |||
| _params_for_CNAME = _params_for_single | |||
| _params_for_PTR = _params_for_single | |||
| def _params_for_MX(self, values): | |||
| rdata = [] | |||
| for r in values: | |||
| preference = r['preference'] | |||
| exchange = r['exchange'] | |||
| record = '{} {}'.format(preference, exchange) | |||
| rdata.append(record) | |||
| return rdata | |||
| def _params_for_NAPTR(self, values): | |||
| rdata = [] | |||
| for r in values: | |||
| ordr = r['order'] | |||
| prf = r['preference'] | |||
| flg = "\"" + r['flags'] + "\"" | |||
| srvc = "\"" + r['service'] + "\"" | |||
| rgx = "\"" + r['regexp'] + "\"" | |||
| rpl = r['replacement'] | |||
| record = '{} {} {} {} {} {}'.format(ordr, prf, flg, srvc, rgx, rpl) | |||
| rdata.append(record) | |||
| return rdata | |||
| def _params_for_SPF(self, values): | |||
| rdata = [] | |||
| for r in values: | |||
| txt = "\"" + r.replace('\\;', ';') + "\"" | |||
| rdata.append(txt) | |||
| return rdata | |||
| def _params_for_SRV(self, values): | |||
| rdata = [] | |||
| for r in values: | |||
| priority = r['priority'] | |||
| weight = r['weight'] | |||
| port = r['port'] | |||
| target = r['target'] | |||
| record = '{} {} {} {}'.format(priority, weight, port, target) | |||
| rdata.append(record) | |||
| return rdata | |||
| def _params_for_SSHFP(self, values): | |||
| rdata = [] | |||
| for r in values: | |||
| algorithm = r['algorithm'] | |||
| fp_type = r['fingerprint_type'] | |||
| fp = r['fingerprint'] | |||
| record = '{} {} {}'.format(algorithm, fp_type, fp) | |||
| rdata.append(record) | |||
| return rdata | |||
| def _params_for_TXT(self, values): | |||
| rdata = [] | |||
| for r in values: | |||
| txt = "\"" + r.replace('\\;', ';') + "\"" | |||
| rdata.append(txt) | |||
| return rdata | |||
| def _build_zone_config(self, zone, _type="primary", comment=None, | |||
| masters=[]): | |||
| if self._contractId is None: | |||
| raise NameError("contractId not specified to create zone") | |||
| return { | |||
| "zone": zone, | |||
| "type": _type, | |||
| "comment": comment, | |||
| "masters": masters | |||
| } | |||
| def _get_values(self, data): | |||
| try: | |||
| vals = data['values'] | |||
| except KeyError: | |||
| vals = [data['value']] | |||
| return vals | |||
| def _set_full_name(self, name, zone): | |||
| name = name + '.' + zone | |||
| # octodns's name for root is '' | |||
| if (name[0] == '.'): | |||
| name = name[1:] | |||
| return name | |||
| @ -0,0 +1,305 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| from collections import defaultdict | |||
| from logging import getLogger | |||
| from requests import Session | |||
| from ..record import Record, Update | |||
| from .base import BaseProvider | |||
| class SelectelAuthenticationRequired(Exception): | |||
| def __init__(self, msg): | |||
| message = 'Authorization failed. Invalid or empty token.' | |||
| super(SelectelAuthenticationRequired, self).__init__(message) | |||
| class SelectelProvider(BaseProvider): | |||
| SUPPORTS_GEO = False | |||
| SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SPF', 'SRV')) | |||
| MIN_TTL = 60 | |||
| PAGINATION_LIMIT = 50 | |||
| API_URL = 'https://api.selectel.ru/domains/v1' | |||
| def __init__(self, id, token, *args, **kwargs): | |||
| self.log = getLogger('SelectelProvider[{}]'.format(id)) | |||
| self.log.debug('__init__: id=%s', id) | |||
| super(SelectelProvider, self).__init__(id, *args, **kwargs) | |||
| self._sess = Session() | |||
| self._sess.headers.update({ | |||
| 'X-Token': token, | |||
| 'Content-Type': 'application/json', | |||
| }) | |||
| self._zone_records = {} | |||
| self._domain_list = self.domain_list() | |||
| self._zones = None | |||
| def _request(self, method, path, params=None, data=None): | |||
| self.log.debug('_request: method=%s, path=%s', method, path) | |||
| url = '{}{}'.format(self.API_URL, path) | |||
| resp = self._sess.request(method, url, params=params, json=data) | |||
| self.log.debug('_request: status=%s', resp.status_code) | |||
| if resp.status_code == 401: | |||
| raise SelectelAuthenticationRequired(resp.text) | |||
| elif resp.status_code == 404: | |||
| return {} | |||
| resp.raise_for_status() | |||
| if method == 'DELETE': | |||
| return {} | |||
| return resp.json() | |||
| def _get_total_count(self, path): | |||
| url = '{}{}'.format(self.API_URL, path) | |||
| resp = self._sess.request('HEAD', url) | |||
| return int(resp.headers['X-Total-Count']) | |||
| def _request_with_pagination(self, path, total_count): | |||
| result = [] | |||
| for offset in range(0, total_count, self.PAGINATION_LIMIT): | |||
| result += self._request('GET', path, | |||
| params={'limit': self.PAGINATION_LIMIT, | |||
| 'offset': offset}) | |||
| return result | |||
| def _include_change(self, change): | |||
| if isinstance(change, Update): | |||
| existing = change.existing.data | |||
| new = change.new.data | |||
| new['ttl'] = max(self.MIN_TTL, new['ttl']) | |||
| if new == existing: | |||
| self.log.debug('_include_changes: new=%s, found existing=%s', | |||
| new, existing) | |||
| return False | |||
| return True | |||
| def _apply(self, plan): | |||
| desired = plan.desired | |||
| changes = plan.changes | |||
| self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, | |||
| len(changes)) | |||
| zone_name = desired.name[:-1] | |||
| for change in changes: | |||
| class_name = change.__class__.__name__ | |||
| getattr(self, '_apply_{}'.format(class_name).lower())(zone_name, | |||
| change) | |||
| def _apply_create(self, zone_name, change): | |||
| new = change.new | |||
| params_for = getattr(self, '_params_for_{}'.format(new._type)) | |||
| for params in params_for(new): | |||
| self.create_record(zone_name, params) | |||
| def _apply_update(self, zone_name, change): | |||
| self._apply_delete(zone_name, change) | |||
| self._apply_create(zone_name, change) | |||
| def _apply_delete(self, zone_name, change): | |||
| existing = change.existing | |||
| self.delete_record(zone_name, existing._type, existing.name) | |||
| def _params_for_multiple(self, record): | |||
| for value in record.values: | |||
| yield { | |||
| 'content': value, | |||
| 'name': record.fqdn, | |||
| 'ttl': max(self.MIN_TTL, record.ttl), | |||
| 'type': record._type, | |||
| } | |||
| def _params_for_single(self, record): | |||
| yield { | |||
| 'content': record.value, | |||
| 'name': record.fqdn, | |||
| 'ttl': max(self.MIN_TTL, record.ttl), | |||
| 'type': record._type | |||
| } | |||
| def _params_for_MX(self, record): | |||
| for value in record.values: | |||
| yield { | |||
| 'content': value.exchange, | |||
| 'name': record.fqdn, | |||
| 'ttl': max(self.MIN_TTL, record.ttl), | |||
| 'type': record._type, | |||
| 'priority': value.preference | |||
| } | |||
| def _params_for_SRV(self, record): | |||
| for value in record.values: | |||
| yield { | |||
| 'name': record.fqdn, | |||
| 'target': value.target, | |||
| 'ttl': max(self.MIN_TTL, record.ttl), | |||
| 'type': record._type, | |||
| 'port': value.port, | |||
| 'weight': value.weight, | |||
| 'priority': value.priority | |||
| } | |||
| _params_for_A = _params_for_multiple | |||
| _params_for_AAAA = _params_for_multiple | |||
| _params_for_NS = _params_for_multiple | |||
| _params_for_TXT = _params_for_multiple | |||
| _params_for_SPF = _params_for_multiple | |||
| _params_for_CNAME = _params_for_single | |||
| def _data_for_A(self, _type, records): | |||
| return { | |||
| 'ttl': records[0]['ttl'], | |||
| 'type': _type, | |||
| 'values': [r['content'] for r in records], | |||
| } | |||
| _data_for_AAAA = _data_for_A | |||
| def _data_for_NS(self, _type, records): | |||
| return { | |||
| 'ttl': records[0]['ttl'], | |||
| 'type': _type, | |||
| 'values': ['{}.'.format(r['content']) for r in records], | |||
| } | |||
| def _data_for_MX(self, _type, records): | |||
| values = [] | |||
| for record in records: | |||
| values.append({ | |||
| 'preference': record['priority'], | |||
| 'exchange': '{}.'.format(record['content']), | |||
| }) | |||
| return { | |||
| 'ttl': records[0]['ttl'], | |||
| 'type': _type, | |||
| 'values': values, | |||
| } | |||
| def _data_for_CNAME(self, _type, records): | |||
| only = records[0] | |||
| return { | |||
| 'ttl': only['ttl'], | |||
| 'type': _type, | |||
| 'value': '{}.'.format(only['content']) | |||
| } | |||
| def _data_for_TXT(self, _type, records): | |||
| return { | |||
| 'ttl': records[0]['ttl'], | |||
| 'type': _type, | |||
| 'values': [r['content'] for r in records], | |||
| } | |||
| def _data_for_SRV(self, _type, records): | |||
| values = [] | |||
| for record in records: | |||
| values.append({ | |||
| 'priority': record['priority'], | |||
| 'weight': record['weight'], | |||
| 'port': record['port'], | |||
| 'target': '{}.'.format(record['target']), | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': records[0]['ttl'], | |||
| 'values': values, | |||
| } | |||
| def populate(self, zone, target=False, lenient=False): | |||
| self.log.debug('populate: name=%s, target=%s, lenient=%s', | |||
| zone.name, target, lenient) | |||
| before = len(zone.records) | |||
| records = self.zone_records(zone) | |||
| if records: | |||
| values = defaultdict(lambda: defaultdict(list)) | |||
| for record in records: | |||
| name = zone.hostname_from_fqdn(record['name']) | |||
| _type = record['type'] | |||
| if _type in self.SUPPORTS: | |||
| values[name][record['type']].append(record) | |||
| for name, types in values.items(): | |||
| for _type, records in types.items(): | |||
| data_for = getattr(self, '_data_for_{}'.format(_type)) | |||
| data = data_for(_type, records) | |||
| record = Record.new(zone, name, data, source=self, | |||
| lenient=lenient) | |||
| zone.add_record(record) | |||
| self.log.info('populate: found %s records', | |||
| len(zone.records) - before) | |||
| def domain_list(self): | |||
| path = '/' | |||
| domains = {} | |||
| domains_list = [] | |||
| total_count = self._get_total_count(path) | |||
| domains_list = self._request_with_pagination(path, total_count) | |||
| for domain in domains_list: | |||
| domains[domain['name']] = domain | |||
| return domains | |||
| def zone_records(self, zone): | |||
| path = '/{}/records/'.format(zone.name[:-1]) | |||
| zone_records = [] | |||
| total_count = self._get_total_count(path) | |||
| zone_records = self._request_with_pagination(path, total_count) | |||
| self._zone_records[zone.name] = zone_records | |||
| return self._zone_records[zone.name] | |||
| def create_domain(self, name, zone=""): | |||
| path = '/' | |||
| data = { | |||
| 'name': name, | |||
| 'bind_zone': zone, | |||
| } | |||
| resp = self._request('POST', path, data=data) | |||
| self._domain_list[name] = resp | |||
| return resp | |||
| def create_record(self, zone_name, data): | |||
| self.log.debug('Create record. Zone: %s, data %s', zone_name, data) | |||
| if zone_name in self._domain_list.keys(): | |||
| domain_id = self._domain_list[zone_name]['id'] | |||
| else: | |||
| domain_id = self.create_domain(zone_name)['id'] | |||
| path = '/{}/records/'.format(domain_id) | |||
| return self._request('POST', path, data=data) | |||
| def delete_record(self, domain, _type, zone): | |||
| self.log.debug('Delete record. Domain: %s, Type: %s', domain, _type) | |||
| domain_id = self._domain_list[domain]['id'] | |||
| records = self._zone_records.get('{}.'.format(domain), False) | |||
| if not records: | |||
| path = '/{}/records/'.format(domain_id) | |||
| records = self._request('GET', path) | |||
| for record in records: | |||
| full_domain = domain | |||
| if zone: | |||
| full_domain = '{}{}'.format(zone, domain) | |||
| if record['type'] == _type and record['name'] == full_domain: | |||
| path = '/{}/records/{}'.format(domain_id, record['id']) | |||
| return self._request('DELETE', path) | |||
| self.log.debug('Delete record failed (Record not found)') | |||
| @ -0,0 +1,353 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| from suds import WebFault | |||
| from collections import defaultdict | |||
| from .base import BaseProvider | |||
| from logging import getLogger | |||
| from ..record import Record | |||
| from transip.service.domain import DomainService | |||
| from transip.service.objects import DnsEntry | |||
| class TransipException(Exception): | |||
| pass | |||
| class TransipConfigException(TransipException): | |||
| pass | |||
| class TransipNewZoneException(TransipException): | |||
| pass | |||
| class TransipProvider(BaseProvider): | |||
| ''' | |||
| Transip DNS provider | |||
| transip: | |||
| class: octodns.provider.transip.TransipProvider | |||
| # Your Transip account name (required) | |||
| account: yourname | |||
| # Path to a private key file (required if key is not used) | |||
| key_file: /path/to/file | |||
| # The api key as string (required if key_file is not used) | |||
| key: | | |||
| \''' | |||
| -----BEGIN PRIVATE KEY----- | |||
| ... | |||
| -----END PRIVATE KEY----- | |||
| \''' | |||
| # if both `key_file` and `key` are presented `key_file` is used | |||
| ''' | |||
| SUPPORTS_GEO = False | |||
| SUPPORTS_DYNAMIC = False | |||
| SUPPORTS = set( | |||
| ('A', 'AAAA', 'CNAME', 'MX', 'SRV', 'SPF', 'TXT', 'SSHFP', 'CAA')) | |||
| # unsupported by OctoDNS: 'TLSA' | |||
| MIN_TTL = 120 | |||
| TIMEOUT = 15 | |||
| ROOT_RECORD = '@' | |||
| def __init__(self, id, account, key=None, key_file=None, *args, **kwargs): | |||
| self.log = getLogger('TransipProvider[{}]'.format(id)) | |||
| self.log.debug('__init__: id=%s, account=%s, token=***', id, | |||
| account) | |||
| super(TransipProvider, self).__init__(id, *args, **kwargs) | |||
| if key_file is not None: | |||
| self._client = DomainService(account, private_key_file=key_file) | |||
| elif key is not None: | |||
| self._client = DomainService(account, private_key=key) | |||
| else: | |||
| raise TransipConfigException( | |||
| 'Missing `key` of `key_file` parameter in config' | |||
| ) | |||
| self.account = account | |||
| self.key = key | |||
| self._currentZone = {} | |||
| def populate(self, zone, target=False, lenient=False): | |||
| exists = False | |||
| self._currentZone = zone | |||
| self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, | |||
| target, lenient) | |||
| before = len(zone.records) | |||
| try: | |||
| zoneInfo = self._client.get_info(zone.name[:-1]) | |||
| except WebFault as e: | |||
| if e.fault.faultcode == '102' and target is False: | |||
| # Zone not found in account, and not a target so just | |||
| # leave an empty zone. | |||
| return exists | |||
| elif e.fault.faultcode == '102' and target is True: | |||
| self.log.warning('populate: Transip can\'t create new zones') | |||
| raise TransipNewZoneException( | |||
| ('populate: ({}) Transip used ' + | |||
| 'as target for non-existing zone: {}').format( | |||
| e.fault.faultcode, zone.name)) | |||
| else: | |||
| self.log.error('populate: (%s) %s ', e.fault.faultcode, | |||
| e.fault.faultstring) | |||
| raise e | |||
| self.log.debug('populate: found %s records for zone %s', | |||
| len(zoneInfo.dnsEntries), zone.name) | |||
| exists = True | |||
| if zoneInfo.dnsEntries: | |||
| values = defaultdict(lambda: defaultdict(list)) | |||
| for record in zoneInfo.dnsEntries: | |||
| name = zone.hostname_from_fqdn(record['name']) | |||
| if name == self.ROOT_RECORD: | |||
| name = '' | |||
| if record['type'] in self.SUPPORTS: | |||
| values[name][record['type']].append(record) | |||
| for name, types in values.items(): | |||
| for _type, records in types.items(): | |||
| data_for = getattr(self, '_data_for_{}'.format(_type)) | |||
| record = Record.new(zone, name, data_for(_type, records), | |||
| source=self, lenient=lenient) | |||
| zone.add_record(record, lenient=lenient) | |||
| self.log.info('populate: found %s records, exists = %s', | |||
| len(zone.records) - before, exists) | |||
| self._currentZone = {} | |||
| return exists | |||
| def _apply(self, plan): | |||
| desired = plan.desired | |||
| changes = plan.changes | |||
| self.log.debug('apply: zone=%s, changes=%d', desired.name, | |||
| len(changes)) | |||
| self._currentZone = plan.desired | |||
| try: | |||
| self._client.get_info(plan.desired.name[:-1]) | |||
| except WebFault as e: | |||
| self.log.warning('_apply: %s ', e.message) | |||
| raise e | |||
| _dns_entries = [] | |||
| for record in plan.desired.records: | |||
| if record._type in self.SUPPORTS: | |||
| entries_for = getattr(self, | |||
| '_entries_for_{}'.format(record._type)) | |||
| # Root records have '@' as name | |||
| name = record.name | |||
| if name == '': | |||
| name = self.ROOT_RECORD | |||
| _dns_entries.extend(entries_for(name, record)) | |||
| try: | |||
| self._client.set_dns_entries(plan.desired.name[:-1], _dns_entries) | |||
| except WebFault as e: | |||
| self.log.warning(('_apply: Set DNS returned ' + | |||
| 'one or more errors: {}').format( | |||
| e.fault.faultstring)) | |||
| raise TransipException(200, e.fault.faultstring) | |||
| self._currentZone = {} | |||
| def _entries_for_multiple(self, name, record): | |||
| _entries = [] | |||
| for value in record.values: | |||
| _entries.append(DnsEntry(name, record.ttl, record._type, value)) | |||
| return _entries | |||
| def _entries_for_single(self, name, record): | |||
| return [DnsEntry(name, record.ttl, record._type, record.value)] | |||
| _entries_for_A = _entries_for_multiple | |||
| _entries_for_AAAA = _entries_for_multiple | |||
| _entries_for_NS = _entries_for_multiple | |||
| _entries_for_SPF = _entries_for_multiple | |||
| _entries_for_CNAME = _entries_for_single | |||
| def _entries_for_MX(self, name, record): | |||
| _entries = [] | |||
| for value in record.values: | |||
| content = "{} {}".format(value.preference, value.exchange) | |||
| _entries.append(DnsEntry(name, record.ttl, record._type, content)) | |||
| return _entries | |||
| def _entries_for_SRV(self, name, record): | |||
| _entries = [] | |||
| for value in record.values: | |||
| content = "{} {} {} {}".format(value.priority, value.weight, | |||
| value.port, value.target) | |||
| _entries.append(DnsEntry(name, record.ttl, record._type, content)) | |||
| return _entries | |||
| def _entries_for_SSHFP(self, name, record): | |||
| _entries = [] | |||
| for value in record.values: | |||
| content = "{} {} {}".format(value.algorithm, | |||
| value.fingerprint_type, | |||
| value.fingerprint) | |||
| _entries.append(DnsEntry(name, record.ttl, record._type, content)) | |||
| return _entries | |||
| def _entries_for_CAA(self, name, record): | |||
| _entries = [] | |||
| for value in record.values: | |||
| content = "{} {} {}".format(value.flags, value.tag, | |||
| value.value) | |||
| _entries.append(DnsEntry(name, record.ttl, record._type, content)) | |||
| return _entries | |||
| def _entries_for_TXT(self, name, record): | |||
| _entries = [] | |||
| for value in record.values: | |||
| value = value.replace('\\;', ';') | |||
| _entries.append(DnsEntry(name, record.ttl, record._type, value)) | |||
| return _entries | |||
| def _parse_to_fqdn(self, value): | |||
| # Enforce switch from suds.sax.text.Text to string | |||
| value = str(value) | |||
| # TransIP allows '@' as value to alias the root record. | |||
| # this provider won't set an '@' value, but can be an existing record | |||
| if value == self.ROOT_RECORD: | |||
| value = self._currentZone.name | |||
| if value[-1] != '.': | |||
| self.log.debug('parseToFQDN: changed %s to %s', value, | |||
| '{}.{}'.format(value, self._currentZone.name)) | |||
| value = '{}.{}'.format(value, self._currentZone.name) | |||
| return value | |||
| def _get_lowest_ttl(self, records): | |||
| _ttl = 100000 | |||
| for record in records: | |||
| _ttl = min(_ttl, record['expire']) | |||
| return _ttl | |||
| def _data_for_multiple(self, _type, records): | |||
| _values = [] | |||
| for record in records: | |||
| # Enforce switch from suds.sax.text.Text to string | |||
| _values.append(str(record['content'])) | |||
| return { | |||
| 'ttl': self._get_lowest_ttl(records), | |||
| 'type': _type, | |||
| 'values': _values | |||
| } | |||
| _data_for_A = _data_for_multiple | |||
| _data_for_AAAA = _data_for_multiple | |||
| _data_for_NS = _data_for_multiple | |||
| _data_for_SPF = _data_for_multiple | |||
| def _data_for_CNAME(self, _type, records): | |||
| return { | |||
| 'ttl': records[0]['expire'], | |||
| 'type': _type, | |||
| 'value': self._parse_to_fqdn(records[0]['content']) | |||
| } | |||
| def _data_for_MX(self, _type, records): | |||
| _values = [] | |||
| for record in records: | |||
| preference, exchange = record['content'].split(" ", 1) | |||
| _values.append({ | |||
| 'preference': preference, | |||
| 'exchange': self._parse_to_fqdn(exchange) | |||
| }) | |||
| return { | |||
| 'ttl': self._get_lowest_ttl(records), | |||
| 'type': _type, | |||
| 'values': _values | |||
| } | |||
| def _data_for_SRV(self, _type, records): | |||
| _values = [] | |||
| for record in records: | |||
| priority, weight, port, target = record['content'].split(' ', 3) | |||
| _values.append({ | |||
| 'port': port, | |||
| 'priority': priority, | |||
| 'target': self._parse_to_fqdn(target), | |||
| 'weight': weight | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': self._get_lowest_ttl(records), | |||
| 'values': _values | |||
| } | |||
| def _data_for_SSHFP(self, _type, records): | |||
| _values = [] | |||
| for record in records: | |||
| algorithm, fp_type, fingerprint = record['content'].split(' ', 2) | |||
| _values.append({ | |||
| 'algorithm': algorithm, | |||
| 'fingerprint': fingerprint.lower(), | |||
| 'fingerprint_type': fp_type | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': self._get_lowest_ttl(records), | |||
| 'values': _values | |||
| } | |||
| def _data_for_CAA(self, _type, records): | |||
| _values = [] | |||
| for record in records: | |||
| flags, tag, value = record['content'].split(' ', 2) | |||
| _values.append({ | |||
| 'flags': flags, | |||
| 'tag': tag, | |||
| 'value': value | |||
| }) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': self._get_lowest_ttl(records), | |||
| 'values': _values | |||
| } | |||
| def _data_for_TXT(self, _type, records): | |||
| _values = [] | |||
| for record in records: | |||
| _values.append(record['content'].replace(';', '\\;')) | |||
| return { | |||
| 'type': _type, | |||
| 'ttl': self._get_lowest_ttl(records), | |||
| 'values': _values | |||
| } | |||
| @ -0,0 +1,28 @@ | |||
| [{ | |||
| "id": 123123, | |||
| "name": "unit.tests", | |||
| "soa": { | |||
| "primaryNameserver": "ns11.constellix.com.", | |||
| "email": "dns.constellix.com.", | |||
| "ttl": 86400, | |||
| "serial": 2015010102, | |||
| "refresh": 43200, | |||
| "retry": 3600, | |||
| "expire": 1209600, | |||
| "negCache": 180 | |||
| }, | |||
| "createdTs": "2019-08-07T03:36:02Z", | |||
| "modifiedTs": "2019-08-07T03:36:02Z", | |||
| "typeId": 1, | |||
| "domainTags": [], | |||
| "folder": null, | |||
| "hasGtdRegions": false, | |||
| "hasGeoIP": false, | |||
| "nameserverGroup": 1, | |||
| "nameservers": ["ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net."], | |||
| "note": "", | |||
| "version": 0, | |||
| "status": "ACTIVE", | |||
| "tags": [], | |||
| "contactIds": [] | |||
| }] | |||
| @ -0,0 +1,598 @@ | |||
| [{ | |||
| "id": 1808529, | |||
| "type": "CAA", | |||
| "recordType": "caa", | |||
| "name": "", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 3600, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565149569216, | |||
| "value": [{ | |||
| "flag": 0, | |||
| "tag": "issue", | |||
| "data": "ca.unit.tests", | |||
| "caaProviderId": 1, | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "flag": 0, | |||
| "tag": "issue", | |||
| "data": "ca.unit.tests", | |||
| "caaProviderId": 1, | |||
| "disableFlag": false | |||
| }] | |||
| }, { | |||
| "id": 1808516, | |||
| "type": "A", | |||
| "recordType": "a", | |||
| "name": "", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 300, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565149623640, | |||
| "value": ["1.2.3.4", "1.2.3.5"], | |||
| "roundRobin": [{ | |||
| "value": "1.2.3.4", | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "1.2.3.5", | |||
| "disableFlag": false | |||
| }], | |||
| "geolocation": null, | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "roundRobinFailover": [], | |||
| "pools": [], | |||
| "poolsDetail": [] | |||
| }, { | |||
| "id": 1808527, | |||
| "type": "SRV", | |||
| "recordType": "srv", | |||
| "name": "_srv._tcp", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 600, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565149714387, | |||
| "value": [{ | |||
| "value": "foo-1.unit.tests.", | |||
| "priority": 10, | |||
| "weight": 20, | |||
| "port": 30, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "foo-2.unit.tests.", | |||
| "priority": 12, | |||
| "weight": 20, | |||
| "port": 30, | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "foo-1.unit.tests.", | |||
| "priority": 10, | |||
| "weight": 20, | |||
| "port": 30, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "foo-2.unit.tests.", | |||
| "priority": 12, | |||
| "weight": 20, | |||
| "port": 30, | |||
| "disableFlag": false | |||
| }] | |||
| }, { | |||
| "id": 1808515, | |||
| "type": "AAAA", | |||
| "recordType": "aaaa", | |||
| "name": "aaaa", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 600, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565149739464, | |||
| "value": ["2601:644:500:e210:62f8:1dff:feb8:947a"], | |||
| "roundRobin": [{ | |||
| "value": "2601:644:500:e210:62f8:1dff:feb8:947a", | |||
| "disableFlag": false | |||
| }], | |||
| "geolocation": null, | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "pools": [], | |||
| "poolsDetail": [], | |||
| "roundRobinFailover": [] | |||
| }, { | |||
| "id": 1808530, | |||
| "type": "ANAME", | |||
| "recordType": "aname", | |||
| "name": "", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 1800, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565150251379, | |||
| "value": [{ | |||
| "value": "aname.unit.tests.", | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "aname.unit.tests.", | |||
| "disableFlag": false | |||
| }], | |||
| "geolocation": null, | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "pools": [], | |||
| "poolsDetail": [] | |||
| }, { | |||
| "id": 1808521, | |||
| "type": "CNAME", | |||
| "recordType": "cname", | |||
| "name": "cname", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 300, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565152113825, | |||
| "value": "", | |||
| "roundRobin": [{ | |||
| "value": "", | |||
| "disableFlag": false | |||
| }], | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [{ | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 1, | |||
| "markedActive": false | |||
| }, { | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 2, | |||
| "markedActive": false | |||
| }] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [{ | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 1, | |||
| "markedActive": false | |||
| }, { | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 2, | |||
| "markedActive": false | |||
| }] | |||
| }, | |||
| "pools": [], | |||
| "poolsDetail": [], | |||
| "geolocation": null, | |||
| "host": "" | |||
| }, { | |||
| "id": 1808522, | |||
| "type": "CNAME", | |||
| "recordType": "cname", | |||
| "name": "included", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 3600, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565152119137, | |||
| "value": "", | |||
| "roundRobin": [{ | |||
| "value": "", | |||
| "disableFlag": false | |||
| }], | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [{ | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 1, | |||
| "markedActive": false | |||
| }, { | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 2, | |||
| "markedActive": false | |||
| }] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [{ | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 1, | |||
| "markedActive": false | |||
| }, { | |||
| "id": null, | |||
| "value": "", | |||
| "disableFlag": false, | |||
| "failedFlag": false, | |||
| "status": "N/A", | |||
| "sortOrder": 2, | |||
| "markedActive": false | |||
| }] | |||
| }, | |||
| "pools": [], | |||
| "poolsDetail": [], | |||
| "geolocation": null, | |||
| "host": "" | |||
| }, { | |||
| "id": 1808523, | |||
| "type": "MX", | |||
| "recordType": "mx", | |||
| "name": "mx", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 300, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565149879856, | |||
| "value": [{ | |||
| "value": "smtp-3.unit.tests.", | |||
| "level": 30, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "smtp-2.unit.tests.", | |||
| "level": 20, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "smtp-4.unit.tests.", | |||
| "level": 10, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "smtp-1.unit.tests.", | |||
| "level": 40, | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "smtp-3.unit.tests.", | |||
| "level": 30, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "smtp-2.unit.tests.", | |||
| "level": 20, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "smtp-4.unit.tests.", | |||
| "level": 10, | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "smtp-1.unit.tests.", | |||
| "level": 40, | |||
| "disableFlag": false | |||
| }] | |||
| }, { | |||
| "id": 1808525, | |||
| "type": "PTR", | |||
| "recordType": "ptr", | |||
| "name": "ptr", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 300, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565150115139, | |||
| "value": [{ | |||
| "value": "foo.bar.com.", | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "foo.bar.com.", | |||
| "disableFlag": false | |||
| }] | |||
| }, { | |||
| "id": 1808526, | |||
| "type": "SPF", | |||
| "recordType": "spf", | |||
| "name": "spf", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 600, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565149916132, | |||
| "value": [{ | |||
| "value": "\"v=spf1 ip4:192.168.0.1/16-all\"", | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "\"v=spf1 ip4:192.168.0.1/16-all\"", | |||
| "disableFlag": false | |||
| }] | |||
| }, { | |||
| "id": 1808528, | |||
| "type": "TXT", | |||
| "recordType": "txt", | |||
| "name": "txt", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 600, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565149966915, | |||
| "value": [{ | |||
| "value": "\"Bah bah black sheep\"", | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "\"have you any wool.\"", | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"", | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "\"Bah bah black sheep\"", | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "\"have you any wool.\"", | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"", | |||
| "disableFlag": false | |||
| }] | |||
| }, { | |||
| "id": 1808524, | |||
| "type": "NS", | |||
| "recordType": "ns", | |||
| "name": "under", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 3600, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565150062850, | |||
| "value": [{ | |||
| "value": "ns1.unit.tests.", | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "ns2", | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "ns1.unit.tests.", | |||
| "disableFlag": false | |||
| }, { | |||
| "value": "ns2", | |||
| "disableFlag": false | |||
| }] | |||
| }, { | |||
| "id": 1808531, | |||
| "type": "HTTPRedirection", | |||
| "recordType": "httpredirection", | |||
| "name": "unsupported", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 300, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565150348154, | |||
| "value": "https://redirect.unit.tests", | |||
| "roundRobin": [{ | |||
| "value": "https://redirect.unit.tests" | |||
| }], | |||
| "title": "Unsupported Record", | |||
| "keywords": "unsupported", | |||
| "description": "unsupported record", | |||
| "hardlinkFlag": false, | |||
| "redirectTypeId": 1, | |||
| "url": "https://redirect.unit.tests" | |||
| }, { | |||
| "id": 1808519, | |||
| "type": "A", | |||
| "recordType": "a", | |||
| "name": "www", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 300, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565150079027, | |||
| "value": ["2.2.3.6"], | |||
| "roundRobin": [{ | |||
| "value": "2.2.3.6", | |||
| "disableFlag": false | |||
| }], | |||
| "geolocation": null, | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "roundRobinFailover": [], | |||
| "pools": [], | |||
| "poolsDetail": [] | |||
| }, { | |||
| "id": 1808603, | |||
| "type": "ANAME", | |||
| "recordType": "aname", | |||
| "name": "sub", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 1800, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565153387855, | |||
| "value": [{ | |||
| "value": "aname.unit.tests.", | |||
| "disableFlag": false | |||
| }], | |||
| "roundRobin": [{ | |||
| "value": "aname.unit.tests.", | |||
| "disableFlag": false | |||
| }], | |||
| "geolocation": null, | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "pools": [], | |||
| "poolsDetail": [] | |||
| }, { | |||
| "id": 1808520, | |||
| "type": "A", | |||
| "recordType": "a", | |||
| "name": "www.sub", | |||
| "recordOption": "roundRobin", | |||
| "noAnswer": false, | |||
| "note": "", | |||
| "ttl": 300, | |||
| "gtdRegion": 1, | |||
| "parentId": 123123, | |||
| "parent": "domain", | |||
| "source": "Domain", | |||
| "modifiedTs": 1565150090588, | |||
| "value": ["2.2.3.6"], | |||
| "roundRobin": [{ | |||
| "value": "2.2.3.6", | |||
| "disableFlag": false | |||
| }], | |||
| "geolocation": null, | |||
| "recordFailover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "failover": { | |||
| "disabled": false, | |||
| "failoverType": 1, | |||
| "failoverTypeStr": "Normal (always lowest level)", | |||
| "values": [] | |||
| }, | |||
| "roundRobinFailover": [], | |||
| "pools": [], | |||
| "poolsDetail": [] | |||
| }] | |||
| @ -0,0 +1,35 @@ | |||
| { | |||
| "recordsets": [ | |||
| { | |||
| "rdata": [ | |||
| "", | |||
| "12 20 foo-2.unit.tests." | |||
| ], | |||
| "type": "SRV", | |||
| "name": "_srv._tcp.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "", | |||
| "1 1" | |||
| ], | |||
| "type": "SSHFP", | |||
| "name": "unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "", | |||
| "100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ." | |||
| ], | |||
| "type": "NAPTR", | |||
| "name": "naptr.unit.tests", | |||
| "ttl": 600 | |||
| } | |||
| ], | |||
| "metadata": { | |||
| "totalElements": 3, | |||
| "showAll": true | |||
| } | |||
| } | |||
| @ -0,0 +1,166 @@ | |||
| { | |||
| "recordsets": [ | |||
| { | |||
| "rdata": [ | |||
| "10 20 30 foo-1.other.tests.", | |||
| "12 20 30 foo-2.other.tests." | |||
| ], | |||
| "type": "SRV", | |||
| "name": "_srv._tcp.old.other.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 20 30 foo-1.other.tests.", | |||
| "12 20 30 foo-2.other.tests." | |||
| ], | |||
| "type": "SRV", | |||
| "name": "_srv._tcp.old.other.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2601:644:500:e210:62f8:1dff:feb8:9471" | |||
| ], | |||
| "type": "AAAA", | |||
| "name": "aaaa.old.other.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.akam.net.", | |||
| "ns2.akam.net.", | |||
| "ns3.akam.net.", | |||
| "ns4.akam.net." | |||
| ], | |||
| "type": "NS", | |||
| "name": "old.other.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "1.2.3.4", | |||
| "1.2.3.5" | |||
| ], | |||
| "type": "A", | |||
| "name": "old.other.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300" | |||
| ], | |||
| "type": "SOA", | |||
| "name": "other.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49", | |||
| "1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73" | |||
| ], | |||
| "type": "SSHFP", | |||
| "name": "old.other.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "other.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "old.cname.other.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "other.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "excluded.old.other.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "other.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "included.old.other.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 smtp-4.other.tests.", | |||
| "20 smtp-2.other.tests.", | |||
| "30 smtp-3.other.tests.", | |||
| "40 smtp-1.other.tests." | |||
| ], | |||
| "type": "MX", | |||
| "name": "mx.old.other.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", | |||
| "100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ." | |||
| ], | |||
| "type": "NAPTR", | |||
| "name": "naptr.old.other.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "foo.bar.com." | |||
| ], | |||
| "type": "PTR", | |||
| "name": "ptr.old.other.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "\"v=spf1 ip4:192.168.0.1/16-all\"" | |||
| ], | |||
| "type": "SPF", | |||
| "name": "spf.old.other.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.other.tests.", | |||
| "ns2.other.tests." | |||
| ], | |||
| "type": "NS", | |||
| "name": "under.old.other.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "\"Bah bah black sheep\"", | |||
| "\"have you any wool.\"", | |||
| "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"" | |||
| ], | |||
| "type": "TXT", | |||
| "name": "txt.old.other.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2.2.3.7" | |||
| ], | |||
| "type": "A", | |||
| "name": "www.other.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2.2.3.6" | |||
| ], | |||
| "type": "A", | |||
| "name": "www.sub.old.other.tests", | |||
| "ttl": 300 | |||
| } | |||
| ], | |||
| "metadata": { | |||
| "totalElements": 16, | |||
| "showAll": true | |||
| } | |||
| } | |||
| @ -0,0 +1,166 @@ | |||
| { | |||
| "recordsets": [ | |||
| { | |||
| "rdata": [ | |||
| "10 20 30 foo-1.unit.tests.", | |||
| "12 20 30 foo-2.unit.tests." | |||
| ], | |||
| "type": "SRV", | |||
| "name": "_srv._tcp.old.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 20 30 foo-1.unit.tests.", | |||
| "12 20 30 foo-2.unit.tests." | |||
| ], | |||
| "type": "SRV", | |||
| "name": "_srv._tcp.old.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2601:644:500:e210:62f8:1dff:feb8:9471" | |||
| ], | |||
| "type": "AAAA", | |||
| "name": "aaaa.old.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.akam.net.", | |||
| "ns2.akam.net.", | |||
| "ns3.akam.net.", | |||
| "ns4.akam.net." | |||
| ], | |||
| "type": "NS", | |||
| "name": "old.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "1.2.3.4", | |||
| "1.2.3.5" | |||
| ], | |||
| "type": "A", | |||
| "name": "old.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300" | |||
| ], | |||
| "type": "SOA", | |||
| "name": "unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49", | |||
| "1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73" | |||
| ], | |||
| "type": "SSHFP", | |||
| "name": "old.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "unit.tests" | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "old.cname.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "unit.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "excluded.old.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "unit.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "included.old.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 smtp-4.unit.tests.", | |||
| "20 smtp-2.unit.tests.", | |||
| "30 smtp-3.unit.tests.", | |||
| "40 smtp-1.unit.tests." | |||
| ], | |||
| "type": "MX", | |||
| "name": "mx.old.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", | |||
| "100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ." | |||
| ], | |||
| "type": "NAPTR", | |||
| "name": "naptr.old.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "foo.bar.com." | |||
| ], | |||
| "type": "PTR", | |||
| "name": "ptr.old.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "\"v=spf1 ip4:192.168.0.1/16-all\"" | |||
| ], | |||
| "type": "SPF", | |||
| "name": "spf.old.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.unit.tests.", | |||
| "ns2.unit.tests." | |||
| ], | |||
| "type": "NS", | |||
| "name": "under.old.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "\"Bah bah black sheep\"", | |||
| "\"have you any wool.\"", | |||
| "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"" | |||
| ], | |||
| "type": "TXT", | |||
| "name": "txt.old.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2.2.3.7" | |||
| ], | |||
| "type": "A", | |||
| "name": "www.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2.2.3.6" | |||
| ], | |||
| "type": "A", | |||
| "name": "www.sub.old.unit.tests", | |||
| "ttl": 300 | |||
| } | |||
| ], | |||
| "metadata": { | |||
| "totalElements": 16, | |||
| "showAll": true | |||
| } | |||
| } | |||
| @ -0,0 +1,157 @@ | |||
| { | |||
| "recordsets": [ | |||
| { | |||
| "rdata": [ | |||
| "10 20 30 foo-1.unit.tests.", | |||
| "12 20 30 foo-2.unit.tests." | |||
| ], | |||
| "type": "SRV", | |||
| "name": "_srv._tcp.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2601:644:500:e210:62f8:1dff:feb8:947a" | |||
| ], | |||
| "type": "AAAA", | |||
| "name": "aaaa.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.akam.net.", | |||
| "ns2.akam.net.", | |||
| "ns3.akam.net.", | |||
| "ns4.akam.net." | |||
| ], | |||
| "type": "NS", | |||
| "name": "unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "1.2.3.4", | |||
| "1.2.3.5" | |||
| ], | |||
| "type": "A", | |||
| "name": "unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300" | |||
| ], | |||
| "type": "SOA", | |||
| "name": "unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49", | |||
| "1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73" | |||
| ], | |||
| "type": "SSHFP", | |||
| "name": "unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "unit.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "cname.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "unit.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "excluded.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "unit.tests." | |||
| ], | |||
| "type": "CNAME", | |||
| "name": "included.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 smtp-4.unit.tests.", | |||
| "20 smtp-2.unit.tests.", | |||
| "30 smtp-3.unit.tests.", | |||
| "40 smtp-1.unit.tests." | |||
| ], | |||
| "type": "MX", | |||
| "name": "mx.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", | |||
| "100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ." | |||
| ], | |||
| "type": "NAPTR", | |||
| "name": "naptr.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "foo.bar.com." | |||
| ], | |||
| "type": "PTR", | |||
| "name": "ptr.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "\"v=spf1 ip4:192.168.0.1/16-all\"" | |||
| ], | |||
| "type": "SPF", | |||
| "name": "spf.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "ns1.unit.tests.", | |||
| "ns2.unit.tests." | |||
| ], | |||
| "type": "NS", | |||
| "name": "under.unit.tests", | |||
| "ttl": 3600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "\"Bah bah black sheep\"", | |||
| "\"have you any wool.\"", | |||
| "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"" | |||
| ], | |||
| "type": "TXT", | |||
| "name": "txt.unit.tests", | |||
| "ttl": 600 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2.2.3.6" | |||
| ], | |||
| "type": "A", | |||
| "name": "www.unit.tests", | |||
| "ttl": 300 | |||
| }, | |||
| { | |||
| "rdata": [ | |||
| "2.2.3.6" | |||
| ], | |||
| "type": "A", | |||
| "name": "www.sub.unit.tests", | |||
| "ttl": 300 | |||
| } | |||
| ], | |||
| "metadata": { | |||
| "totalElements": 16, | |||
| "showAll": true | |||
| } | |||
| } | |||
| @ -0,0 +1,218 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| from mock import Mock, call | |||
| from os.path import dirname, join | |||
| from requests import HTTPError | |||
| from requests_mock import ANY, mock as requests_mock | |||
| from unittest import TestCase | |||
| from octodns.record import Record | |||
| from octodns.provider.constellix import ConstellixClientNotFound, \ | |||
| ConstellixProvider | |||
| from octodns.provider.yaml import YamlProvider | |||
| from octodns.zone import Zone | |||
| import json | |||
| class TestConstellixProvider(TestCase): | |||
| expected = Zone('unit.tests.', []) | |||
| source = YamlProvider('test', join(dirname(__file__), 'config')) | |||
| source.populate(expected) | |||
| # Our test suite differs a bit, add our NS and remove the simple one | |||
| expected.add_record(Record.new(expected, 'under', { | |||
| 'ttl': 3600, | |||
| 'type': 'NS', | |||
| 'values': [ | |||
| 'ns1.unit.tests.', | |||
| 'ns2.unit.tests.', | |||
| ] | |||
| })) | |||
| # Add some ALIAS records | |||
| expected.add_record(Record.new(expected, '', { | |||
| 'ttl': 1800, | |||
| 'type': 'ALIAS', | |||
| 'value': 'aname.unit.tests.' | |||
| })) | |||
| expected.add_record(Record.new(expected, 'sub', { | |||
| 'ttl': 1800, | |||
| 'type': 'ALIAS', | |||
| 'value': 'aname.unit.tests.' | |||
| })) | |||
| for record in list(expected.records): | |||
| if record.name == 'sub' and record._type == 'NS': | |||
| expected._remove_record(record) | |||
| break | |||
| def test_populate(self): | |||
| provider = ConstellixProvider('test', 'api', 'secret') | |||
| # Bad auth | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=401, | |||
| text='{"errors": ["Unable to authenticate token"]}') | |||
| with self.assertRaises(Exception) as ctx: | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals('Unauthorized', ctx.exception.message) | |||
| # Bad request | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=400, | |||
| text='{"errors": ["\\"unittests\\" is not ' | |||
| 'a valid domain name"]}') | |||
| with self.assertRaises(Exception) as ctx: | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals('\n - "unittests" is not a valid domain name', | |||
| ctx.exception.message) | |||
| # General error | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=502, text='Things caught fire') | |||
| with self.assertRaises(HTTPError) as ctx: | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals(502, ctx.exception.response.status_code) | |||
| # Non-existent zone doesn't populate anything | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=404, | |||
| text='<html><head></head><body></body></html>') | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals(set(), zone.records) | |||
| # No diffs == no changes | |||
| with requests_mock() as mock: | |||
| base = 'https://api.dns.constellix.com/v1/domains' | |||
| with open('tests/fixtures/constellix-domains.json') as fh: | |||
| mock.get('{}{}'.format(base, '/'), text=fh.read()) | |||
| with open('tests/fixtures/constellix-records.json') as fh: | |||
| mock.get('{}{}'.format(base, '/123123/records'), | |||
| text=fh.read()) | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals(15, len(zone.records)) | |||
| changes = self.expected.changes(zone, provider) | |||
| self.assertEquals(0, len(changes)) | |||
| # 2nd populate makes no network calls/all from cache | |||
| again = Zone('unit.tests.', []) | |||
| provider.populate(again) | |||
| self.assertEquals(15, len(again.records)) | |||
| # bust the cache | |||
| del provider._zone_records[zone.name] | |||
| def test_apply(self): | |||
| provider = ConstellixProvider('test', 'api', 'secret') | |||
| resp = Mock() | |||
| resp.json = Mock() | |||
| provider._client._request = Mock(return_value=resp) | |||
| with open('tests/fixtures/constellix-domains.json') as fh: | |||
| domains = json.load(fh) | |||
| # non-existent domain, create everything | |||
| resp.json.side_effect = [ | |||
| ConstellixClientNotFound, # no zone in populate | |||
| ConstellixClientNotFound, # no domain during apply | |||
| domains | |||
| ] | |||
| plan = provider.plan(self.expected) | |||
| # No root NS, no ignored, no excluded, no unsupported | |||
| n = len(self.expected.records) - 5 | |||
| self.assertEquals(n, len(plan.changes)) | |||
| self.assertEquals(n, provider.apply(plan)) | |||
| provider._client._request.assert_has_calls([ | |||
| # created the domain | |||
| call('POST', '/', data={'names': ['unit.tests']}), | |||
| # get all domains to build the cache | |||
| call('GET', '/'), | |||
| call('POST', '/123123/records/SRV', data={ | |||
| 'roundRobin': [{ | |||
| 'priority': 10, | |||
| 'weight': 20, | |||
| 'value': 'foo-1.unit.tests.', | |||
| 'port': 30 | |||
| }, { | |||
| 'priority': 12, | |||
| 'weight': 20, | |||
| 'value': 'foo-2.unit.tests.', | |||
| 'port': 30 | |||
| }], | |||
| 'name': '_srv._tcp', | |||
| 'ttl': 600, | |||
| }), | |||
| ]) | |||
| self.assertEquals(20, provider._client._request.call_count) | |||
| provider._client._request.reset_mock() | |||
| provider._client.records = Mock(return_value=[ | |||
| { | |||
| 'id': 11189897, | |||
| 'type': 'A', | |||
| 'name': 'www', | |||
| 'ttl': 300, | |||
| 'value': [ | |||
| '1.2.3.4', | |||
| '2.2.3.4', | |||
| ] | |||
| }, { | |||
| 'id': 11189898, | |||
| 'type': 'A', | |||
| 'name': 'ttl', | |||
| 'ttl': 600, | |||
| 'value': [ | |||
| '3.2.3.4' | |||
| ] | |||
| } | |||
| ]) | |||
| # Domain exists, we don't care about return | |||
| resp.json.side_effect = ['{}'] | |||
| wanted = Zone('unit.tests.', []) | |||
| wanted.add_record(Record.new(wanted, 'ttl', { | |||
| 'ttl': 300, | |||
| 'type': 'A', | |||
| 'value': '3.2.3.4' | |||
| })) | |||
| plan = provider.plan(wanted) | |||
| self.assertEquals(2, len(plan.changes)) | |||
| self.assertEquals(2, provider.apply(plan)) | |||
| # recreate for update, and deletes for the 2 parts of the other | |||
| provider._client._request.assert_has_calls([ | |||
| call('POST', '/123123/records/A', data={ | |||
| 'roundRobin': [{ | |||
| 'value': '3.2.3.4' | |||
| }], | |||
| 'name': 'ttl', | |||
| 'ttl': 300 | |||
| }), | |||
| call('DELETE', '/123123/records/A/11189897'), | |||
| call('DELETE', '/123123/records/A/11189898') | |||
| ], any_order=True) | |||
| @ -0,0 +1,150 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| # from mock import Mock, call | |||
| from os.path import dirname, join | |||
| from requests import HTTPError | |||
| from requests_mock import ANY, mock as requests_mock | |||
| from unittest import TestCase | |||
| from octodns.record import Record | |||
| from octodns.provider.fastdns import AkamaiProvider | |||
| from octodns.provider.yaml import YamlProvider | |||
| from octodns.zone import Zone | |||
| class TestFastdnsProvider(TestCase): | |||
| expected = Zone('unit.tests.', []) | |||
| source = YamlProvider('test', join(dirname(__file__), 'config')) | |||
| source.populate(expected) | |||
| # Our test suite differs a bit, add our NS and remove the simple one | |||
| expected.add_record(Record.new(expected, 'under', { | |||
| 'ttl': 3600, | |||
| 'type': 'NS', | |||
| 'values': [ | |||
| 'ns1.unit.tests.', | |||
| 'ns2.unit.tests.', | |||
| ] | |||
| })) | |||
| for record in list(expected.records): | |||
| if record.name == 'sub' and record._type == 'NS': | |||
| expected._remove_record(record) | |||
| break | |||
| def test_populate(self): | |||
| provider = AkamaiProvider("test", "secret", "akam.com", "atok", "ctok") | |||
| # Bad Auth | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=401, text='{"message": "Unauthorized"}') | |||
| with self.assertRaises(Exception) as ctx: | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals(401, ctx.exception.response.status_code) | |||
| # general error | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=502, text='Things caught fire') | |||
| with self.assertRaises(HTTPError) as ctx: | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals(502, ctx.exception.response.status_code) | |||
| # Non-existant zone doesn't populate anything | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=404, | |||
| text='{"message": "Domain `foo.bar` not found"}') | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals(set(), zone.records) | |||
| # No diffs == no changes | |||
| with requests_mock() as mock: | |||
| with open('tests/fixtures/fastdns-records.json') as fh: | |||
| mock.get(ANY, text=fh.read()) | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone) | |||
| self.assertEquals(16, len(zone.records)) | |||
| changes = self.expected.changes(zone, provider) | |||
| self.assertEquals(0, len(changes)) | |||
| # 2nd populate makes no network calls/all from cache | |||
| again = Zone('unit.tests.', []) | |||
| provider.populate(again) | |||
| self.assertEquals(16, len(again.records)) | |||
| # bust the cache | |||
| del provider._zone_records[zone.name] | |||
| def test_apply(self): | |||
| provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok", | |||
| "cid", "gid") | |||
| # tests create update delete through previous state config json | |||
| with requests_mock() as mock: | |||
| with open('tests/fixtures/fastdns-records-prev.json') as fh: | |||
| mock.get(ANY, text=fh.read()) | |||
| plan = provider.plan(self.expected) | |||
| mock.post(ANY, status_code=201) | |||
| mock.put(ANY, status_code=200) | |||
| mock.delete(ANY, status_code=204) | |||
| changes = provider.apply(plan) | |||
| self.assertEquals(29, changes) | |||
| # Test against a zone that doesn't exist yet | |||
| with requests_mock() as mock: | |||
| with open('tests/fixtures/fastdns-records-prev-other.json') as fh: | |||
| mock.get(ANY, status_code=404) | |||
| plan = provider.plan(self.expected) | |||
| mock.post(ANY, status_code=201) | |||
| mock.put(ANY, status_code=200) | |||
| mock.delete(ANY, status_code=204) | |||
| changes = provider.apply(plan) | |||
| self.assertEquals(14, changes) | |||
| # Test against a zone that doesn't exist yet, but gid not provided | |||
| with requests_mock() as mock: | |||
| with open('tests/fixtures/fastdns-records-prev-other.json') as fh: | |||
| mock.get(ANY, status_code=404) | |||
| provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok", | |||
| "cid") | |||
| plan = provider.plan(self.expected) | |||
| mock.post(ANY, status_code=201) | |||
| mock.put(ANY, status_code=200) | |||
| mock.delete(ANY, status_code=204) | |||
| changes = provider.apply(plan) | |||
| self.assertEquals(14, changes) | |||
| # Test against a zone that doesn't exist, but cid not provided | |||
| with requests_mock() as mock: | |||
| mock.get(ANY, status_code=404) | |||
| provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok") | |||
| plan = provider.plan(self.expected) | |||
| mock.post(ANY, status_code=201) | |||
| mock.put(ANY, status_code=200) | |||
| mock.delete(ANY, status_code=204) | |||
| try: | |||
| changes = provider.apply(plan) | |||
| except NameError as e: | |||
| expected = "contractId not specified to create zone" | |||
| self.assertEquals(e.message, expected) | |||
| @ -0,0 +1,401 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| from unittest import TestCase | |||
| import requests_mock | |||
| from octodns.provider.selectel import SelectelProvider | |||
| from octodns.record import Record, Update | |||
| from octodns.zone import Zone | |||
| class TestSelectelProvider(TestCase): | |||
| API_URL = 'https://api.selectel.ru/domains/v1' | |||
| api_record = [] | |||
| zone = Zone('unit.tests.', []) | |||
| expected = set() | |||
| domain = [{"name": "unit.tests", "id": 100000}] | |||
| # A, subdomain='' | |||
| api_record.append({ | |||
| 'type': 'A', | |||
| 'ttl': 100, | |||
| 'content': '1.2.3.4', | |||
| 'name': 'unit.tests', | |||
| 'id': 1 | |||
| }) | |||
| expected.add(Record.new(zone, '', { | |||
| 'ttl': 100, | |||
| 'type': 'A', | |||
| 'value': '1.2.3.4', | |||
| })) | |||
| # A, subdomain='sub' | |||
| api_record.append({ | |||
| 'type': 'A', | |||
| 'ttl': 200, | |||
| 'content': '1.2.3.4', | |||
| 'name': 'sub.unit.tests', | |||
| 'id': 2 | |||
| }) | |||
| expected.add(Record.new(zone, 'sub', { | |||
| 'ttl': 200, | |||
| 'type': 'A', | |||
| 'value': '1.2.3.4', | |||
| })) | |||
| # CNAME | |||
| api_record.append({ | |||
| 'type': 'CNAME', | |||
| 'ttl': 300, | |||
| 'content': 'unit.tests', | |||
| 'name': 'www2.unit.tests', | |||
| 'id': 3 | |||
| }) | |||
| expected.add(Record.new(zone, 'www2', { | |||
| 'ttl': 300, | |||
| 'type': 'CNAME', | |||
| 'value': 'unit.tests.', | |||
| })) | |||
| # MX | |||
| api_record.append({ | |||
| 'type': 'MX', | |||
| 'ttl': 400, | |||
| 'content': 'mx1.unit.tests', | |||
| 'priority': 10, | |||
| 'name': 'unit.tests', | |||
| 'id': 4 | |||
| }) | |||
| expected.add(Record.new(zone, '', { | |||
| 'ttl': 400, | |||
| 'type': 'MX', | |||
| 'values': [{ | |||
| 'preference': 10, | |||
| 'exchange': 'mx1.unit.tests.', | |||
| }] | |||
| })) | |||
| # NS | |||
| api_record.append({ | |||
| 'type': 'NS', | |||
| 'ttl': 600, | |||
| 'content': 'ns1.unit.tests', | |||
| 'name': 'unit.tests.', | |||
| 'id': 6 | |||
| }) | |||
| api_record.append({ | |||
| 'type': 'NS', | |||
| 'ttl': 600, | |||
| 'content': 'ns2.unit.tests', | |||
| 'name': 'unit.tests', | |||
| 'id': 7 | |||
| }) | |||
| expected.add(Record.new(zone, '', { | |||
| 'ttl': 600, | |||
| 'type': 'NS', | |||
| 'values': ['ns1.unit.tests.', 'ns2.unit.tests.'], | |||
| })) | |||
| # NS with sub | |||
| api_record.append({ | |||
| 'type': 'NS', | |||
| 'ttl': 700, | |||
| 'content': 'ns3.unit.tests', | |||
| 'name': 'www3.unit.tests', | |||
| 'id': 8 | |||
| }) | |||
| api_record.append({ | |||
| 'type': 'NS', | |||
| 'ttl': 700, | |||
| 'content': 'ns4.unit.tests', | |||
| 'name': 'www3.unit.tests', | |||
| 'id': 9 | |||
| }) | |||
| expected.add(Record.new(zone, 'www3', { | |||
| 'ttl': 700, | |||
| 'type': 'NS', | |||
| 'values': ['ns3.unit.tests.', 'ns4.unit.tests.'], | |||
| })) | |||
| # SRV | |||
| api_record.append({ | |||
| 'type': 'SRV', | |||
| 'ttl': 800, | |||
| 'target': 'foo-1.unit.tests', | |||
| 'weight': 20, | |||
| 'priority': 10, | |||
| 'port': 30, | |||
| 'id': 10, | |||
| 'name': '_srv._tcp.unit.tests' | |||
| }) | |||
| api_record.append({ | |||
| 'type': 'SRV', | |||
| 'ttl': 800, | |||
| 'target': 'foo-2.unit.tests', | |||
| 'name': '_srv._tcp.unit.tests', | |||
| 'weight': 50, | |||
| 'priority': 40, | |||
| 'port': 60, | |||
| 'id': 11 | |||
| }) | |||
| expected.add(Record.new(zone, '_srv._tcp', { | |||
| 'ttl': 800, | |||
| 'type': 'SRV', | |||
| 'values': [{ | |||
| 'priority': 10, | |||
| 'weight': 20, | |||
| 'port': 30, | |||
| 'target': 'foo-1.unit.tests.', | |||
| }, { | |||
| 'priority': 40, | |||
| 'weight': 50, | |||
| 'port': 60, | |||
| 'target': 'foo-2.unit.tests.', | |||
| }] | |||
| })) | |||
| # AAAA | |||
| aaaa_record = { | |||
| 'type': 'AAAA', | |||
| 'ttl': 200, | |||
| 'content': '1:1ec:1::1', | |||
| 'name': 'unit.tests', | |||
| 'id': 15 | |||
| } | |||
| api_record.append(aaaa_record) | |||
| expected.add(Record.new(zone, '', { | |||
| 'ttl': 200, | |||
| 'type': 'AAAA', | |||
| 'value': '1:1ec:1::1', | |||
| })) | |||
| # TXT | |||
| api_record.append({ | |||
| 'type': 'TXT', | |||
| 'ttl': 300, | |||
| 'content': 'little text', | |||
| 'name': 'text.unit.tests', | |||
| 'id': 16 | |||
| }) | |||
| expected.add(Record.new(zone, 'text', { | |||
| 'ttl': 200, | |||
| 'type': 'TXT', | |||
| 'value': 'little text', | |||
| })) | |||
| @requests_mock.Mocker() | |||
| def test_populate(self, fake_http): | |||
| zone = Zone('unit.tests.', []) | |||
| fake_http.get('{}/unit.tests/records/'.format(self.API_URL), | |||
| json=self.api_record) | |||
| fake_http.get('{}/'.format(self.API_URL), json=self.domain) | |||
| fake_http.head('{}/unit.tests/records/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.api_record))}) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| provider = SelectelProvider(123, 'secret_token') | |||
| provider.populate(zone) | |||
| self.assertEquals(self.expected, zone.records) | |||
| @requests_mock.Mocker() | |||
| def test_populate_invalid_record(self, fake_http): | |||
| more_record = self.api_record | |||
| more_record.append({"name": "unit.tests", | |||
| "id": 100001, | |||
| "content": "support.unit.tests.", | |||
| "ttl": 300, "ns": "ns1.unit.tests", | |||
| "type": "SOA", | |||
| "email": "support@unit.tests"}) | |||
| zone = Zone('unit.tests.', []) | |||
| fake_http.get('{}/unit.tests/records/'.format(self.API_URL), | |||
| json=more_record) | |||
| fake_http.get('{}/'.format(self.API_URL), json=self.domain) | |||
| fake_http.head('{}/unit.tests/records/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.api_record))}) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| zone.add_record(Record.new(self.zone, 'unsup', { | |||
| 'ttl': 200, | |||
| 'type': 'NAPTR', | |||
| 'value': { | |||
| 'order': 40, | |||
| 'preference': 70, | |||
| 'flags': 'U', | |||
| 'service': 'SIP+D2U', | |||
| 'regexp': '!^.*$!sip:info@bar.example.com!', | |||
| 'replacement': '.', | |||
| } | |||
| })) | |||
| provider = SelectelProvider(123, 'secret_token') | |||
| provider.populate(zone) | |||
| self.assertNotEqual(self.expected, zone.records) | |||
| @requests_mock.Mocker() | |||
| def test_apply(self, fake_http): | |||
| fake_http.get('{}/unit.tests/records/'.format(self.API_URL), | |||
| json=list()) | |||
| fake_http.get('{}/'.format(self.API_URL), json=self.domain) | |||
| fake_http.head('{}/unit.tests/records/'.format(self.API_URL), | |||
| headers={'X-Total-Count': '0'}) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| fake_http.post('{}/100000/records/'.format(self.API_URL), json=list()) | |||
| provider = SelectelProvider(123, 'test_token') | |||
| zone = Zone('unit.tests.', []) | |||
| for record in self.expected: | |||
| zone.add_record(record) | |||
| plan = provider.plan(zone) | |||
| self.assertEquals(8, len(plan.changes)) | |||
| self.assertEquals(8, provider.apply(plan)) | |||
| @requests_mock.Mocker() | |||
| def test_domain_list(self, fake_http): | |||
| fake_http.get('{}/'.format(self.API_URL), json=self.domain) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| expected = {'unit.tests': self.domain[0]} | |||
| provider = SelectelProvider(123, 'test_token') | |||
| result = provider.domain_list() | |||
| self.assertEquals(result, expected) | |||
| @requests_mock.Mocker() | |||
| def test_authentication_fail(self, fake_http): | |||
| fake_http.get('{}/'.format(self.API_URL), status_code=401) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| with self.assertRaises(Exception) as ctx: | |||
| SelectelProvider(123, 'fail_token') | |||
| self.assertEquals(ctx.exception.message, | |||
| 'Authorization failed. Invalid or empty token.') | |||
| @requests_mock.Mocker() | |||
| def test_not_exist_domain(self, fake_http): | |||
| fake_http.get('{}/'.format(self.API_URL), status_code=404, json='') | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| fake_http.post('{}/'.format(self.API_URL), | |||
| json={"name": "unit.tests", | |||
| "create_date": 1507154178, | |||
| "id": 100000}) | |||
| fake_http.get('{}/unit.tests/records/'.format(self.API_URL), | |||
| json=list()) | |||
| fake_http.head('{}/unit.tests/records/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.api_record))}) | |||
| fake_http.post('{}/100000/records/'.format(self.API_URL), | |||
| json=list()) | |||
| provider = SelectelProvider(123, 'test_token') | |||
| zone = Zone('unit.tests.', []) | |||
| for record in self.expected: | |||
| zone.add_record(record) | |||
| plan = provider.plan(zone) | |||
| self.assertEquals(8, len(plan.changes)) | |||
| self.assertEquals(8, provider.apply(plan)) | |||
| @requests_mock.Mocker() | |||
| def test_delete_no_exist_record(self, fake_http): | |||
| fake_http.get('{}/'.format(self.API_URL), json=self.domain) | |||
| fake_http.get('{}/100000/records/'.format(self.API_URL), json=list()) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| fake_http.head('{}/unit.tests/records/'.format(self.API_URL), | |||
| headers={'X-Total-Count': '0'}) | |||
| provider = SelectelProvider(123, 'test_token') | |||
| zone = Zone('unit.tests.', []) | |||
| provider.delete_record('unit.tests', 'NS', zone) | |||
| @requests_mock.Mocker() | |||
| def test_change_record(self, fake_http): | |||
| exist_record = [self.aaaa_record, | |||
| {"content": "6.6.5.7", | |||
| "ttl": 100, | |||
| "type": "A", | |||
| "id": 100001, | |||
| "name": "delete.unit.tests"}, | |||
| {"content": "9.8.2.1", | |||
| "ttl": 100, | |||
| "type": "A", | |||
| "id": 100002, | |||
| "name": "unit.tests"}] # exist | |||
| fake_http.get('{}/unit.tests/records/'.format(self.API_URL), | |||
| json=exist_record) | |||
| fake_http.get('{}/'.format(self.API_URL), json=self.domain) | |||
| fake_http.get('{}/100000/records/'.format(self.API_URL), | |||
| json=exist_record) | |||
| fake_http.head('{}/unit.tests/records/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(exist_record))}) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| fake_http.head('{}/100000/records/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(exist_record))}) | |||
| fake_http.post('{}/100000/records/'.format(self.API_URL), | |||
| json=list()) | |||
| fake_http.delete('{}/100000/records/100001'.format(self.API_URL), | |||
| text="") | |||
| fake_http.delete('{}/100000/records/100002'.format(self.API_URL), | |||
| text="") | |||
| provider = SelectelProvider(123, 'test_token') | |||
| zone = Zone('unit.tests.', []) | |||
| for record in self.expected: | |||
| zone.add_record(record) | |||
| plan = provider.plan(zone) | |||
| self.assertEquals(8, len(plan.changes)) | |||
| self.assertEquals(8, provider.apply(plan)) | |||
| @requests_mock.Mocker() | |||
| def test_include_change_returns_false(self, fake_http): | |||
| fake_http.get('{}/'.format(self.API_URL), json=self.domain) | |||
| fake_http.head('{}/'.format(self.API_URL), | |||
| headers={'X-Total-Count': str(len(self.domain))}) | |||
| provider = SelectelProvider(123, 'test_token') | |||
| zone = Zone('unit.tests.', []) | |||
| exist_record = Record.new(zone, '', { | |||
| 'ttl': 60, | |||
| 'type': 'A', | |||
| 'values': ['1.1.1.1', '2.2.2.2'] | |||
| }) | |||
| new = Record.new(zone, '', { | |||
| 'ttl': 10, | |||
| 'type': 'A', | |||
| 'values': ['1.1.1.1', '2.2.2.2'] | |||
| }) | |||
| change = Update(exist_record, new) | |||
| include_change = provider._include_change(change) | |||
| self.assertFalse(include_change) | |||
| @ -0,0 +1,275 @@ | |||
| # | |||
| # | |||
| # | |||
| from __future__ import absolute_import, division, print_function, \ | |||
| unicode_literals | |||
| # from mock import Mock, call | |||
| from os.path import dirname, join | |||
| from suds import WebFault | |||
| from unittest import TestCase | |||
| from octodns.provider.transip import TransipProvider | |||
| from octodns.provider.yaml import YamlProvider | |||
| from octodns.zone import Zone | |||
| from transip.service.domain import DomainService | |||
| from transip.service.objects import DnsEntry | |||
| class MockFault(object): | |||
| faultstring = "" | |||
| faultcode = "" | |||
| def __init__(self, code, string, *args, **kwargs): | |||
| self.faultstring = string | |||
| self.faultcode = code | |||
| class MockResponse(object): | |||
| dnsEntries = [] | |||
| class MockDomainService(DomainService): | |||
| def __init__(self, *args, **kwargs): | |||
| super(MockDomainService, self).__init__('MockDomainService', *args, | |||
| **kwargs) | |||
| self.mockupEntries = [] | |||
| def mockup(self, records): | |||
| provider = TransipProvider('', '', '') | |||
| _dns_entries = [] | |||
| for record in records: | |||
| if record._type in provider.SUPPORTS: | |||
| entries_for = getattr(provider, | |||
| '_entries_for_{}'.format(record._type)) | |||
| # Root records have '@' as name | |||
| name = record.name | |||
| if name == '': | |||
| name = provider.ROOT_RECORD | |||
| _dns_entries.extend(entries_for(name, record)) | |||
| # NS is not supported as a DNS Entry, | |||
| # so it should cover the if statement | |||
| _dns_entries.append( | |||
| DnsEntry('@', '3600', 'NS', 'ns01.transip.nl.')) | |||
| self.mockupEntries = _dns_entries | |||
| # Skips authentication layer and returns the entries loaded by "Mockup" | |||
| def get_info(self, domain_name): | |||
| # Special 'domain' to trigger error | |||
| if str(domain_name) == str('notfound.unit.tests'): | |||
| self.raiseZoneNotFound() | |||
| result = MockResponse() | |||
| result.dnsEntries = self.mockupEntries | |||
| return result | |||
| def set_dns_entries(self, domain_name, dns_entries): | |||
| # Special 'domain' to trigger error | |||
| if str(domain_name) == str('failsetdns.unit.tests'): | |||
| self.raiseSaveError() | |||
| return True | |||
| def raiseZoneNotFound(self): | |||
| fault = MockFault(str('102'), '102 is zone not found') | |||
| document = {} | |||
| raise WebFault(fault, document) | |||
| def raiseInvalidAuth(self): | |||
| fault = MockFault(str('200'), '200 is invalid auth') | |||
| document = {} | |||
| raise WebFault(fault, document) | |||
| def raiseSaveError(self): | |||
| fault = MockFault(str('200'), '202 random error') | |||
| document = {} | |||
| raise WebFault(fault, document) | |||
| class TestTransipProvider(TestCase): | |||
| bogus_key = str("""-----BEGIN RSA PRIVATE KEY----- | |||
| MIIEowIBAAKCAQEA0U5HGCkLrz423IyUf3u4cKN2WrNz1x5KNr6PvH2M/zxas+zB | |||
| elbxkdT3AQ+wmfcIvOuTmFRTHv35q2um1aBrPxVw+2s+lWo28VwIRttwIB1vIeWu | |||
| lSBnkEZQRLyPI2tH0i5QoMX4CVPf9rvij3Uslimi84jdzDfPFIh6jZ6C8nLipOTG | |||
| 0IMhge1ofVfB0oSy5H+7PYS2858QLAf5ruYbzbAxZRivS402wGmQ0d0Lc1KxraAj | |||
| kiMM5yj/CkH/Vm2w9I6+tLFeASE4ub5HCP5G/ig4dbYtqZMQMpqyAbGxd5SOVtyn | |||
| UHagAJUxf8DT3I8PyjEHjxdOPUsxNyRtepO/7QIDAQABAoIBAQC7fiZ7gxE/ezjD | |||
| 2n6PsHFpHVTBLS2gzzZl0dCKZeFvJk6ODJDImaeuHhrh7X8ifMNsEI9XjnojMhl8 | |||
| MGPzy88mZHugDNK0H8B19x5G8v1/Fz7dG5WHas660/HFkS+b59cfdXOugYiOOn9O | |||
| 08HBBpLZNRUOmVUuQfQTjapSwGLG8PocgpyRD4zx0LnldnJcqYCxwCdev+AAsPnq | |||
| ibNtOd/MYD37w9MEGcaxLE8wGgkv8yd97aTjkgE+tp4zsM4QE4Rag133tsLLNznT | |||
| 4Qr/of15M3NW/DXq/fgctyRcJjZpU66eCXLCz2iRTnLyyxxDC2nwlxKbubV+lcS0 | |||
| S4hbfd/BAoGBAO8jXxEaiybR0aIhhSR5esEc3ymo8R8vBN3ZMJ+vr5jEPXr/ZuFj | |||
| /R4cZ2XV3VoQJG0pvIOYVPZ5DpJM7W+zSXtJ/7bLXy4Bnmh/rc+YYgC+AXQoLSil | |||
| iD2OuB2xAzRAK71DVSO0kv8gEEXCersPT2i6+vC2GIlJvLcYbOdRKWGxAoGBAOAQ | |||
| aJbRLtKujH+kMdoMI7tRlL8XwI+SZf0FcieEu//nFyerTePUhVgEtcE+7eQ7hyhG | |||
| fIXUFx/wALySoqFzdJDLc8U8pTLhbUaoLOTjkwnCTKQVprhnISqQqqh/0U5u47IE | |||
| RWzWKN6OHb0CezNTq80Dr6HoxmPCnJHBHn5LinT9AoGAQSpvZpbIIqz8pmTiBl2A | |||
| QQ2gFpcuFeRXPClKYcmbXVLkuhbNL1BzEniFCLAt4LQTaRf9ghLJ3FyCxwVlkpHV | |||
| zV4N6/8hkcTpKOraL38D/dXJSaEFJVVuee/hZl3tVJjEEpA9rDwx7ooLRSdJEJ6M | |||
| ciq55UyKBSdt4KssSiDI2RECgYBL3mJ7xuLy5bWfNsrGiVvD/rC+L928/5ZXIXPw | |||
| 26oI0Yfun7ulDH4GOroMcDF/GYT/Zzac3h7iapLlR0WYI47xxGI0A//wBZLJ3QIu | |||
| krxkDo2C9e3Y/NqnHgsbOQR3aWbiDT4wxydZjIeXS3LKA2fl6Hyc90PN3cTEOb8I | |||
| hq2gRQKBgEt0SxhhtyB93SjgTzmUZZ7PiEf0YJatfM6cevmjWHexrZH+x31PB72s | |||
| fH2BQyTKKzoCLB1k/6HRaMnZdrWyWSZ7JKz3AHJ8+58d0Hr8LTrzDM1L6BbjeDct | |||
| N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd | |||
| -----END RSA PRIVATE KEY-----""") | |||
| def make_expected(self): | |||
| expected = Zone('unit.tests.', []) | |||
| source = YamlProvider('test', join(dirname(__file__), 'config')) | |||
| source.populate(expected) | |||
| return expected | |||
| def test_init(self): | |||
| with self.assertRaises(Exception) as ctx: | |||
| TransipProvider('test', 'unittest') | |||
| self.assertEquals( | |||
| str('Missing `key` of `key_file` parameter in config'), | |||
| str(ctx.exception)) | |||
| TransipProvider('test', 'unittest', key=self.bogus_key) | |||
| # Existence and content of the key is tested in the SDK on client call | |||
| TransipProvider('test', 'unittest', key_file='/fake/path') | |||
| def test_populate(self): | |||
| _expected = self.make_expected() | |||
| # Unhappy Plan - Not authenticated | |||
| # Live test against API, will fail in an unauthorized error | |||
| with self.assertRaises(WebFault) as ctx: | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone, True) | |||
| self.assertEquals(str('WebFault'), | |||
| str(ctx.exception.__class__.__name__)) | |||
| self.assertEquals(str('200'), ctx.exception.fault.faultcode) | |||
| # Unhappy Plan - Zone does not exists | |||
| # Will trigger an exception if provider is used as a target for a | |||
| # non-existing zone | |||
| with self.assertRaises(Exception) as ctx: | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| zone = Zone('notfound.unit.tests.', []) | |||
| provider.populate(zone, True) | |||
| self.assertEquals(str('TransipNewZoneException'), | |||
| str(ctx.exception.__class__.__name__)) | |||
| self.assertEquals( | |||
| 'populate: (102) Transip used as target' + | |||
| ' for non-existing zone: notfound.unit.tests.', | |||
| ctx.exception.message) | |||
| # Happy Plan - Zone does not exists | |||
| # Won't trigger an exception if provider is NOT used as a target for a | |||
| # non-existing zone. | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| zone = Zone('notfound.unit.tests.', []) | |||
| provider.populate(zone, False) | |||
| # Happy Plan - Populate with mockup records | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| provider._client.mockup(_expected.records) | |||
| zone = Zone('unit.tests.', []) | |||
| provider.populate(zone, False) | |||
| # Transip allows relative values for types like cname, mx. | |||
| # Test is these are correctly appended with the domain | |||
| provider._currentZone = zone | |||
| self.assertEquals("www.unit.tests.", provider._parse_to_fqdn("www")) | |||
| self.assertEquals("www.unit.tests.", | |||
| provider._parse_to_fqdn("www.unit.tests.")) | |||
| self.assertEquals("www.sub.sub.sub.unit.tests.", | |||
| provider._parse_to_fqdn("www.sub.sub.sub")) | |||
| self.assertEquals("unit.tests.", | |||
| provider._parse_to_fqdn("@")) | |||
| # Happy Plan - Even if the zone has no records the zone should exist | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| zone = Zone('unit.tests.', []) | |||
| exists = provider.populate(zone, True) | |||
| self.assertTrue(exists, 'populate should return true') | |||
| return | |||
| def test_plan(self): | |||
| _expected = self.make_expected() | |||
| # Test Happy plan, only create | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| plan = provider.plan(_expected) | |||
| self.assertEqual(12, plan.change_counts['Create']) | |||
| self.assertEqual(0, plan.change_counts['Update']) | |||
| self.assertEqual(0, plan.change_counts['Delete']) | |||
| return | |||
| def test_apply(self): | |||
| _expected = self.make_expected() | |||
| # Test happy flow. Create all supoorted records | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| plan = provider.plan(_expected) | |||
| self.assertEqual(12, len(plan.changes)) | |||
| changes = provider.apply(plan) | |||
| self.assertEqual(changes, len(plan.changes)) | |||
| # Test unhappy flow. Trigger 'not found error' in apply stage | |||
| # This should normally not happen as populate will capture it first | |||
| # but just in case. | |||
| changes = [] # reset changes | |||
| with self.assertRaises(Exception) as ctx: | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| plan = provider.plan(_expected) | |||
| plan.desired.name = 'notfound.unit.tests.' | |||
| changes = provider.apply(plan) | |||
| # Changes should not be set due to an Exception | |||
| self.assertEqual([], changes) | |||
| self.assertEquals(str('WebFault'), | |||
| str(ctx.exception.__class__.__name__)) | |||
| self.assertEquals(str('102'), ctx.exception.fault.faultcode) | |||
| # Test unhappy flow. Trigger a unrecoverable error while saving | |||
| _expected = self.make_expected() # reset expected | |||
| changes = [] # reset changes | |||
| with self.assertRaises(Exception) as ctx: | |||
| provider = TransipProvider('test', 'unittest', self.bogus_key) | |||
| provider._client = MockDomainService('unittest', self.bogus_key) | |||
| plan = provider.plan(_expected) | |||
| plan.desired.name = 'failsetdns.unit.tests.' | |||
| changes = provider.apply(plan) | |||
| # Changes should not be set due to an Exception | |||
| self.assertEqual([], changes) | |||
| self.assertEquals(str('TransipException'), | |||
| str(ctx.exception.__class__.__name__)) | |||