From 6b9cdc336a62e6e628fd38f40f390af7ddac7e40 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 6 Jan 2022 09:36:25 -0800 Subject: [PATCH] Extract & shim DnsMadeEasyProvider --- CHANGELOG.md | 1 + README.md | 2 +- octodns/provider/dnsmadeeasy.py | 429 +-------------------- tests/fixtures/dnsmadeeasy-domains.json | 16 - tests/fixtures/dnsmadeeasy-records.json | 344 ----------------- tests/test_octodns_provider_dnsmadeeasy.py | 222 +---------- 6 files changed, 22 insertions(+), 992 deletions(-) delete mode 100644 tests/fixtures/dnsmadeeasy-domains.json delete mode 100644 tests/fixtures/dnsmadeeasy-records.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 420bdc4..9a2c7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * [ConstellixProvider](https://github.com/octodns/octodns-constellix/) * [DigitalOceanProvider](https://github.com/octodns/octodns-digitalocean/) * [DnsimpleProvider](https://github.com/octodns/octodns-dnsimple/) + * [DnsMadeEasyProvider](https://github.com/octodns/octodns-dnsmadeeasy/) * [Ns1Provider](https://github.com/octodns/octodns-ns1/) * [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/) * [Route53Provider](https://github.com/octodns/octodns-route53/) also diff --git a/README.md b/README.md index 3926cf6..9600744 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ The table below lists the providers octoDNS supports. We're currently in the pro | [CloudflareProvider](/octodns/provider/cloudflare.py) | | | A, AAAA, ALIAS, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT, URLFWD | No | CAA tags restricted | | [ConstellixProvider](https://github.com/octodns/octodns-constellix/) | [octodns_constellix](https://github.com/octodns/octodns-constellix/) | | | | | | [DigitalOceanProvider](https://github.com/octodns/octodns-digitalocean/) | [octodns_digitalocean](https://github.com/octodns/octodns-digitalocean/) | | | | | -| [DnsMadeEasyProvider](/octodns/provider/dnsmadeeasy.py) | | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted | +| [DnsMadeEasyProvider](https://github.com/octodns/octodns-dnsmadeeasy/) | [octodns_dnsmadeeasy](https://github.com/octodns/octodns-dnsmadeeasy/) | | | | | | [DnsimpleProvider](https://github.com/octodns/octodns-dnsimple/) | [octodns_dnsimple](https://github.com/octodns/octodns-dnsimple/) | | | | | | [DynProvider](/octodns/provider/dyn.py) | | dyn | All | Both | | | [EasyDNSProvider](/octodns/provider/easydns.py) | | | A, AAAA, CAA, CNAME, MX, NAPTR, NS, SRV, TXT | No | | diff --git a/octodns/provider/dnsmadeeasy.py b/octodns/provider/dnsmadeeasy.py index 9aab39c..68c718f 100644 --- a/octodns/provider/dnsmadeeasy.py +++ b/octodns/provider/dnsmadeeasy.py @@ -5,417 +5,18 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from collections import defaultdict -from requests import Session -from time import strftime, gmtime, sleep -import hashlib -import hmac -import logging - -from ..record import Record -from . import ProviderException -from .base import BaseProvider - - -class DnsMadeEasyClientException(ProviderException): - pass - - -class DnsMadeEasyClientBadRequest(DnsMadeEasyClientException): - - def __init__(self, resp): - errors = '\n - '.join(resp.json()['error']) - super(DnsMadeEasyClientBadRequest, self).__init__(f'\n - {errors}') - - -class DnsMadeEasyClientUnauthorized(DnsMadeEasyClientException): - - def __init__(self): - super(DnsMadeEasyClientUnauthorized, self).__init__('Unauthorized') - - -class DnsMadeEasyClientNotFound(DnsMadeEasyClientException): - - def __init__(self): - super(DnsMadeEasyClientNotFound, self).__init__('Not Found') - - -class DnsMadeEasyClient(object): - PRODUCTION = 'https://api.dnsmadeeasy.com/V2.0/dns/managed' - SANDBOX = 'https://api.sandbox.dnsmadeeasy.com/V2.0/dns/managed' - - def __init__(self, api_key, secret_key, sandbox=False, - ratelimit_delay=0.0): - self.api_key = api_key - self.secret_key = secret_key - self._base = self.SANDBOX if sandbox else self.PRODUCTION - self.ratelimit_delay = ratelimit_delay - self._sess = Session() - self._sess.headers.update({'x-dnsme-apiKey': self.api_key}) - self._domains = None - - def _current_time(self): - return strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) - - def _hmac_hash(self, now): - return hmac.new(self.secret_key.encode(), now.encode(), - hashlib.sha1).hexdigest() - - def _request(self, method, path, params=None, data=None): - now = self._current_time() - hmac_hash = self._hmac_hash(now) - - headers = { - 'x-dnsme-hmac': hmac_hash, - 'x-dnsme-requestDate': now - } - - url = f'{self._base}{path}' - resp = self._sess.request(method, url, headers=headers, - params=params, json=data) - if resp.status_code == 400: - raise DnsMadeEasyClientBadRequest(resp) - if resp.status_code in [401, 403]: - raise DnsMadeEasyClientUnauthorized() - if resp.status_code == 404: - raise DnsMadeEasyClientNotFound() - resp.raise_for_status() - sleep(self.ratelimit_delay) - return resp - - @property - def domains(self): - if self._domains is None: - zones = [] - - # has pages in resp, do we need paging? - resp = self._request('GET', '/').json() - zones += resp['data'] - - self._domains = {f'{z["name"]}.': z['id'] for z in zones} - - return self._domains - - def domain(self, name): - path = f'/id/{name}' - return self._request('GET', path).json() - - def domain_create(self, name): - self._request('POST', '/', data={'name': name}) - - def records(self, zone_name): - zone_id = self.domains.get(zone_name, False) - path = f'/{zone_id}/records' - ret = [] - - # has pages in resp, do we need paging? - resp = self._request('GET', path).json() - ret += resp['data'] - - for record in ret: - # 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 value == '': - record['value'] = zone_name - elif not value.endswith('.'): - record['value'] = f'{value}.{zone_name}' - - return ret - - def record_create(self, zone_name, params): - zone_id = self.domains.get(zone_name, False) - path = f'/{zone_id}/records' - - # change ALIAS records to ANAME - if params['type'] == 'ALIAS': - params['type'] = 'ANAME' - - self._request('POST', path, data=params) - - def record_delete(self, zone_name, record_id): - zone_id = self.domains.get(zone_name, False) - path = f'/{zone_id}/records/{record_id}' - self._request('DELETE', path) - - -class DnsMadeEasyProvider(BaseProvider): - ''' - DNSMadeEasy DNS provider using v2.0 API - - dnsmadeeasy: - class: octodns.provider.dnsmadeeasy.DnsMadeEasyProvider - # Your DnsMadeEasy api key (required) - api_key: env/DNSMADEEASY_API_KEY - # Your DnsMadeEasy secret key (required) - secret_key: env/DNSMADEEASY_SECRET_KEY - # Whether or not to use Sandbox environment - # (optional, default is false) - sandbox: true - ''' - 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, sandbox=False, - ratelimit_delay=0.0, *args, **kwargs): - self.log = logging.getLogger(f'DnsMadeEasyProvider[{id}]') - self.log.debug('__init__: id=%s, api_key=***, secret_key=***, ' - 'sandbox=%s', id, sandbox) - super(DnsMadeEasyProvider, self).__init__(id, *args, **kwargs) - self._client = DnsMadeEasyClient(api_key, secret_key, sandbox, - ratelimit_delay) - - self._zone_records = {} - - def _data_for_multiple(self, _type, records): - return { - 'ttl': records[0]['ttl'], - 'type': _type, - 'values': [r['value'] for r in records] - } - - _data_for_A = _data_for_multiple - _data_for_AAAA = _data_for_multiple - _data_for_NS = _data_for_multiple - - def _data_for_CAA(self, _type, records): - values = [] - for record in records: - values.append({ - 'flags': record['issuerCritical'], - 'tag': record['caaType'], - 'value': record['value'][1:-1] - }) - return { - 'ttl': records[0]['ttl'], - 'type': _type, - 'values': values - } - - def _data_for_TXT(self, _type, records): - values = [value['value'].replace(';', '\\;') for value in records] - return { - 'ttl': records[0]['ttl'], - 'type': _type, - 'values': values - } - - _data_for_SPF = _data_for_TXT - - def _data_for_MX(self, _type, records): - values = [] - for record in records: - values.append({ - 'preference': record['mxLevel'], - 'exchange': record['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 - _data_for_PTR = _data_for_single - _data_for_ALIAS = _data_for_single - - def _data_for_SRV(self, _type, records): - values = [] - for record in records: - values.append({ - 'port': record['port'], - 'priority': record['priority'], - 'target': record['value'], - 'weight': record['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 DnsMadeEasyClientNotFound: - 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, f'_data_for_{_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 supports(self, record): - # DNS Made Easy does not support empty/NULL SRV records - # - # Attempting to sync such a record would generate the following error - # - # octodns.provider.dnsmadeeasy.DnsMadeEasyClientBadRequest: - # - Record value may not be a standalone dot. - # - # Skip the record and continue - if record._type == "SRV": - if 'value' in record.data: - targets = (record.data['value']['target'],) - else: - targets = [value['target'] for value in record.data['values']] - - if "." in targets: - self.log.warning( - 'supports: unsupported %s record with target (%s)', - record._type, targets - ) - return False - - return super(DnsMadeEasyProvider, self).supports(record) - - def _params_for_multiple(self, record): - for value in record.values: - yield { - 'value': value, - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type - } - - _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 { - 'value': record.value, - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type - } - - _params_for_CNAME = _params_for_single - _params_for_PTR = _params_for_single - _params_for_ALIAS = _params_for_single - - def _params_for_MX(self, record): - for value in record.values: - yield { - 'value': value.exchange, - 'name': record.name, - 'mxLevel': value.preference, - 'ttl': record.ttl, - 'type': record._type - } - - def _params_for_SRV(self, record): - for value in record.values: - yield { - 'value': value.target, - 'name': record.name, - 'port': value.port, - 'priority': value.priority, - 'ttl': record.ttl, - 'type': record._type, - 'weight': value.weight - } - - def _params_for_TXT(self, record): - # DNSMadeEasy does not want values escaped - for value in record.chunked_values: - yield { - 'value': value.replace('\\;', ';'), - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type - } - - _params_for_SPF = _params_for_TXT - - def _params_for_CAA(self, record): - for value in record.values: - yield { - 'value': value.value, - 'issuerCritical': value.flags, - 'name': record.name, - 'caaType': value.tag, - 'ttl': record.ttl, - 'type': record._type - } - - def _apply_Create(self, change): - new = change.new - params_for = getattr(self, f'_params_for_{new._type}') - for params in params_for(new): - self._client.record_create(new.zone.name, 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['id']) - - def _apply(self, plan): - desired = plan.desired - changes = plan.changes - self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, - len(changes)) - - domain_name = desired.name[:-1] - try: - self._client.domain(domain_name) - except DnsMadeEasyClientNotFound: - self.log.debug('_apply: no matching zone, creating domain') - self._client.domain_create(domain_name) - - for change in changes: - class_name = change.__class__.__name__ - getattr(self, f'_apply_{class_name}')(change) - - # Clear out the cache if any - self._zone_records.pop(desired.name, None) +from logging import getLogger + +logger = getLogger('DnsMadeEasy') +try: + logger.warn('octodns_dnsmadeeasy shimmed. Update your provider class to ' + 'octodns_dnsmadeeasy.DnsMadeEasyProvider. ' + 'Shim will be removed in 1.0') + from octodns_dnsmadeeasy import DnsMadeEasyProvider + DnsMadeEasyProvider # pragma: no cover +except ModuleNotFoundError: + logger.exception('DnsMadeEasyProvider has been moved into a seperate ' + 'module, octodns_dnsmadeeasy is now required. Provider ' + 'class should be updated to ' + 'octodns_dnsmadeeasy.DnsMadeEasyProvider') + raise diff --git a/tests/fixtures/dnsmadeeasy-domains.json b/tests/fixtures/dnsmadeeasy-domains.json deleted file mode 100644 index de7f7db..0000000 --- a/tests/fixtures/dnsmadeeasy-domains.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "totalPages": 1, - "totalRecords": 1, - "data": [{ - "created": 1511740800000, - "folderId": 1990, - "gtdEnabled": false, - "pendingActionId": 0, - "updated": 1511766661574, - "processMulti": false, - "activeThirdParties": [], - "name": "unit.tests", - "id": 123123 - }], - "page": 0 -} \ No newline at end of file diff --git a/tests/fixtures/dnsmadeeasy-records.json b/tests/fixtures/dnsmadeeasy-records.json deleted file mode 100644 index aefd6ce..0000000 --- a/tests/fixtures/dnsmadeeasy-records.json +++ /dev/null @@ -1,344 +0,0 @@ -{ - "totalPages": 1, - "totalRecords": 23, - "data": [{ - "failover": false, - "monitor": false, - "sourceId": 123123, - "caaType": "issue", - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "issuerCritical": 0, - "ttl": 3600, - "source": 1, - "name": "", - "value": "\"ca.unit.tests\"", - "id": 11189874, - "type": "CAA" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 300, - "source": 1, - "name": "", - "value": "1.2.3.4", - "id": 11189875, - "type": "A" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 300, - "source": 1, - "name": "", - "value": "1.2.3.5", - "id": 11189876, - "type": "A" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 600, - "weight": 20, - "source": 1, - "name": "_srv._tcp", - "value": "foo-1.unit.tests.", - "id": 11189877, - "priority": 10, - "type": "SRV", - "port": 30 - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 600, - "weight": 20, - "source": 1, - "name": "_srv._tcp", - "value": "foo-2.unit.tests.", - "id": 11189878, - "priority": 12, - "type": "SRV", - "port": 30 - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 600, - "source": 1, - "name": "aaaa", - "value": "2601:644:500:e210:62f8:1dff:feb8:947a", - "id": 11189879, - "type": "AAAA" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 300, - "source": 1, - "name": "cname", - "value": "", - "id": 11189880, - "type": "CNAME" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 3600, - "source": 1, - "name": "included", - "value": "", - "id": 11189881, - "type": "CNAME" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "mxLevel": 30, - "ttl": 300, - "source": 1, - "name": "mx", - "value": "smtp-3.unit.tests.", - "id": 11189882, - "type": "MX" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "mxLevel": 20, - "ttl": 300, - "source": 1, - "name": "mx", - "value": "smtp-2.unit.tests.", - "id": 11189883, - "type": "MX" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "mxLevel": 10, - "ttl": 300, - "source": 1, - "name": "mx", - "value": "smtp-4.unit.tests.", - "id": 11189884, - "type": "MX" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "mxLevel": 40, - "ttl": 300, - "source": 1, - "name": "mx", - "value": "smtp-1.unit.tests.", - "id": 11189885, - "type": "MX" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 600, - "source": 1, - "name": "spf", - "value": "\"v=spf1 ip4:192.168.0.1/16-all\"", - "id": 11189886, - "type": "SPF" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 600, - "source": 1, - "name": "txt", - "value": "\"Bah bah black sheep\"", - "id": 11189887, - "type": "TXT" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 600, - "source": 1, - "name": "txt", - "value": "\"have you any wool.\"", - "id": 11189888, - "type": "TXT" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 600, - "source": 1, - "name": "txt", - "value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"", - "id": 11189889, - "type": "TXT" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 3600, - "source": 1, - "name": "under", - "value": "ns1.unit.tests.", - "id": 11189890, - "type": "NS" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 3600, - "source": 1, - "name": "under", - "value": "ns2", - "id": 11189891, - "type": "NS" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 300, - "source": 1, - "name": "www", - "value": "2.2.3.6", - "id": 11189892, - "type": "A" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 300, - "source": 1, - "name": "www.sub", - "value": "2.2.3.6", - "id": 11189893, - "type": "A" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 300, - "source": 1, - "name": "ptr", - "value": "foo.bar.com.", - "id": 11189894, - "type": "PTR" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": false, - "ttl": 1800, - "source": 1, - "name": "", - "value": "aname.unit.tests.", - "id": 11189895, - "type": "ANAME" - }, { - "failover": false, - "monitor": false, - "sourceId": 123123, - "dynamicDns": false, - "failed": false, - "gtdLocation": "DEFAULT", - "hardLink": true, - "ttl": 1800, - "source": 1, - "name": "unsupported", - "value": "https://redirect.unit.tests", - "id": 11189897, - "title": "Unsupported Record", - "keywords": "unsupported", - "redirectType": "Standard - 302", - "description": "unsupported record", - "type": "HTTPRED" - }], - "page": 0 -} diff --git a/tests/test_octodns_provider_dnsmadeeasy.py b/tests/test_octodns_provider_dnsmadeeasy.py index e0b21a6..968ae30 100644 --- a/tests/test_octodns_provider_dnsmadeeasy.py +++ b/tests/test_octodns_provider_dnsmadeeasy.py @@ -2,227 +2,15 @@ # # - 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.dnsmadeeasy import DnsMadeEasyClientNotFound, \ - DnsMadeEasyProvider -from octodns.provider.yaml import YamlProvider -from octodns.zone import Zone -import json +class TestDnsMadeEasyShim(TestCase): - -class TestDnsMadeEasyProvider(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.' - })) - - for record in list(expected.records): - if record.name == 'sub' and record._type == 'NS': - expected._remove_record(record) - break - - def test_populate(self): - provider = DnsMadeEasyProvider('test', 'api', 'secret') - - # Bad auth - with requests_mock() as mock: - mock.get(ANY, status_code=401, - text='{"error": ["API key not found"]}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals('Unauthorized', str(ctx.exception)) - - # Bad request - with requests_mock() as mock: - mock.get(ANY, status_code=400, - text='{"error": ["Rate limit exceeded"]}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals('\n - Rate limit exceeded', str(ctx.exception)) - - # 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='') - - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals(set(), zone.records) - - # No diffs == no changes - with requests_mock() as mock: - base = 'https://api.dnsmadeeasy.com/V2.0/dns/managed' - with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: - mock.get(f'{base}/', text=fh.read()) - with open('tests/fixtures/dnsmadeeasy-records.json') as fh: - mock.get(f'{base}/123123/records', text=fh.read()) - - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals(14, 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(14, len(again.records)) - - # bust the cache - del provider._zone_records[zone.name] - - def test_apply(self): - # Create provider with sandbox enabled - provider = DnsMadeEasyProvider('test', 'api', 'secret', True) - - resp = Mock() - resp.json = Mock() - provider._client._request = Mock(return_value=resp) - - with open('tests/fixtures/dnsmadeeasy-domains.json') as fh: - domains = json.load(fh) - - # non-existent domain, create everything - resp.json.side_effect = [ - DnsMadeEasyClientNotFound, # no zone in populate - DnsMadeEasyClientNotFound, # no domain during apply - domains - ] - plan = provider.plan(self.expected) - - # No root NS, no ignored, no excluded, no unsupported - n = len(self.expected.records) - 10 - self.assertEquals(n, len(plan.changes)) - self.assertEquals(n, provider.apply(plan)) - - provider._client._request.assert_has_calls([ - # created the domain - call('POST', '/', data={'name': 'unit.tests'}), - # get all domains to build the cache - call('GET', '/'), - # created at least some of the record with expected data - call('POST', '/123123/records', data={ - 'type': 'A', - 'name': '', - 'value': '1.2.3.4', - 'ttl': 300}), - call('POST', '/123123/records', data={ - 'type': 'A', - 'name': '', - 'value': '1.2.3.5', - 'ttl': 300}), - call('POST', '/123123/records', data={ - 'type': 'ANAME', - 'name': '', - 'value': 'aname.unit.tests.', - 'ttl': 1800}), - call('POST', '/123123/records', data={ - 'name': '', - 'value': 'ca.unit.tests', - 'issuerCritical': 0, 'caaType': 'issue', - 'ttl': 3600, 'type': 'CAA'}), - call('POST', '/123123/records', data={ - 'name': '_srv._tcp', - 'weight': 20, - 'value': 'foo-1.unit.tests.', - 'priority': 10, - 'ttl': 600, - 'type': 'SRV', - 'port': 30 - }), - ]) - self.assertEquals(26, provider._client._request.call_count) - - provider._client._request.reset_mock() - - # delete 1 and update 1 - provider._client.records = Mock(return_value=[ - { - 'id': 11189897, - 'name': 'www', - 'value': '1.2.3.4', - 'ttl': 300, - 'type': 'A', - }, - { - 'id': 11189898, - 'name': 'www', - 'value': '2.2.3.4', - 'ttl': 300, - 'type': 'A', - }, - { - 'id': 11189899, - 'name': 'ttl', - 'value': '3.2.3.4', - 'ttl': 600, - 'type': 'A', - } - ]) - - # 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', data={ - 'value': '3.2.3.4', - 'type': 'A', - 'name': 'ttl', - 'ttl': 300 - }), - call('DELETE', '/123123/records/11189899'), - call('DELETE', '/123123/records/11189897'), - call('DELETE', '/123123/records/11189898') - ], any_order=True) + def test_missing(self): + with self.assertRaises(ModuleNotFoundError): + from octodns.provider.dnsmadeeasy import DnsMadeEasyProvider + DnsMadeEasyProvider