From 8d3cc818aab1aa31845f3ccd94cc985a9ee18df2 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Fri, 31 Dec 2021 14:30:13 -0800 Subject: [PATCH] Extract DnsimpleProvider to octodns_dnsimple --- README.md | 2 +- octodns/provider/dnsimple.py | 459 +------------------ tests/fixtures/dnsimple-invalid-content.json | 106 ----- tests/fixtures/dnsimple-page-1.json | 314 ------------- tests/fixtures/dnsimple-page-2.json | 202 -------- tests/test_octodns_provider_dnsimple.py | 229 +-------- 6 files changed, 20 insertions(+), 1292 deletions(-) delete mode 100644 tests/fixtures/dnsimple-invalid-content.json delete mode 100644 tests/fixtures/dnsimple-page-1.json delete mode 100644 tests/fixtures/dnsimple-page-2.json diff --git a/README.md b/README.md index 1f42ca4..a7c8a7e 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ The table below lists the providers octoDNS supports. We're currently in the pro | [ConstellixProvider](/octodns/provider/constellix.py) | | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | Yes | CAA tags restricted | | [DigitalOceanProvider](/octodns/provider/digitalocean.py) | | | A, AAAA, CAA, CNAME, MX, NS, TXT, SRV | No | CAA tags restricted | | [DnsMadeEasyProvider](/octodns/provider/dnsmadeeasy.py) | | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted | -| [DnsimpleProvider](/octodns/provider/dnsimple.py) | | | All | No | CAA tags restricted | +| [DnsimpleProvider](https://github.com/octodns/octodns-powerdns/) | [octodns_dnsimple](https://github.com/octodns/octodns-powerdns/) | | | | | | [DynProvider](/octodns/provider/dyn.py) | | dyn | All | Both | | | [EasyDNSProvider](/octodns/provider/easydns.py) | | | A, AAAA, CAA, CNAME, MX, NAPTR, NS, SRV, TXT | No | | | [EtcHostsProvider](/octodns/provider/etc_hosts.py) | | | A, AAAA, ALIAS, CNAME | No | | diff --git a/octodns/provider/dnsimple.py b/octodns/provider/dnsimple.py index 1ce1673..7be4fcb 100644 --- a/octodns/provider/dnsimple.py +++ b/octodns/provider/dnsimple.py @@ -5,448 +5,17 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from collections import defaultdict -from requests import Session -import logging - -from ..record import Record -from . import ProviderException -from .base import BaseProvider - - -class DnsimpleClientException(ProviderException): - pass - - -class DnsimpleClientNotFound(DnsimpleClientException): - - def __init__(self): - super(DnsimpleClientNotFound, self).__init__('Not found') - - -class DnsimpleClientUnauthorized(DnsimpleClientException): - - def __init__(self): - super(DnsimpleClientUnauthorized, self).__init__('Unauthorized') - - -class DnsimpleClient(object): - - def __init__(self, token, account, sandbox): - self.account = account - sess = Session() - sess.headers.update({'Authorization': f'Bearer {token}'}) - self._sess = sess - if sandbox: - self.base = 'https://api.sandbox.dnsimple.com/v2/' - else: - self.base = 'https://api.dnsimple.com/v2/' - - def _request(self, method, path, params=None, data=None): - url = f'{self.base}{self.account}{path}' - resp = self._sess.request(method, url, params=params, json=data) - if resp.status_code == 401: - raise DnsimpleClientUnauthorized() - if resp.status_code == 404: - raise DnsimpleClientNotFound() - resp.raise_for_status() - return resp - - def zone(self, name): - path = f'/zones/{name}' - return self._request('GET', path).json() - - def domain_create(self, name): - return self._request('POST', '/domains', data={'name': name}) - - def records(self, zone_name): - ret = [] - - page = 1 - while True: - data = self._request('GET', f'/zones/{zone_name}/records', - {'page': page}).json() - ret += data['data'] - pagination = data['pagination'] - if page >= pagination['total_pages']: - break - page += 1 - - return ret - - def record_create(self, zone_name, params): - path = f'/zones/{zone_name}/records' - self._request('POST', path, data=params) - - def record_delete(self, zone_name, record_id): - path = f'/zones/{zone_name}/records/{record_id}' - self._request('DELETE', path) - - -class DnsimpleProvider(BaseProvider): - ''' - Dnsimple provider using API v2 - - dnsimple: - class: octodns.provider.dnsimple.DnsimpleProvider - # API v2 account access token (required) - token: letmein - # Your account number (required) - account: 42 - # Use sandbox (optional) - sandbox: true - ''' - SUPPORTS_GEO = False - SUPPORTS_DYNAMIC = False - SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', - 'PTR', 'SPF', 'SRV', 'SSHFP', 'TXT')) - - def __init__(self, id, token, account, sandbox=False, *args, **kwargs): - self.log = logging.getLogger(f'DnsimpleProvider[{id}]') - self.log.debug('__init__: id=%s, token=***, account=%s', id, account) - super(DnsimpleProvider, self).__init__(id, *args, **kwargs) - self._client = DnsimpleClient(token, account, sandbox) - - self._zone_records = {} - - def _data_for_multiple(self, _type, records): - return { - 'ttl': records[0]['ttl'], - 'type': _type, - 'values': [r['content'] for r in records] - } - - _data_for_A = _data_for_multiple - _data_for_AAAA = _data_for_multiple - _data_for_SPF = _data_for_multiple - - def _data_for_TXT(self, _type, records): - return { - 'ttl': records[0]['ttl'], - 'type': _type, - # escape semicolons - 'values': [r['content'].replace(';', '\\;') for r in records] - } - - def _data_for_CAA(self, _type, records): - values = [] - for record in records: - flags, tag, value = record['content'].split(' ') - values.append({ - 'flags': flags, - 'tag': tag, - 'value': value[1:-1], - }) - return { - 'ttl': records[0]['ttl'], - 'type': _type, - 'values': values - } - - def _data_for_CNAME(self, _type, records): - record = records[0] - return { - 'ttl': record['ttl'], - 'type': _type, - 'value': f'{record["content"]}.' - } - - _data_for_ALIAS = _data_for_CNAME - - def _data_for_MX(self, _type, records): - values = [] - for record in records: - values.append({ - 'preference': record['priority'], - 'exchange': f'{record["content"]}.' - }) - return { - 'ttl': records[0]['ttl'], - 'type': _type, - 'values': values - } - - def _data_for_NAPTR(self, _type, records): - values = [] - for record in records: - try: - order, preference, flags, service, regexp, replacement = \ - record['content'].split(' ', 5) - except ValueError: - # their api will let you create invalid records, this - # essentially handles that by ignoring them for values - # purposes. That will cause updates to happen to delete them if - # they shouldn't exist or update them if they're wrong - continue - values.append({ - 'flags': flags[1:-1], - 'order': order, - 'preference': preference, - 'regexp': regexp[1:-1], - 'replacement': replacement, - 'service': service[1:-1], - }) - return { - 'type': _type, - 'ttl': records[0]['ttl'], - 'values': values - } - - def _data_for_NS(self, _type, records): - values = [] - for record in records: - content = record['content'] - if content[-1] != '.': - content = f'{content}.' - values.append(content) - return { - 'ttl': records[0]['ttl'], - 'type': _type, - 'values': values, - } - - def _data_for_PTR(self, _type, records): - record = records[0] - return { - 'ttl': record['ttl'], - 'type': _type, - 'value': record['content'] - } - - def _data_for_SRV(self, _type, records): - values = [] - for record in records: - try: - weight, port, target = record['content'].split(' ', 2) - except ValueError: - # their api/website will let you create invalid records, this - # essentially handles that by ignoring them for values - # purposes. That will cause updates to happen to delete them if - # they shouldn't exist or update them if they're wrong - self.log.warning( - '_data_for_SRV: unsupported %s record (%s)', - _type, - record['content'] - ) - continue - - target = f'{target}.' if target != "." else "." - - values.append({ - 'port': port, - 'priority': record['priority'], - 'target': target, - 'weight': weight - }) - return { - 'type': _type, - 'ttl': records[0]['ttl'], - 'values': values - } - - def _data_for_SSHFP(self, _type, records): - values = [] - for record in records: - try: - algorithm, fingerprint_type, fingerprint = \ - record['content'].split(' ', 2) - except ValueError: - # see _data_for_NAPTR's continue - continue - values.append({ - 'algorithm': algorithm, - 'fingerprint': fingerprint, - 'fingerprint_type': fingerprint_type - }) - 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[:-1]) - except DnsimpleClientNotFound: - 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 - elif _type == 'TXT' and record['content'].startswith('ALIAS for'): - # ALIAS has a "ride along" TXT record with 'ALIAS for XXXX', - # we're ignoring it - 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): - # DNSimple does not support empty/NULL SRV records - # - # Fails silently and leaves a corrupt record - # - # 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(DnsimpleProvider, self).supports(record) - - def _params_for_multiple(self, record): - for value in record.values: - yield { - 'content': value, - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type, - } - - _params_for_A = _params_for_multiple - _params_for_AAAA = _params_for_multiple - _params_for_NS = _params_for_multiple - _params_for_SPF = _params_for_multiple - - def _params_for_TXT(self, record): - for value in record.values: - yield { - # un-escape semicolons - 'content': value.replace('\\', ''), - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type, - } - - def _params_for_CAA(self, record): - for value in record.values: - yield { - 'content': f'{value.flags} {value.tag} "{value.value}"', - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type - } - - def _params_for_single(self, record): - yield { - 'content': record.value, - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type - } - - _params_for_ALIAS = _params_for_single - _params_for_CNAME = _params_for_single - _params_for_PTR = _params_for_single - - def _params_for_MX(self, record): - for value in record.values: - yield { - 'content': value.exchange, - 'name': record.name, - 'priority': value.preference, - 'ttl': record.ttl, - 'type': record._type - } - - def _params_for_NAPTR(self, record): - for value in record.values: - content = f'{value.order} {value.preference} "{value.flags}" ' \ - f'"{value.service}" "{value.preference}" {value.flags}' - yield { - 'content': content, - 'name': record.name, - 'ttl': record.ttl, - 'type': record._type - } - - def _params_for_SRV(self, record): - for value in record.values: - yield { - 'content': f'{value.weight} {value.port} {value.target}', - 'name': record.name, - 'priority': value.priority, - 'ttl': record.ttl, - 'type': record._type - } - - def _params_for_SSHFP(self, record): - for value in record.values: - yield { - 'content': f'{value.algorithm} {value.fingerprint_type} ' - f'{value.fingerprint}', - 'name': record.name, - '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[:-1], 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[:-1], 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.zone(domain_name) - except DnsimpleClientNotFound: - 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('Dnsimple') +try: + logger.warn('octodns_dnsimple shimmed. Update your provider class to ' + 'octodns_dnsimple.DnsimpleProvider. ' + 'Shim will be removed in 1.0') + from octodns_dnsimple import DnsimpleProvider + DnsimpleProvider # pragma: no cover +except ModuleNotFoundError: + logger.exception('DnsimpleProvider has been moved into a seperate module, ' + 'octodns_dnsimple is now required. Provider class should ' + 'be updated to octodns_dnsimple.DnsimpleProvider') + raise diff --git a/tests/fixtures/dnsimple-invalid-content.json b/tests/fixtures/dnsimple-invalid-content.json deleted file mode 100644 index 4e6e10b..0000000 --- a/tests/fixtures/dnsimple-invalid-content.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "data": [ - { - "id": 11189898, - "zone_id": "unit.tests", - "parent_id": null, - "name": "naptr", - "content": "", - "ttl": 600, - "priority": null, - "type": "NAPTR", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:11Z", - "updated_at": "2017-03-09T15:55:11Z" - }, - { - "id": 11189899, - "zone_id": "unit.tests", - "parent_id": null, - "name": "naptr", - "content": "100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", - "ttl": 600, - "priority": null, - "type": "NAPTR", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:11Z", - "updated_at": "2017-03-09T15:55:11Z" - }, - { - "id": 11189878, - "zone_id": "unit.tests", - "parent_id": null, - "name": "_srv._tcp", - "content": "", - "ttl": 600, - "priority": 10, - "type": "SRV", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189879, - "zone_id": "unit.tests", - "parent_id": null, - "name": "_srv._tcp", - "content": "20 foo-2.unit.tests", - "ttl": 600, - "priority": 12, - "type": "SRV", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189882, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "", - "ttl": 3600, - "priority": null, - "type": "SSHFP", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189883, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "1 1", - "ttl": 3600, - "priority": null, - "type": "SSHFP", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - } - ], - "pagination": { - "current_page": 1, - "per_page": 20, - "total_entries": 6, - "total_pages": 1 - } -} diff --git a/tests/fixtures/dnsimple-page-1.json b/tests/fixtures/dnsimple-page-1.json deleted file mode 100644 index fca2111..0000000 --- a/tests/fixtures/dnsimple-page-1.json +++ /dev/null @@ -1,314 +0,0 @@ -{ - "data": [ - { - "id": 11189873, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "ns1.dnsimple.com admin.dnsimple.com 1489074932 86400 7200 604800 300", - "ttl": 3600, - "priority": null, - "type": "SOA", - "regions": [ - "global" - ], - "system_record": true, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:56:21Z" - }, - { - "id": 11189874, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "ns1.dnsimple.com", - "ttl": 3600, - "priority": null, - "type": "NS", - "regions": [ - "global" - ], - "system_record": true, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189875, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "ns2.dnsimple.com", - "ttl": 3600, - "priority": null, - "type": "NS", - "regions": [ - "global" - ], - "system_record": true, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189876, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "ns3.dnsimple.com", - "ttl": 3600, - "priority": null, - "type": "NS", - "regions": [ - "global" - ], - "system_record": true, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189877, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "ns4.dnsimple.com", - "ttl": 3600, - "priority": null, - "type": "NS", - "regions": [ - "global" - ], - "system_record": true, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189878, - "zone_id": "unit.tests", - "parent_id": null, - "name": "_srv._tcp", - "content": "20 30 foo-1.unit.tests", - "ttl": 600, - "priority": 10, - "type": "SRV", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189879, - "zone_id": "unit.tests", - "parent_id": null, - "name": "_srv._tcp", - "content": "20 30 foo-2.unit.tests", - "ttl": 600, - "priority": 12, - "type": "SRV", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189880, - "zone_id": "unit.tests", - "parent_id": null, - "name": "under", - "content": "ns1.unit.tests.", - "ttl": 3600, - "priority": null, - "type": "NS", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189881, - "zone_id": "unit.tests", - "parent_id": null, - "name": "under", - "content": "ns2.unit.tests.", - "ttl": 3600, - "priority": null, - "type": "NS", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189882, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49", - "ttl": 3600, - "priority": null, - "type": "SSHFP", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:08Z", - "updated_at": "2017-03-09T15:55:08Z" - }, - { - "id": 11189883, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73", - "ttl": 3600, - "priority": null, - "type": "SSHFP", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 11189884, - "zone_id": "unit.tests", - "parent_id": null, - "name": "txt", - "content": "Bah bah black sheep", - "ttl": 600, - "priority": null, - "type": "TXT", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 11189885, - "zone_id": "unit.tests", - "parent_id": null, - "name": "txt", - "content": "have you any wool.", - "ttl": 600, - "priority": null, - "type": "TXT", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 11189886, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "1.2.3.4", - "ttl": 300, - "priority": null, - "type": "A", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 11189887, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "1.2.3.5", - "ttl": 300, - "priority": null, - "type": "A", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 11189889, - "zone_id": "unit.tests", - "parent_id": null, - "name": "www", - "content": "2.2.3.6", - "ttl": 300, - "priority": null, - "type": "A", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 11189890, - "zone_id": "unit.tests", - "parent_id": null, - "name": "mx", - "content": "smtp-4.unit.tests", - "ttl": 300, - "priority": 10, - "type": "MX", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - }, - { - "id": 11189891, - "zone_id": "unit.tests", - "parent_id": null, - "name": "mx", - "content": "smtp-2.unit.tests", - "ttl": 300, - "priority": 20, - "type": "MX", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - }, - { - "id": 11189892, - "zone_id": "unit.tests", - "parent_id": null, - "name": "mx", - "content": "smtp-3.unit.tests", - "ttl": 300, - "priority": 30, - "type": "MX", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - } - ], - "pagination": { - "current_page": 1, - "per_page": 20, - "total_entries": 29, - "total_pages": 2 - } -} diff --git a/tests/fixtures/dnsimple-page-2.json b/tests/fixtures/dnsimple-page-2.json deleted file mode 100644 index c12c4f4..0000000 --- a/tests/fixtures/dnsimple-page-2.json +++ /dev/null @@ -1,202 +0,0 @@ -{ - "data": [ - { - "id": 11189893, - "zone_id": "unit.tests", - "parent_id": null, - "name": "mx", - "content": "smtp-1.unit.tests", - "ttl": 300, - "priority": 40, - "type": "MX", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - }, - { - "id": 11189894, - "zone_id": "unit.tests", - "parent_id": null, - "name": "aaaa", - "content": "2601:644:500:e210:62f8:1dff:feb8:947a", - "ttl": 600, - "priority": null, - "type": "AAAA", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - }, - { - "id": 11189895, - "zone_id": "unit.tests", - "parent_id": null, - "name": "cname", - "content": "unit.tests", - "ttl": 300, - "priority": null, - "type": "CNAME", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - }, - { - "id": 11189896, - "zone_id": "unit.tests", - "parent_id": null, - "name": "ptr", - "content": "foo.bar.com.", - "ttl": 300, - "priority": null, - "type": "PTR", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - }, - { - "id": 11189897, - "zone_id": "unit.tests", - "parent_id": null, - "name": "www.sub", - "content": "2.2.3.6", - "ttl": 300, - "priority": null, - "type": "A", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:10Z", - "updated_at": "2017-03-09T15:55:10Z" - }, - { - "id": 11189898, - "zone_id": "unit.tests", - "parent_id": null, - "name": "naptr", - "content": "10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", - "ttl": 600, - "priority": null, - "type": "NAPTR", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:11Z", - "updated_at": "2017-03-09T15:55:11Z" - }, - { - "id": 11189899, - "zone_id": "unit.tests", - "parent_id": null, - "name": "naptr", - "content": "100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", - "ttl": 600, - "priority": null, - "type": "NAPTR", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:11Z", - "updated_at": "2017-03-09T15:55:11Z" - }, - { - "id": 11189900, - "zone_id": "unit.tests", - "parent_id": null, - "name": "spf", - "content": "v=spf1 ip4:192.168.0.1/16-all", - "ttl": 600, - "priority": null, - "type": "SPF", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:11Z", - "updated_at": "2017-03-09T15:55:11Z" - }, - { - "id": 11189901, - "zone_id": "unit.tests", - "parent_id": null, - "name": "txt", - "content": "v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs", - "ttl": 600, - "priority": null, - "type": "TXT", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 11188802, - "zone_id": "unit.tests", - "parent_id": null, - "name": "txt", - "content": "ALIAS for www.unit.tests.", - "ttl": 600, - "priority": null, - "type": "TXT", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 12188803, - "zone_id": "unit.tests", - "parent_id": null, - "name": "", - "content": "0 issue \"ca.unit.tests\"", - "ttl": 3600, - "priority": null, - "type": "CAA", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - }, - { - "id": 12188805, - "zone_id": "unit.tests", - "parent_id": null, - "name": "included", - "content": "unit.tests", - "ttl": 3600, - "priority": null, - "type": "CNAME", - "regions": [ - "global" - ], - "system_record": false, - "created_at": "2017-03-09T15:55:09Z", - "updated_at": "2017-03-09T15:55:09Z" - } - ], - "pagination": { - "current_page": 2, - "per_page": 20, - "total_entries": 32, - "total_pages": 2 - } -} diff --git a/tests/test_octodns_provider_dnsimple.py b/tests/test_octodns_provider_dnsimple.py index e496d41..611caea 100644 --- a/tests/test_octodns_provider_dnsimple.py +++ b/tests/test_octodns_provider_dnsimple.py @@ -5,231 +5,12 @@ 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.dnsimple import DnsimpleClientNotFound, DnsimpleProvider -from octodns.provider.yaml import YamlProvider -from octodns.zone import Zone +class TestDnsimpleShim(TestCase): -class TestDnsimpleProvider(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): - - # Sandbox - provider = DnsimpleProvider('test', 'token', 42, 'true') - self.assertTrue('sandbox' in provider._client.base) - - provider = DnsimpleProvider('test', 'token', 42) - self.assertFalse('sandbox' in provider._client.base) - - # Bad auth - with requests_mock() as mock: - mock.get(ANY, status_code=401, - text='{"message": "Authentication failed"}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals('Unauthorized', 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='{"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: - base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \ - 'records?page=' - with open('tests/fixtures/dnsimple-page-1.json') as fh: - mock.get(f'{base}1', text=fh.read()) - with open('tests/fixtures/dnsimple-page-2.json') as fh: - mock.get(f'{base}2', 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] - - # test handling of invalid content - with requests_mock() as mock: - with open('tests/fixtures/dnsimple-invalid-content.json') as fh: - mock.get(ANY, text=fh.read()) - - zone = Zone('unit.tests.', []) - provider.populate(zone, lenient=True) - self.assertEquals(set([ - Record.new(zone, '', { - 'ttl': 3600, - 'type': 'SSHFP', - 'values': [] - }, lenient=True), - Record.new(zone, '_srv._tcp', { - 'ttl': 600, - 'type': 'SRV', - 'values': [] - }, lenient=True), - Record.new(zone, 'naptr', { - 'ttl': 600, - 'type': 'NAPTR', - 'values': [] - }, lenient=True), - ]), zone.records) - - def test_apply(self): - provider = DnsimpleProvider('test', 'token', 42) - - resp = Mock() - resp.json = Mock() - provider._client._request = Mock(return_value=resp) - - # non-existent domain, create everything - resp.json.side_effect = [ - DnsimpleClientNotFound, # no zone in populate - DnsimpleClientNotFound, # no domain during apply - ] - plan = provider.plan(self.expected) - - # No root NS, no ignored, no excluded - n = len(self.expected.records) - 8 - self.assertEquals(n, len(plan.changes)) - self.assertEquals(n, provider.apply(plan)) - self.assertFalse(plan.exists) - - provider._client._request.assert_has_calls([ - # created the domain - call('POST', '/domains', data={'name': 'unit.tests'}), - # created at least some of the record with expected data - call('POST', '/zones/unit.tests/records', data={ - 'content': '1.2.3.4', - 'type': 'A', - 'name': '', - 'ttl': 300}), - call('POST', '/zones/unit.tests/records', data={ - 'content': '1.2.3.5', - 'type': 'A', - 'name': '', - 'ttl': 300}), - call('POST', '/zones/unit.tests/records', data={ - 'content': '0 issue "ca.unit.tests"', - 'type': 'CAA', - 'name': '', - 'ttl': 3600}), - call('POST', '/zones/unit.tests/records', data={ - 'content': '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49', - 'type': 'SSHFP', - 'name': '', - 'ttl': 3600}), - call('POST', '/zones/unit.tests/records', data={ - 'content': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73', - 'type': 'SSHFP', - 'name': '', - 'ttl': 3600}), - call('POST', '/zones/unit.tests/records', data={ - 'content': '20 30 foo-1.unit.tests.', - 'priority': 10, - 'type': 'SRV', - 'name': '_srv._tcp', - 'ttl': 600 - }), - ]) - # expected number of total calls - self.assertEquals(28, 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', - 'content': '1.2.3.4', - 'ttl': 300, - 'type': 'A', - }, - { - 'id': 11189898, - 'name': 'www', - 'content': '2.2.3.4', - 'ttl': 300, - 'type': 'A', - }, - { - 'id': 11189899, - 'name': 'ttl', - 'content': '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.assertTrue(plan.exists) - 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', '/zones/unit.tests/records', data={ - 'content': '3.2.3.4', - 'type': 'A', - 'name': 'ttl', - 'ttl': 300 - }), - call('DELETE', '/zones/unit.tests/records/11189899'), - call('DELETE', '/zones/unit.tests/records/11189897'), - call('DELETE', '/zones/unit.tests/records/11189898') - ], any_order=True) + def test_missing(self): + with self.assertRaises(ModuleNotFoundError): + from octodns.provider.dnsimple import DnsimpleProvider + DnsimpleProvider