From 355995daddb37e1286d88f5a92bb5be4736fa120 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 5 Jan 2022 08:17:09 -0800 Subject: [PATCH] Extract and shim CloudflareProvider --- CHANGELOG.md | 1 + README.md | 2 +- octodns/provider/cloudflare.py | 861 +-------- .../cloudflare-dns_records-page-1.json | 188 -- .../cloudflare-dns_records-page-2.json | 238 --- .../cloudflare-dns_records-page-3.json | 128 -- tests/fixtures/cloudflare-pagerules.json | 103 -- tests/fixtures/cloudflare-zones-page-1.json | 140 -- tests/fixtures/cloudflare-zones-page-2.json | 140 -- tests/test_octodns_provider_cloudflare.py | 1629 +---------------- 10 files changed, 20 insertions(+), 3410 deletions(-) delete mode 100644 tests/fixtures/cloudflare-dns_records-page-1.json delete mode 100644 tests/fixtures/cloudflare-dns_records-page-2.json delete mode 100644 tests/fixtures/cloudflare-dns_records-page-3.json delete mode 100644 tests/fixtures/cloudflare-pagerules.json delete mode 100644 tests/fixtures/cloudflare-zones-page-1.json delete mode 100644 tests/fixtures/cloudflare-zones-page-2.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 0984e2c..c39a899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ https://github.com/octodns/octodns/issues/622 & https://github.com/octodns/octodns/pull/822 for more information. Providers that have been extracted in this release include: + * [CloudflareProvider](https://github.com/octodns/octodns-cloudflare/) * [DnsimpleProvider](https://github.com/octodns/octodns-dnsimple/) * [Ns1Provider](https://github.com/octodns/octodns-ns1/) * [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/) diff --git a/README.md b/README.md index b70442b..46e3597 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ The table below lists the providers octoDNS supports. We're currently in the pro |--|--|--|--|--|--| | [AzureProvider](/octodns/provider/azuredns.py) | | azure-identity, azure-mgmt-dns, azure-mgmt-trafficmanager | A, AAAA, CAA, CNAME, MX, NS, PTR, SRV, TXT | Alpha (A, AAAA, CNAME) | | | [Akamai](/octodns/provider/edgedns.py) | | edgegrid-python | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT | No | | -| [CloudflareProvider](/octodns/provider/cloudflare.py) | | | A, AAAA, ALIAS, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT, URLFWD | No | CAA tags restricted | +| [CloudflareProvider](https://github.com/octodns/octodns-cloudflare/) | [octodns_cloudflare](https://github.com/octodns/octodns-cloudflare/) | | | | | | [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 | diff --git a/octodns/provider/cloudflare.py b/octodns/provider/cloudflare.py index 517a364..b30e0e8 100644 --- a/octodns/provider/cloudflare.py +++ b/octodns/provider/cloudflare.py @@ -5,853 +5,18 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from collections import defaultdict -from copy import deepcopy from logging import getLogger -from requests import Session -from time import sleep -from urllib.parse import urlsplit -from ..record import Record, Update -from . import ProviderException -from .base import BaseProvider - - -class CloudflareError(ProviderException): - def __init__(self, data): - try: - message = data['errors'][0]['message'] - except (IndexError, KeyError, TypeError): - message = 'Cloudflare error' - super(CloudflareError, self).__init__(message) - - -class CloudflareAuthenticationError(CloudflareError): - def __init__(self, data): - CloudflareError.__init__(self, data) - - -class CloudflareRateLimitError(CloudflareError): - def __init__(self, data): - CloudflareError.__init__(self, data) - - -_PROXIABLE_RECORD_TYPES = {'A', 'AAAA', 'ALIAS', 'CNAME'} - - -class CloudflareProvider(BaseProvider): - ''' - Cloudflare DNS provider - - cloudflare: - class: octodns.provider.cloudflare.CloudflareProvider - # The api key (required) - # Your Cloudflare account email address (required) - email: dns-manager@example.com (optional if using token) - token: foo - # Import CDN enabled records as CNAME to {}.cdn.cloudflare.net. Records - # ending at .cdn.cloudflare.net. will be ignored when this provider is - # not used as the source and the cdn option is enabled. - # - # See: https://support.cloudflare.com/hc/en-us/articles/115000830351 - cdn: false - # Optional. Default: 4. Number of times to retry if a 429 response - # is received. - retry_count: 4 - # Optional. Default: 300. Number of seconds to wait before retrying. - retry_period: 300 - # Optional. Default: 50. Number of zones per page. - zones_per_page: 50 - # Optional. Default: 100. Number of dns records per page. - records_per_page: 100 - - Note: The "proxied" flag of "A", "AAAA" and "CNAME" records can be managed - via the YAML provider like so: - name: - octodns: - cloudflare: - proxied: true - ttl: 120 - type: A - value: 1.2.3.4 - ''' - SUPPORTS_GEO = False - SUPPORTS_DYNAMIC = False - SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'LOC', 'MX', 'NS', - 'PTR', 'SRV', 'SPF', 'TXT', 'URLFWD')) - - MIN_TTL = 120 - TIMEOUT = 15 - - def __init__(self, id, email=None, token=None, cdn=False, retry_count=4, - retry_period=300, zones_per_page=50, records_per_page=100, - *args, **kwargs): - self.log = getLogger(f'CloudflareProvider[{id}]') - self.log.debug('__init__: id=%s, email=%s, token=***, cdn=%s', id, - email, cdn) - super(CloudflareProvider, self).__init__(id, *args, **kwargs) - - sess = Session() - if email and token: - sess.headers.update({ - 'X-Auth-Email': email, - 'X-Auth-Key': token, - }) - else: - # https://api.cloudflare.com/#getting-started-requests - # https://tools.ietf.org/html/rfc6750#section-2.1 - sess.headers.update({ - 'Authorization': f'Bearer {token}', - }) - self.cdn = cdn - self.retry_count = retry_count - self.retry_period = retry_period - self.zones_per_page = zones_per_page - self.records_per_page = records_per_page - self._sess = sess - - self._zones = None - self._zone_records = {} - - def _try_request(self, *args, **kwargs): - tries = self.retry_count - while True: # We'll raise to break after our tries expire - try: - return self._request(*args, **kwargs) - except CloudflareRateLimitError: - if tries <= 1: - raise - tries -= 1 - self.log.warn('rate limit encountered, pausing ' - 'for %ds and trying again, %d remaining', - self.retry_period, tries) - sleep(self.retry_period) - - def _request(self, method, path, params=None, data=None): - self.log.debug('_request: method=%s, path=%s', method, path) - - url = f'https://api.cloudflare.com/client/v4{path}' - resp = self._sess.request(method, url, params=params, json=data, - timeout=self.TIMEOUT) - self.log.debug('_request: status=%d', resp.status_code) - if resp.status_code == 400: - self.log.debug('_request: data=%s', data) - raise CloudflareError(resp.json()) - if resp.status_code == 403: - raise CloudflareAuthenticationError(resp.json()) - if resp.status_code == 429: - raise CloudflareRateLimitError(resp.json()) - - resp.raise_for_status() - return resp.json() - - def _change_keyer(self, change): - key = change.__class__.__name__ - order = {'Delete': 0, 'Create': 1, 'Update': 2} - return order[key] - - @property - def zones(self): - if self._zones is None: - page = 1 - zones = [] - while page: - resp = self._try_request('GET', '/zones', - params={ - 'page': page, - 'per_page': self.zones_per_page - }) - zones += resp['result'] - info = resp['result_info'] - if info['count'] > 0 and info['count'] == info['per_page']: - page += 1 - else: - page = None - - self._zones = {f'{z["name"]}.': z['id'] for z in zones} - - return self._zones - - def _ttl_data(self, ttl): - return 300 if ttl == 1 else ttl - - def _data_for_cdn(self, name, _type, records): - self.log.info('CDN rewrite for %s', records[0]['name']) - _type = "CNAME" - if name == "": - _type = "ALIAS" - - return { - 'ttl': self._ttl_data(records[0]['ttl']), - 'type': _type, - 'value': f'{records[0]["name"]}.cdn.cloudflare.net.', - } - - def _data_for_multiple(self, _type, records): - return { - 'ttl': self._ttl_data(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': self._ttl_data(records[0]['ttl']), - 'type': _type, - 'values': [r['content'].replace(';', '\\;') for r in records], - } - - def _data_for_CAA(self, _type, records): - values = [] - for r in records: - data = r['data'] - values.append(data) - return { - 'ttl': self._ttl_data(records[0]['ttl']), - 'type': _type, - 'values': values, - } - - def _data_for_CNAME(self, _type, records): - only = records[0] - return { - 'ttl': self._ttl_data(only['ttl']), - 'type': _type, - 'value': f'{only["content"]}.' - } - - _data_for_ALIAS = _data_for_CNAME - _data_for_PTR = _data_for_CNAME - - def _data_for_LOC(self, _type, records): - values = [] - for record in records: - r = record['data'] - values.append({ - 'lat_degrees': int(r['lat_degrees']), - 'lat_minutes': int(r['lat_minutes']), - 'lat_seconds': float(r['lat_seconds']), - 'lat_direction': r['lat_direction'], - 'long_degrees': int(r['long_degrees']), - 'long_minutes': int(r['long_minutes']), - 'long_seconds': float(r['long_seconds']), - 'long_direction': r['long_direction'], - 'altitude': float(r['altitude']), - 'size': float(r['size']), - 'precision_horz': float(r['precision_horz']), - 'precision_vert': float(r['precision_vert']), - }) - return { - 'ttl': self._ttl_data(records[0]['ttl']), - 'type': _type, - 'values': values - } - - def _data_for_MX(self, _type, records): - values = [] - for r in records: - values.append({ - 'preference': r['priority'], - 'exchange': f'{r["content"]}.', - }) - return { - 'ttl': self._ttl_data(records[0]['ttl']), - 'type': _type, - 'values': values, - } - - def _data_for_NS(self, _type, records): - return { - 'ttl': self._ttl_data(records[0]['ttl']), - 'type': _type, - 'values': [f'{r["content"]}.' for r in records], - } - - def _data_for_SRV(self, _type, records): - values = [] - for r in records: - target = (f'{r["data"]["target"]}.' - if r['data']['target'] != "." else ".") - values.append({ - 'priority': r['data']['priority'], - 'weight': r['data']['weight'], - 'port': r['data']['port'], - 'target': target, - }) - return { - 'type': _type, - 'ttl': self._ttl_data(records[0]['ttl']), - 'values': values - } - - def _data_for_URLFWD(self, _type, records): - values = [] - for r in records: - values.append({ - 'path': r['path'], - 'target': r['url'], - 'code': r['status_code'], - 'masking': 2, - 'query': 0, - }) - return { - 'type': _type, - 'ttl': 300, # ttl does not exist for this type, forcing a setting - 'values': values - } - - def zone_records(self, zone): - if zone.name not in self._zone_records: - zone_id = self.zones.get(zone.name, False) - if not zone_id: - return [] - - records = [] - path = f'/zones/{zone_id}/dns_records' - page = 1 - while page: - resp = self._try_request('GET', path, params={'page': page, - 'per_page': self.records_per_page}) - records += resp['result'] - info = resp['result_info'] - if info['count'] > 0 and info['count'] == info['per_page']: - page += 1 - else: - page = None - - path = f'/zones/{zone_id}/pagerules' - resp = self._try_request('GET', path, params={'status': 'active'}) - for r in resp['result']: - # assumption, base on API guide, will only contain 1 action - if r['actions'][0]['id'] == 'forwarding_url': - records += [r] - - self._zone_records[zone.name] = records - - return self._zone_records[zone.name] - - def _record_for(self, zone, name, _type, records, lenient): - # rewrite Cloudflare proxied records - if self.cdn and records[0]['proxied']: - data = self._data_for_cdn(name, _type, records) - else: - # Cloudflare supports ALIAS semantics with root CNAMEs - if _type == 'CNAME' and name == '': - _type = 'ALIAS' - - data_for = getattr(self, f'_data_for_{_type}') - data = data_for(_type, records) - - record = Record.new(zone, name, data, source=self, lenient=lenient) - - if _type in _PROXIABLE_RECORD_TYPES: - record._octodns['cloudflare'] = { - 'proxied': records[0].get('proxied', False) - } - - return record - - def populate(self, zone, target=False, lenient=False): - self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, - target, lenient) - - exists = False - before = len(zone.records) - records = self.zone_records(zone) - if records: - exists = True - values = defaultdict(lambda: defaultdict(list)) - for record in records: - if 'targets' in record: - # assumption, targets will always contain 1 target - # API documentation only indicates 'url' as the only target - # if record['targets'][0]['target'] == 'url': - uri = record['targets'][0]['constraint']['value'] - uri = '//' + uri if not uri.startswith('http') else uri - parsed_uri = urlsplit(uri) - name = zone.hostname_from_fqdn(parsed_uri.netloc) - path = parsed_uri.path - _type = 'URLFWD' - # assumption, actions will always contain 1 action - _values = record['actions'][0]['value'] - _values['path'] = path - # no ttl set by pagerule, creating one - _values['ttl'] = 300 - values[name][_type].append(_values) - # the dns_records branch - # elif 'name' in record: - else: - 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(): - record = self._record_for(zone, name, _type, records, - lenient) - - # only one rewrite is needed for names where the proxy is - # enabled at multiple records with a different type but - # the same name - if (self.cdn and records[0]['proxied'] and - record in zone._records[name]): - self.log.info('CDN rewrite %s already in zone', name) - continue - - zone.add_record(record, lenient=lenient) - - self.log.info('populate: found %s records, exists=%s', - len(zone.records) - before, exists) - return exists - - def _include_change(self, change): - if isinstance(change, Update): - new = change.new.data - - # Cloudflare manages TTL of proxied records, so we should exclude - # TTL from the comparison (to prevent false-positives). - if self._record_is_proxied(change.existing): - existing = deepcopy(change.existing.data) - existing.update({ - 'ttl': new['ttl'] - }) - elif change.new._type == 'URLFWD': - existing = deepcopy(change.existing.data) - existing.update({ - 'ttl': new['ttl'] - }) - else: - existing = change.existing.data - - new['ttl'] = max(self.MIN_TTL, new['ttl']) - if new == existing: - return False - - # If this is a record to enable Cloudflare CDN don't update as - # we don't know the original values. - if (change.record._type in ('ALIAS', 'CNAME') and - change.record.value.endswith('.cdn.cloudflare.net.')): - return False - - return True - - def _contents_for_multiple(self, record): - for value in record.values: - yield {'content': value} - - _contents_for_A = _contents_for_multiple - _contents_for_AAAA = _contents_for_multiple - _contents_for_NS = _contents_for_multiple - _contents_for_SPF = _contents_for_multiple - - def _contents_for_CAA(self, record): - for value in record.values: - yield { - 'data': { - 'flags': value.flags, - 'tag': value.tag, - 'value': value.value, - } - } - - def _contents_for_TXT(self, record): - for value in record.values: - yield {'content': value.replace('\\;', ';')} - - def _contents_for_CNAME(self, record): - yield {'content': record.value} - - _contents_for_PTR = _contents_for_CNAME - - def _contents_for_LOC(self, record): - for value in record.values: - yield { - 'data': { - 'lat_degrees': value.lat_degrees, - 'lat_minutes': value.lat_minutes, - 'lat_seconds': value.lat_seconds, - 'lat_direction': value.lat_direction, - 'long_degrees': value.long_degrees, - 'long_minutes': value.long_minutes, - 'long_seconds': value.long_seconds, - 'long_direction': value.long_direction, - 'altitude': value.altitude, - 'size': value.size, - 'precision_horz': value.precision_horz, - 'precision_vert': value.precision_vert, - } - } - - def _contents_for_MX(self, record): - for value in record.values: - yield { - 'priority': value.preference, - 'content': value.exchange - } - - def _contents_for_SRV(self, record): - try: - service, proto, subdomain = record.name.split('.', 2) - # We have a SRV in a sub-zone - except ValueError: - # We have a SRV in the zone - service, proto = record.name.split('.', 1) - subdomain = None - - name = record.zone.name - if subdomain: - name = subdomain - - for value in record.values: - target = value.target[:-1] if value.target != "." else "." - - yield { - 'data': { - 'service': service, - 'proto': proto, - 'name': name, - 'priority': value.priority, - 'weight': value.weight, - 'port': value.port, - 'target': target, - } - } - - def _contents_for_URLFWD(self, record): - name = record.fqdn[:-1] - for value in record.values: - yield { - 'targets': [ - { - 'target': 'url', - 'constraint': { - 'operator': 'matches', - 'value': name + value.path - } - } - ], - 'actions': [ - { - 'id': 'forwarding_url', - 'value': { - 'url': value.target, - 'status_code': value.code, - } - } - ], - 'status': 'active', - } - - def _record_is_proxied(self, record): - return ( - not self.cdn and - record._octodns.get('cloudflare', {}).get('proxied', False) - ) - - def _gen_data(self, record): - name = record.fqdn[:-1] - _type = record._type - ttl = max(self.MIN_TTL, record.ttl) - - # Cloudflare supports ALIAS semantics with a root CNAME - if _type == 'ALIAS': - _type = 'CNAME' - - if _type == 'URLFWD': - contents_for = getattr(self, f'_contents_for_{_type}') - for content in contents_for(record): - yield content - else: - contents_for = getattr(self, f'_contents_for_{_type}') - for content in contents_for(record): - content.update({ - 'name': name, - 'type': _type, - 'ttl': ttl, - }) - - if _type in _PROXIABLE_RECORD_TYPES: - content.update({ - 'proxied': self._record_is_proxied(record) - }) - - yield content - - def _gen_key(self, data): - # Note that most CF record data has a `content` field the value of - # which is a unique/hashable string for the record's. It includes all - # the "value" bits, but not the secondary stuff like TTL's. E.g. for - # an A it'll include the value, for a CAA it'll include the flags, tag, - # and value, ... We'll take advantage of this to try and match up old & - # new records cleanly. In general when there are multiple records for a - # name & type each will have a distinct/consistent `content` that can - # serve as a unique identifier. - # BUT... there are exceptions. MX, CAA, LOC and SRV don't have a simple - # content as things are currently implemented so we need to handle - # those explicitly and create unique/hashable strings for them. - # AND... for URLFWD/Redirects additional adventures are created. - _type = data.get('type', 'URLFWD') - if _type == 'MX': - priority = data['priority'] - content = data['content'] - return f'{priority} {content}' - elif _type == 'CAA': - data = data['data'] - flags = data['flags'] - tag = data['tag'] - value = data['value'] - return f'{flags} {tag} {value}' - elif _type == 'SRV': - data = data['data'] - port = data['port'] - priority = data['priority'] - target = data['target'] - weight = data['weight'] - return f'{port} {priority} {target} {weight}' - elif _type == 'LOC': - data = data['data'] - lat_degrees = data['lat_degrees'] - lat_minutes = data['lat_minutes'] - lat_seconds = data['lat_seconds'] - lat_direction = data['lat_direction'] - long_degrees = data['long_degrees'] - long_minutes = data['long_minutes'] - long_seconds = data['long_seconds'] - long_direction = data['long_direction'] - altitude = data['altitude'] - size = data['size'] - precision_horz = data['precision_horz'] - precision_vert = data['precision_vert'] - return f'{lat_degrees} {lat_minutes} {lat_seconds} ' \ - f'{lat_direction} {long_degrees} {long_minutes} ' \ - f'{long_seconds} {long_direction} {altitude} {size} ' \ - f'{precision_horz} {precision_vert}' - elif _type == 'URLFWD': - uri = data['targets'][0]['constraint']['value'] - uri = '//' + uri if not uri.startswith('http') else uri - parsed_uri = urlsplit(uri) - url = data['actions'][0]['value']['url'] - status_code = data['actions'][0]['value']['status_code'] - return f'{parsed_uri.netloc} {parsed_uri.path} {url} ' + \ - f'{status_code}' - - return data['content'] - - def _apply_Create(self, change): - new = change.new - zone_id = self.zones[new.zone.name] - if new._type == 'URLFWD': - path = f'/zones/{zone_id}/pagerules' - else: - path = f'/zones/{zone_id}/dns_records' - for content in self._gen_data(new): - self._try_request('POST', path, data=content) - - def _apply_Update(self, change): - zone = change.new.zone - zone_id = self.zones[zone.name] - hostname = zone.hostname_from_fqdn(change.new.fqdn[:-1]) - _type = change.new._type - - existing = {} - # Find all of the existing CF records for this name & type - for record in self.zone_records(zone): - if 'targets' in record: - uri = record['targets'][0]['constraint']['value'] - uri = '//' + uri if not uri.startswith('http') else uri - parsed_uri = urlsplit(uri) - name = zone.hostname_from_fqdn(parsed_uri.netloc) - path = parsed_uri.path - # assumption, actions will always contain 1 action - _values = record['actions'][0]['value'] - _values['path'] = path - _values['ttl'] = 300 - _values['type'] = 'URLFWD' - record.update(_values) - else: - name = zone.hostname_from_fqdn(record['name']) - # Use the _record_for so that we include all of standard - # conversion logic - r = self._record_for(zone, name, record['type'], [record], True) - if hostname == r.name and _type == r._type: - # Round trip the single value through a record to contents - # flow to get a consistent _gen_data result that matches - # what went in to new_contents - data = next(self._gen_data(r)) - - # Record the record_id and data for this existing record - key = self._gen_key(data) - existing[key] = { - 'record_id': record['id'], - 'data': data, - } - - # Build up a list of new CF records for this Update - new = { - self._gen_key(d): d for d in self._gen_data(change.new) - } - - # OK we now have a picture of the old & new CF records, our next step - # is to figure out which records need to be deleted - deletes = {} - for key, info in existing.items(): - if key not in new: - deletes[key] = info - # Now we need to figure out which records will need to be created - creates = {} - # And which will be updated - updates = {} - for key, data in new.items(): - if key in existing: - # To update we need to combine the new data and existing's - # record_id. old_data is just for debugging/logging purposes - old_info = existing[key] - updates[key] = { - 'record_id': old_info['record_id'], - 'data': data, - 'old_data': old_info['data'], - } - else: - creates[key] = data - - # To do this as safely as possible we'll add new things first, update - # existing things, and then remove old things. This should (try) and - # ensure that we have as many value CF records in their system as - # possible at any given time. Ideally we'd have a "batch" API that - # would allow create, delete, and upsert style stuff so operations - # could be done atomically, but that's not available so we made the - # best of it... - - # However, there are record types like CNAME that can only have a - # single value. B/c of that our create and then delete approach isn't - # actually viable. To address this we'll convert as many creates & - # deletes as we can to updates. This will have a minor upside of - # resulting in fewer ops and in the case of things like CNAME where - # there's a single create and delete result in a single update instead. - create_keys = sorted(creates.keys()) - delete_keys = sorted(deletes.keys()) - for i in range(0, min(len(create_keys), len(delete_keys))): - create_key = create_keys[i] - create_data = creates.pop(create_key) - delete_info = deletes.pop(delete_keys[i]) - updates[create_key] = { - 'record_id': delete_info['record_id'], - 'data': create_data, - 'old_data': delete_info['data'], - } - - # The sorts ensure a consistent order of operations, they're not - # otherwise required, just makes things deterministic - - # Creates - if _type == 'URLFWD': - path = f'/zones/{zone_id}/pagerules' - else: - path = f'/zones/{zone_id}/dns_records' - for _, data in sorted(creates.items()): - self.log.debug('_apply_Update: creating %s', data) - self._try_request('POST', path, data=data) - - # Updates - for _, info in sorted(updates.items()): - record_id = info['record_id'] - data = info['data'] - old_data = info['old_data'] - if _type == 'URLFWD': - path = f'/zones/{zone_id}/pagerules/{record_id}' - else: - path = f'/zones/{zone_id}/dns_records/{record_id}' - self.log.debug('_apply_Update: updating %s, %s -> %s', - record_id, data, old_data) - self._try_request('PUT', path, data=data) - - # Deletes - for _, info in sorted(deletes.items()): - record_id = info['record_id'] - old_data = info['data'] - if _type == 'URLFWD': - path = f'/zones/{zone_id}/pagerules/{record_id}' - else: - path = f'/zones/{zone_id}/dns_records/{record_id}' - self.log.debug('_apply_Update: removing %s, %s', record_id, - old_data) - self._try_request('DELETE', path) - - def _apply_Delete(self, change): - existing = change.existing - existing_name = existing.fqdn[:-1] - # Make sure to map ALIAS to CNAME when looking for the target to delete - existing_type = 'CNAME' if existing._type == 'ALIAS' \ - else existing._type - for record in self.zone_records(existing.zone): - if 'targets' in record: - uri = record['targets'][0]['constraint']['value'] - uri = '//' + uri if not uri.startswith('http') else uri - parsed_uri = urlsplit(uri) - record_name = parsed_uri.netloc - record_type = 'URLFWD' - zone_id = self.zones.get(existing.zone.name, False) - if existing_name == record_name and \ - existing_type == record_type: - path = f'/zones/{zone_id}/pagerules/{record["id"]}' - self._try_request('DELETE', path) - else: - if existing_name == record['name'] and \ - existing_type == record['type']: - path = f'/zones/{record["zone_id"]}/dns_records/' \ - f'{record["id"]}' - self._try_request('DELETE', path) - - def _apply(self, plan): - desired = plan.desired - changes = plan.changes - self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, - len(changes)) - - name = desired.name - if name not in self.zones: - self.log.debug('_apply: no matching zone, creating') - data = { - 'name': name[:-1], - 'jump_start': False, - } - resp = self._try_request('POST', '/zones', data=data) - zone_id = resp['result']['id'] - self.zones[name] = zone_id - self._zone_records[name] = {} - - # Force the operation order to be Delete() -> Create() -> Update() - # This will help avoid problems in updating a CNAME record into an - # A record and vice-versa - changes.sort(key=self._change_keyer) - - for change in changes: - class_name = change.__class__.__name__ - getattr(self, f'_apply_{class_name}')(change) - - # clear the cache - self._zone_records.pop(name, None) - - def _extra_changes(self, existing, desired, changes): - extra_changes = [] - - existing_records = {r: r for r in existing.records} - changed_records = {c.record for c in changes} - - for desired_record in desired.records: - existing_record = existing_records.get(desired_record, None) - if not existing_record: # Will be created - continue - elif desired_record in changed_records: # Already being updated - continue - - if (self._record_is_proxied(existing_record) != - self._record_is_proxied(desired_record)): - extra_changes.append(Update(existing_record, desired_record)) - - return extra_changes +logger = getLogger('Cloudflare') +try: + logger.warn('octodns_cloudflare shimmed. Update your provider class to ' + 'octodns_cloudflare.CloudflareProvider. ' + 'Shim will be removed in 1.0') + from octodns_cloudflare import CloudflareProvider + CloudflareProvider # pragma: no cover +except ModuleNotFoundError: + logger.exception('CloudflareProvider has been moved into a seperate ' + 'module, octodns_cloudflare is now required. Provider ' + 'class should be updated to ' + 'octodns_cloudflare.CloudflareProvider') + raise diff --git a/tests/fixtures/cloudflare-dns_records-page-1.json b/tests/fixtures/cloudflare-dns_records-page-1.json deleted file mode 100644 index efe0654..0000000 --- a/tests/fixtures/cloudflare-dns_records-page-1.json +++ /dev/null @@ -1,188 +0,0 @@ -{ - "result": [ - { - "id": "fc12ab34cd5611334422ab3322997650", - "type": "A", - "name": "unit.tests", - "content": "1.2.3.4", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.054409Z", - "created_on": "2017-03-11T18:01:43.054409Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997651", - "type": "A", - "name": "unit.tests", - "content": "1.2.3.5", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.160148Z", - "created_on": "2017-03-11T18:01:43.160148Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997653", - "type": "A", - "name": "www.unit.tests", - "content": "2.2.3.6", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997654", - "type": "A", - "name": "www.sub.unit.tests", - "content": "2.2.3.6", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:44.030044Z", - "created_on": "2017-03-11T18:01:44.030044Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997655", - "type": "AAAA", - "name": "aaaa.unit.tests", - "content": "2601:644:500:e210:62f8:1dff:feb8:947a", - "proxiable": true, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.843594Z", - "created_on": "2017-03-11T18:01:43.843594Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997656", - "type": "CNAME", - "name": "cname.unit.tests", - "content": "unit.tests", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.940682Z", - "created_on": "2017-03-11T18:01:43.940682Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997657", - "type": "MX", - "name": "mx.unit.tests", - "content": "smtp-1.unit.tests", - "proxiable": false, - "proxied": false, - "ttl": 300, - "priority": 40, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.764273Z", - "created_on": "2017-03-11T18:01:43.764273Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997658", - "type": "MX", - "name": "mx.unit.tests", - "content": "smtp-2.unit.tests", - "proxiable": false, - "proxied": false, - "ttl": 300, - "priority": 20, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.586007Z", - "created_on": "2017-03-11T18:01:43.586007Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997659", - "type": "MX", - "name": "mx.unit.tests", - "content": "smtp-3.unit.tests", - "proxiable": false, - "proxied": false, - "ttl": 300, - "priority": 30, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.670592Z", - "created_on": "2017-03-11T18:01:43.670592Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997660", - "type": "MX", - "name": "mx.unit.tests", - "content": "smtp-4.unit.tests", - "proxiable": false, - "proxied": false, - "ttl": 300, - "priority": 10, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.505671Z", - "created_on": "2017-03-11T18:01:43.505671Z", - "meta": { - "auto_added": false - } - } - ], - "result_info": { - "page": 1, - "per_page": 10, - "total_pages": 2, - "count": 10, - "total_count": 20 - }, - "success": true, - "errors": [], - "messages": [] -} diff --git a/tests/fixtures/cloudflare-dns_records-page-2.json b/tests/fixtures/cloudflare-dns_records-page-2.json deleted file mode 100644 index 366fe9c..0000000 --- a/tests/fixtures/cloudflare-dns_records-page-2.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "result": [ - { - "id": "fc12ab34cd5611334422ab3322997661", - "type": "NS", - "name": "under.unit.tests", - "content": "ns1.unit.tests", - "proxiable": false, - "proxied": false, - "ttl": 3600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:42.599878Z", - "created_on": "2017-03-11T18:01:42.599878Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997662", - "type": "NS", - "name": "under.unit.tests", - "content": "ns2.unit.tests", - "proxiable": false, - "proxied": false, - "ttl": 3600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:42.727011Z", - "created_on": "2017-03-11T18:01:42.727011Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997663", - "type": "SPF", - "name": "spf.unit.tests", - "content": "v=spf1 ip4:192.168.0.1/16-all", - "proxiable": false, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:44.112568Z", - "created_on": "2017-03-11T18:01:44.112568Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997664", - "type": "TXT", - "name": "txt.unit.tests", - "content": "Bah bah black sheep", - "proxiable": false, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:42.837282Z", - "created_on": "2017-03-11T18:01:42.837282Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997665", - "type": "TXT", - "name": "txt.unit.tests", - "content": "have you any wool.", - "proxiable": false, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:42.961566Z", - "created_on": "2017-03-11T18:01:42.961566Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997666", - "type": "SOA", - "name": "unit.tests", - "content": "ignored", - "proxiable": false, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:42.961566Z", - "created_on": "2017-03-11T18:01:42.961566Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997667", - "type": "TXT", - "name": "txt.unit.tests", - "content": "v=DKIM1;k=rsa;s=email;h=sha256;p=A\/kinda+of\/long\/string+with+numb3rs", - "proxiable": false, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:42.961566Z", - "created_on": "2017-03-11T18:01:42.961566Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc223b34cd5611334422ab3322997667", - "type": "CAA", - "name": "unit.tests", - "data": { - "flags": 0, - "tag": "issue", - "value": "ca.unit.tests" - }, - "proxiable": false, - "proxied": false, - "ttl": 3600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:42.961566Z", - "created_on": "2017-03-11T18:01:42.961566Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997656", - "type": "CNAME", - "name": "included.unit.tests", - "content": "unit.tests", - "proxiable": true, - "proxied": false, - "ttl": 3600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.940682Z", - "created_on": "2017-03-11T18:01:43.940682Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997677", - "type": "PTR", - "name": "ptr.unit.tests", - "content": "foo.bar.com", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.940682Z", - "created_on": "2017-03-11T18:01:43.940682Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997656", - "type": "SRV", - "name": "_imap._tcp.unit.tests", - "data": { - "service": "_imap", - "proto": "_tcp", - "name": "unit.tests", - "priority": 0, - "weight": 0, - "port": 0, - "target": "." - }, - "proxiable": true, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.940682Z", - "created_on": "2017-03-11T18:01:43.940682Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997656", - "type": "SRV", - "name": "_pop3._tcp.unit.tests", - "data": { - "service": "_imap", - "proto": "_pop3", - "name": "unit.tests", - "priority": 0, - "weight": 0, - "port": 0, - "target": "." - }, - "proxiable": true, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.940682Z", - "created_on": "2017-03-11T18:01:43.940682Z", - "meta": { - "auto_added": false - } - } - ], - "result_info": { - "page": 2, - "per_page": 10, - "total_pages": 3, - "count": 10, - "total_count": 24 - }, - "success": true, - "errors": [], - "messages": [] -} diff --git a/tests/fixtures/cloudflare-dns_records-page-3.json b/tests/fixtures/cloudflare-dns_records-page-3.json deleted file mode 100644 index 0f06ab4..0000000 --- a/tests/fixtures/cloudflare-dns_records-page-3.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "result": [ - { - "id": "fc12ab34cd5611334422ab3322997656", - "type": "SRV", - "name": "_srv._tcp.unit.tests", - "data": { - "service": "_srv", - "proto": "_tcp", - "name": "unit.tests", - "priority": 12, - "weight": 20, - "port": 30, - "target": "foo-2.unit.tests" - }, - "proxiable": true, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.940682Z", - "created_on": "2017-03-11T18:01:43.940682Z", - "meta": { - "auto_added": false - } - }, - { - "id": "fc12ab34cd5611334422ab3322997656", - "type": "SRV", - "name": "_srv._tcp.unit.tests", - "data": { - "service": "_srv", - "proto": "_tcp", - "name": "unit.tests", - "priority": 10, - "weight": 20, - "port": 30, - "target": "foo-1.unit.tests" - }, - "proxiable": true, - "proxied": false, - "ttl": 600, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.940682Z", - "created_on": "2017-03-11T18:01:43.940682Z", - "meta": { - "auto_added": false - } - }, - { - "id": "372e67954025e0ba6aaa6d586b9e0b59", - "type": "LOC", - "name": "loc.unit.tests", - "content": "IN LOC 31 58 52.1 S 115 49 11.7 E 20m 10m 10m 2m", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "created_on": "2020-01-28T05:20:00.12345Z", - "modified_on": "2020-01-28T05:20:00.12345Z", - "data": { - "lat_degrees": 31, - "lat_minutes": 58, - "lat_seconds": 52.1, - "lat_direction": "S", - "long_degrees": 115, - "long_minutes": 49, - "long_seconds": 11.7, - "long_direction": "E", - "altitude": 20, - "size": 10, - "precision_horz": 10, - "precision_vert": 2 - }, - "meta": { - "auto_added": true, - "source": "primary" - } - }, - { - "id": "372e67954025e0ba6aaa6d586b9e0b59", - "type": "LOC", - "name": "loc.unit.tests", - "content": "IN LOC 53 14 10 N 2 18 26 W 20m 10m 1000m 2m", - "proxiable": true, - "proxied": false, - "ttl": 300, - "locked": false, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "created_on": "2020-01-28T05:20:00.12345Z", - "modified_on": "2020-01-28T05:20:00.12345Z", - "data": { - "lat_degrees": 53, - "lat_minutes": 13, - "lat_seconds": 10, - "lat_direction": "N", - "long_degrees": 2, - "long_minutes": 18, - "long_seconds": 26, - "long_direction": "W", - "altitude": 20, - "size": 10, - "precision_horz": 1000, - "precision_vert": 2 - }, - "meta": { - "auto_added": true, - "source": "primary" - } - } - ], - "result_info": { - "page": 3, - "per_page": 10, - "total_pages": 3, - "count": 4, - "total_count": 24 - }, - "success": true, - "errors": [], - "messages": [] -} diff --git a/tests/fixtures/cloudflare-pagerules.json b/tests/fixtures/cloudflare-pagerules.json deleted file mode 100644 index 7efa018..0000000 --- a/tests/fixtures/cloudflare-pagerules.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "result": [ - { - "id": "2b1ec1793185213139f22059a165376e", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwd0.unit.tests/" - } - } - ], - "actions": [ - { - "id": "always_use_https" - } - ], - "priority": 4, - "status": "active", - "created_on": "2021-06-29T17:14:28.000000Z", - "modified_on": "2021-06-29T17:15:33.000000Z" - }, - { - "id": "2b1ec1793185213139f22059a165376f", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwd0.unit.tests/*" - } - } - ], - "actions": [ - { - "id": "forwarding_url", - "value": { - "url": "https://www.unit.tests/", - "status_code": 301 - } - } - ], - "priority": 3, - "status": "active", - "created_on": "2021-06-29T17:07:12.000000Z", - "modified_on": "2021-06-29T17:15:12.000000Z" - }, - { - "id": "2b1ec1793185213139f22059a165377e", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwd1.unit.tests/*" - } - } - ], - "actions": [ - { - "id": "forwarding_url", - "value": { - "url": "https://www.unit.tests/", - "status_code": 302 - } - } - ], - "priority": 2, - "status": "active", - "created_on": "2021-06-28T22:42:27.000000Z", - "modified_on": "2021-06-28T22:43:13.000000Z" - }, - { - "id": "2a9140b17ffb0e6aed826049eec970b8", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwd2.unit.tests/*" - } - } - ], - "actions": [ - { - "id": "forwarding_url", - "value": { - "url": "https://www.unit.tests/", - "status_code": 301 - } - } - ], - "priority": 1, - "status": "active", - "created_on": "2021-06-25T20:10:50.000000Z", - "modified_on": "2021-06-28T22:38:10.000000Z" - } - ], - "success": true, - "errors": [], - "messages": [] -} diff --git a/tests/fixtures/cloudflare-zones-page-1.json b/tests/fixtures/cloudflare-zones-page-1.json deleted file mode 100644 index 86f3b21..0000000 --- a/tests/fixtures/cloudflare-zones-page-1.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "result": [ - { - "id": "234234243423aaabb334342aaa343433", - "name": "github.com", - "status": "pending", - "paused": false, - "type": "full", - "development_mode": 0, - "name_servers": [ - "alice.ns.cloudflare.com", - "tom.ns.cloudflare.com" - ], - "original_name_servers": [], - "original_registrar": null, - "original_dnshost": null, - "modified_on": "2017-02-20T03:57:03.753292Z", - "created_on": "2017-02-20T03:53:59.274170Z", - "meta": { - "step": 4, - "wildcard_proxiable": false, - "custom_certificate_quota": 0, - "page_rule_quota": 3, - "phishing_detected": false, - "multiple_railguns_allowed": false - }, - "owner": { - "type": "user", - "id": "334234243423aaabb334342aaa343433", - "email": "noreply@github.com" - }, - "permissions": [ - "#analytics:read", - "#billing:edit", - "#billing:read", - "#cache_purge:edit", - "#dns_records:edit", - "#dns_records:read", - "#lb:edit", - "#lb:read", - "#logs:read", - "#organization:edit", - "#organization:read", - "#ssl:edit", - "#ssl:read", - "#waf:edit", - "#waf:read", - "#zone:edit", - "#zone:read", - "#zone_settings:edit", - "#zone_settings:read" - ], - "plan": { - "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "name": "Free Website", - "price": 0, - "currency": "USD", - "frequency": "", - "is_subscribed": true, - "can_subscribe": false, - "legacy_id": "free", - "legacy_discount": false, - "externally_managed": false - } - }, - { - "id": "234234243423aaabb334342aaa343434", - "name": "github.io", - "status": "pending", - "paused": false, - "type": "full", - "development_mode": 0, - "name_servers": [ - "alice.ns.cloudflare.com", - "tom.ns.cloudflare.com" - ], - "original_name_servers": [], - "original_registrar": null, - "original_dnshost": null, - "modified_on": "2017-02-20T04:12:00.732827Z", - "created_on": "2017-02-20T04:11:58.250696Z", - "meta": { - "step": 4, - "wildcard_proxiable": false, - "custom_certificate_quota": 0, - "page_rule_quota": 3, - "phishing_detected": false, - "multiple_railguns_allowed": false - }, - "owner": { - "type": "user", - "id": "334234243423aaabb334342aaa343433", - "email": "noreply@github.com" - }, - "permissions": [ - "#analytics:read", - "#billing:edit", - "#billing:read", - "#cache_purge:edit", - "#dns_records:edit", - "#dns_records:read", - "#lb:edit", - "#lb:read", - "#logs:read", - "#organization:edit", - "#organization:read", - "#ssl:edit", - "#ssl:read", - "#waf:edit", - "#waf:read", - "#zone:edit", - "#zone:read", - "#zone_settings:edit", - "#zone_settings:read" - ], - "plan": { - "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "name": "Free Website", - "price": 0, - "currency": "USD", - "frequency": "", - "is_subscribed": true, - "can_subscribe": false, - "legacy_id": "free", - "legacy_discount": false, - "externally_managed": false - } - } - ], - "result_info": { - "page": 1, - "per_page": 2, - "total_pages": 2, - "count": 2, - "total_count": 4 - }, - "success": true, - "errors": [], - "messages": [] -} diff --git a/tests/fixtures/cloudflare-zones-page-2.json b/tests/fixtures/cloudflare-zones-page-2.json deleted file mode 100644 index bc4abdf..0000000 --- a/tests/fixtures/cloudflare-zones-page-2.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "result": [ - { - "id": "234234243423aaabb334342aaa343434", - "name": "githubusercontent.com", - "status": "pending", - "paused": false, - "type": "full", - "development_mode": 0, - "name_servers": [ - "alice.ns.cloudflare.com", - "tom.ns.cloudflare.com" - ], - "original_name_servers": [], - "original_registrar": null, - "original_dnshost": null, - "modified_on": "2017-02-20T04:06:46.019706Z", - "created_on": "2017-02-20T04:05:51.683040Z", - "meta": { - "step": 4, - "wildcard_proxiable": false, - "custom_certificate_quota": 0, - "page_rule_quota": 3, - "phishing_detected": false, - "multiple_railguns_allowed": false - }, - "owner": { - "type": "user", - "id": "334234243423aaabb334342aaa343433", - "email": "noreply@github.com" - }, - "permissions": [ - "#analytics:read", - "#billing:edit", - "#billing:read", - "#cache_purge:edit", - "#dns_records:edit", - "#dns_records:read", - "#lb:edit", - "#lb:read", - "#logs:read", - "#organization:edit", - "#organization:read", - "#ssl:edit", - "#ssl:read", - "#waf:edit", - "#waf:read", - "#zone:edit", - "#zone:read", - "#zone_settings:edit", - "#zone_settings:read" - ], - "plan": { - "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "name": "Free Website", - "price": 0, - "currency": "USD", - "frequency": "", - "is_subscribed": true, - "can_subscribe": false, - "legacy_id": "free", - "legacy_discount": false, - "externally_managed": false - } - }, - { - "id": "234234243423aaabb334342aaa343435", - "name": "unit.tests", - "status": "pending", - "paused": false, - "type": "full", - "development_mode": 0, - "name_servers": [ - "alice.ns.cloudflare.com", - "tom.ns.cloudflare.com" - ], - "original_name_servers": [], - "original_registrar": null, - "original_dnshost": null, - "modified_on": "2017-02-20T04:10:23.687329Z", - "created_on": "2017-02-20T04:10:18.294562Z", - "meta": { - "step": 4, - "wildcard_proxiable": false, - "custom_certificate_quota": 0, - "page_rule_quota": 3, - "phishing_detected": false, - "multiple_railguns_allowed": false - }, - "owner": { - "type": "user", - "id": "334234243423aaabb334342aaa343433", - "email": "noreply@github.com" - }, - "permissions": [ - "#analytics:read", - "#billing:edit", - "#billing:read", - "#cache_purge:edit", - "#dns_records:edit", - "#dns_records:read", - "#lb:edit", - "#lb:read", - "#logs:read", - "#organization:edit", - "#organization:read", - "#ssl:edit", - "#ssl:read", - "#waf:edit", - "#waf:read", - "#zone:edit", - "#zone:read", - "#zone_settings:edit", - "#zone_settings:read" - ], - "plan": { - "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "name": "Free Website", - "price": 0, - "currency": "USD", - "frequency": "", - "is_subscribed": true, - "can_subscribe": false, - "legacy_id": "free", - "legacy_discount": false, - "externally_managed": false - } - } - ], - "result_info": { - "page": 2, - "per_page": 2, - "total_pages": 2, - "count": 2, - "total_count": 4 - }, - "success": true, - "errors": [], - "messages": [] -} diff --git a/tests/test_octodns_provider_cloudflare.py b/tests/test_octodns_provider_cloudflare.py index b1303f7..1053b7d 100644 --- a/tests/test_octodns_provider_cloudflare.py +++ b/tests/test_octodns_provider_cloudflare.py @@ -5,1631 +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, Update -from octodns.provider.base import Plan -from octodns.provider.cloudflare import CloudflareProvider, \ - CloudflareRateLimitError -from octodns.provider.yaml import YamlProvider -from octodns.zone import Zone +class TestCloudflareShim(TestCase): -def set_record_proxied_flag(record, proxied): - try: - record._octodns['cloudflare']['proxied'] = proxied - except KeyError: - record._octodns['cloudflare'] = { - 'proxied': proxied - } - - return record - - -class TestCloudflareProvider(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 - - empty = {'result': [], 'result_info': {'count': 0, 'per_page': 0}} - - def test_populate(self): - provider = CloudflareProvider('test', 'email', 'token', retry_period=0) - - # Bad requests - with requests_mock() as mock: - mock.get(ANY, status_code=400, - text='{"success":false,"errors":[{"code":1101,' - '"message":"request was invalid"}],' - '"messages":[],"result":null}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - - self.assertEquals('CloudflareError', type(ctx.exception).__name__) - self.assertEquals('request was invalid', str(ctx.exception)) - - # Bad auth - with requests_mock() as mock: - mock.get(ANY, status_code=403, - text='{"success":false,"errors":[{"code":9103,' - '"message":"Unknown X-Auth-Key or X-Auth-Email"}],' - '"messages":[],"result":null}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals('CloudflareAuthenticationError', - type(ctx.exception).__name__) - self.assertEquals('Unknown X-Auth-Key or X-Auth-Email', - str(ctx.exception)) - - # Bad auth, unknown resp - with requests_mock() as mock: - mock.get(ANY, status_code=403, text='{}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals('CloudflareAuthenticationError', - type(ctx.exception).__name__) - self.assertEquals('Cloudflare error', 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) - - # Rate Limit error - with requests_mock() as mock: - mock.get(ANY, status_code=429, - text='{"success":false,"errors":[{"code":10100,' - '"message":"More than 1200 requests per 300 seconds ' - 'reached. Please wait and consider throttling your ' - 'request speed"}],"messages":[],"result":null}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - - self.assertEquals('CloudflareRateLimitError', - type(ctx.exception).__name__) - self.assertEquals('More than 1200 requests per 300 seconds ' - 'reached. Please wait and consider throttling ' - 'your request speed', str(ctx.exception)) - - # Rate Limit error, unknown resp - with requests_mock() as mock: - mock.get(ANY, status_code=429, text='{}') - - with self.assertRaises(Exception) as ctx: - zone = Zone('unit.tests.', []) - provider.populate(zone) - - self.assertEquals('CloudflareRateLimitError', - type(ctx.exception).__name__) - self.assertEquals('Cloudflare error', str(ctx.exception)) - - # Non-existent zone doesn't populate anything - with requests_mock() as mock: - mock.get(ANY, status_code=200, json=self.empty) - - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals(set(), zone.records) - - # re-populating the same non-existent zone uses cache and makes no - # calls - again = Zone('unit.tests.', []) - provider.populate(again) - self.assertEquals(set(), again.records) - - # bust zone cache - provider._zones = None - - # existing zone with data - with requests_mock() as mock: - base = 'https://api.cloudflare.com/client/v4/zones' - - # zones - with open('tests/fixtures/cloudflare-zones-page-1.json') as fh: - mock.get(f'{base}?page=1', status_code=200, text=fh.read()) - with open('tests/fixtures/cloudflare-zones-page-2.json') as fh: - mock.get(f'{base}?page=2', status_code=200, text=fh.read()) - mock.get(f'{base}?page=3', status_code=200, - json={'result': [], 'result_info': {'count': 0, - 'per_page': 0}}) - - base = f'{base}/234234243423aaabb334342aaa343435' - - # pagerules/URLFWD - with open('tests/fixtures/cloudflare-pagerules.json') as fh: - mock.get(f'{base}/pagerules?status=active', - status_code=200, text=fh.read()) - - # records - base = f'{base}/dns_records' - with open('tests/fixtures/cloudflare-dns_records-' - 'page-1.json') as fh: - mock.get(f'{base}?page=1', status_code=200, text=fh.read()) - with open('tests/fixtures/cloudflare-dns_records-' - 'page-2.json') as fh: - mock.get(f'{base}?page=2', status_code=200, text=fh.read()) - with open('tests/fixtures/cloudflare-dns_records-' - 'page-3.json') as fh: - mock.get(f'{base}?page=3', status_code=200, text=fh.read()) - - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals(19, len(zone.records)) - - changes = self.expected.changes(zone, provider) - - self.assertEquals(4, len(changes)) - - # re-populating the same zone/records comes out of cache, no calls - again = Zone('unit.tests.', []) - provider.populate(again) - self.assertEquals(19, len(again.records)) - - def test_apply(self): - provider = CloudflareProvider('test', 'email', 'token', retry_period=0) - - provider._request = Mock() - - provider._request.side_effect = [ - self.empty, # no zones - { - 'result': { - 'id': 42, - } - }, # zone create - ] + [None] * 27 # individual record creates - - # non-existent zone, create everything - plan = provider.plan(self.expected) - self.assertEquals(17, len(plan.changes)) - self.assertEquals(17, provider.apply(plan)) - self.assertFalse(plan.exists) - - provider._request.assert_has_calls([ - # created the domain - call('POST', '/zones', data={ - 'jump_start': False, - 'name': 'unit.tests' - }), - # created at least one of the record with expected data - call('POST', '/zones/42/dns_records', data={ - 'content': 'ns1.unit.tests.', - 'type': 'NS', - 'name': 'under.unit.tests', - 'ttl': 3600 - }), - # make sure semicolons are not escaped when sending data - call('POST', '/zones/42/dns_records', data={ - 'content': 'v=DKIM1;k=rsa;s=email;h=sha256;' - 'p=A/kinda+of/long/string+with+numb3rs', - 'type': 'TXT', - 'name': 'txt.unit.tests', - 'ttl': 600 - }), - # create at least one pagerules - call('POST', '/zones/42/pagerules', data={ - 'targets': [ - { - 'target': 'url', - 'constraint': { - 'operator': 'matches', - 'value': 'urlfwd.unit.tests/' - } - } - ], - 'actions': [ - { - 'id': 'forwarding_url', - 'value': { - 'url': 'http://www.unit.tests', - 'status_code': 302 - } - } - ], - 'status': 'active' - }), - ], True) - # expected number of total calls - self.assertEquals(29, provider._request.call_count) - - provider._request.reset_mock() - - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997653", - "type": "A", - "name": "www.unit.tests", - "content": "1.2.3.4", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997654", - "type": "A", - "name": "www.unit.tests", - "content": "2.2.3.4", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:44.030044Z", - "created_on": "2017-03-11T18:01:44.030044Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997655", - "type": "A", - "name": "nc.unit.tests", - "content": "3.2.3.4", - "proxiable": True, - "proxied": False, - "ttl": 120, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:44.030044Z", - "created_on": "2017-03-11T18:01:44.030044Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997655", - "type": "A", - "name": "ttl.unit.tests", - "content": "4.2.3.4", - "proxiable": True, - "proxied": False, - "ttl": 600, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:44.030044Z", - "created_on": "2017-03-11T18:01:44.030044Z", - "meta": { - "auto_added": False - } - }, - { - "id": "2a9140b17ffb0e6aed826049eec970b7", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwd.unit.tests/" - } - } - ], - "actions": [ - { - "id": "forwarding_url", - "value": { - "url": "https://www.unit.tests", - "status_code": 302 - } - } - ], - "priority": 1, - "status": "active", - "created_on": "2021-06-25T20:10:50.000000Z", - "modified_on": "2021-06-28T22:38:10.000000Z" - }, - { - "id": "2a9141b18ffb0e6aed826050eec970b8", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwdother.unit.tests/target" - } - } - ], - "actions": [ - { - "id": "forwarding_url", - "value": { - "url": "https://target.unit.tests", - "status_code": 301 - } - } - ], - "priority": 2, - "status": "active", - "created_on": "2021-06-25T20:10:50.000000Z", - "modified_on": "2021-06-28T22:38:10.000000Z" - }, - ]) - - # we don't care about the POST/create return values - provider._request.return_value = {} - - # Test out the create rate-limit handling, then 9 successes - provider._request.side_effect = [ - CloudflareRateLimitError('{}'), - ] + ([None] * 5) - - wanted = Zone('unit.tests.', []) - wanted.add_record(Record.new(wanted, 'nc', { - 'ttl': 60, # TTL is below their min - 'type': 'A', - 'value': '3.2.3.4' - })) - wanted.add_record(Record.new(wanted, 'ttl', { - 'ttl': 300, # TTL change - 'type': 'A', - 'value': '3.2.3.4' - })) - wanted.add_record(Record.new(wanted, 'urlfwd', { - 'ttl': 300, - 'type': 'URLFWD', - 'value': { - 'path': '/*', # path change - 'target': 'https://www.unit.tests/', # target change - 'code': 301, # status_code change - 'masking': '2', - 'query': 0, - } - })) - - plan = provider.plan(wanted) - # only see the delete & ttl update, below min-ttl is filtered out - self.assertEquals(4, len(plan.changes)) - self.assertEquals(4, provider.apply(plan)) - self.assertTrue(plan.exists) - # creates a the new value and then deletes all the old - provider._request.assert_has_calls([ - call('DELETE', '/zones/42/' - 'pagerules/2a9141b18ffb0e6aed826050eec970b8'), - call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' - 'dns_records/fc12ab34cd5611334422ab3322997653'), - call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' - 'dns_records/fc12ab34cd5611334422ab3322997654'), - call('PUT', '/zones/42/dns_records/' - 'fc12ab34cd5611334422ab3322997655', data={ - 'content': '3.2.3.4', - 'type': 'A', - 'name': 'ttl.unit.tests', - 'proxied': False, - 'ttl': 300 - }), - call('PUT', '/zones/42/pagerules/' - '2a9140b17ffb0e6aed826049eec970b7', data={ - 'targets': [ - { - 'target': 'url', - 'constraint': { - 'operator': 'matches', - 'value': 'urlfwd.unit.tests/*' - } - } - ], - 'actions': [ - { - 'id': 'forwarding_url', - 'value': { - 'url': 'https://www.unit.tests/', - 'status_code': 301 - } - } - ], - 'status': 'active', - }), - ]) - - def test_update_add_swap(self): - provider = CloudflareProvider('test', 'email', 'token', retry_period=0) - - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997653", - "type": "A", - "name": "a.unit.tests", - "content": "1.1.1.1", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997654", - "type": "A", - "name": "a.unit.tests", - "content": "2.2.2.2", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - ]) - - provider._request = Mock() - provider._request.side_effect = [ - CloudflareRateLimitError('{}'), - self.empty, # no zones - { - 'result': { - 'id': 42, - } - }, # zone create - None, - None, - None, - None, - ] - - # Add something and delete something - zone = Zone('unit.tests.', []) - existing = Record.new(zone, 'a', { - 'ttl': 300, - 'type': 'A', - # This matches the zone data above, one to swap, one to leave - 'values': ['1.1.1.1', '2.2.2.2'], - }) - new = Record.new(zone, 'a', { - 'ttl': 300, - 'type': 'A', - # This leaves one, swaps ones, and adds one - 'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'], - }) - change = Update(existing, new) - plan = Plan(zone, zone, [change], True) - provider._apply(plan) - - # get the list of zones, create a zone, add some records, update - # something, and delete something - provider._request.assert_has_calls([ - call('GET', '/zones', params={'page': 1, 'per_page': 50}), - call('POST', '/zones', data={ - 'jump_start': False, - 'name': 'unit.tests' - }), - call('POST', '/zones/42/dns_records', data={ - 'content': '4.4.4.4', - 'type': 'A', - 'name': 'a.unit.tests', - 'proxied': False, - 'ttl': 300 - }), - call('PUT', '/zones/42/dns_records/' - 'fc12ab34cd5611334422ab3322997654', data={ - 'content': '2.2.2.2', - 'type': 'A', - 'name': 'a.unit.tests', - 'proxied': False, - 'ttl': 300 - }), - call('PUT', '/zones/42/dns_records/' - 'fc12ab34cd5611334422ab3322997653', data={ - 'content': '3.3.3.3', - 'type': 'A', - 'name': 'a.unit.tests', - 'proxied': False, - 'ttl': 300 - }), - ]) - - def test_update_delete(self): - # We need another run so that we can delete, we can't both add and - # delete in one go b/c of swaps - provider = CloudflareProvider('test', 'email', 'token', retry_period=0) - - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997653", - "type": "NS", - "name": "unit.tests", - "content": "ns1.foo.bar", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997654", - "type": "NS", - "name": "unit.tests", - "content": "ns2.foo.bar", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "2a9140b17ffb0e6aed826049eec974b7", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwd1.unit.tests/" - } - } - ], - "actions": [ - { - "id": "forwarding_url", - "value": { - "url": "https://www.unit.tests", - "status_code": 302 - } - } - ], - "priority": 1, - "status": "active", - "created_on": "2021-06-25T20:10:50.000000Z", - "modified_on": "2021-06-28T22:38:10.000000Z" - }, - { - "id": "2a9141b18ffb0e6aed826054eec970b8", - "targets": [ - { - "target": "url", - "constraint": { - "operator": "matches", - "value": "urlfwd1.unit.tests/target" - } - } - ], - "actions": [ - { - "id": "forwarding_url", - "value": { - "url": "https://target.unit.tests", - "status_code": 301 - } - } - ], - "priority": 2, - "status": "active", - "created_on": "2021-06-25T20:10:50.000000Z", - "modified_on": "2021-06-28T22:38:10.000000Z" - }, - ]) - - provider._request = Mock() - provider._request.side_effect = [ - CloudflareRateLimitError('{}'), - self.empty, # no zones - { - 'result': { - 'id': 42, - } - }, # zone create - None, - None, - None, - None, - ] - - # Add something and delete something - zone = Zone('unit.tests.', []) - existing = Record.new(zone, '', { - 'ttl': 300, - 'type': 'NS', - # This matches the zone data above, one to delete, one to leave - 'values': ['ns1.foo.bar.', 'ns2.foo.bar.'], - }) - exstingurlfwd = Record.new(zone, 'urlfwd1', { - 'ttl': 300, - 'type': 'URLFWD', - 'values': [ - { - 'path': '/', - 'target': 'https://www.unit.tests', - 'code': 302, - 'masking': '2', - 'query': 0, - }, - { - 'path': '/target', - 'target': 'https://target.unit.tests', - 'code': 301, - 'masking': '2', - 'query': 0, - } - ] - }) - new = Record.new(zone, '', { - 'ttl': 300, - 'type': 'NS', - # This leaves one and deletes one - 'value': 'ns2.foo.bar.', - }) - newurlfwd = Record.new(zone, 'urlfwd1', { - 'ttl': 300, - 'type': 'URLFWD', - 'value': { - 'path': '/', - 'target': 'https://www.unit.tests', - 'code': 302, - 'masking': '2', - 'query': 0, - } - }) - change = Update(existing, new) - changeurlfwd = Update(exstingurlfwd, newurlfwd) - plan = Plan(zone, zone, [change, changeurlfwd], True) - provider._apply(plan) - - # Get zones, create zone, create a record, delete a record - provider._request.assert_has_calls([ - call('GET', '/zones', params={'page': 1, 'per_page': 50}), - call('POST', '/zones', data={ - 'jump_start': False, - 'name': 'unit.tests' - }), - call('PUT', '/zones/42/dns_records/' - 'fc12ab34cd5611334422ab3322997654', data={ - 'content': 'ns2.foo.bar.', - 'type': 'NS', - 'name': 'unit.tests', - 'ttl': 300 - }), - call('DELETE', '/zones/42/dns_records/' - 'fc12ab34cd5611334422ab3322997653'), - call('PUT', '/zones/42/pagerules/' - '2a9140b17ffb0e6aed826049eec974b7', data={ - 'targets': [ - { - 'target': 'url', - 'constraint': { - 'operator': 'matches', - 'value': 'urlfwd1.unit.tests/' - } - } - ], - 'actions': [ - { - 'id': 'forwarding_url', - 'value': { - 'url': 'https://www.unit.tests', - 'status_code': 302 - } - } - ], - 'status': 'active' - }), - call('DELETE', '/zones/42/pagerules/' - '2a9141b18ffb0e6aed826054eec970b8'), - ]) - - def test_ptr(self): - provider = CloudflareProvider('test', 'email', 'token') - - zone = Zone('unit.tests.', []) - # PTR record - ptr_record = Record.new(zone, 'ptr', { - 'ttl': 300, - 'type': 'PTR', - 'value': 'foo.bar.com.' - }) - - ptr_record_contents = provider._gen_data(ptr_record) - self.assertEquals({ - 'name': 'ptr.unit.tests', - 'ttl': 300, - 'type': 'PTR', - 'content': 'foo.bar.com.' - }, list(ptr_record_contents)[0]) - - def test_loc(self): - self.maxDiff = None - provider = CloudflareProvider('test', 'email', 'token') - - zone = Zone('unit.tests.', []) - # LOC record - loc_record = Record.new(zone, 'example', { - 'ttl': 300, - 'type': 'LOC', - 'value': { - 'lat_degrees': 31, - 'lat_minutes': 58, - 'lat_seconds': 52.1, - 'lat_direction': 'S', - 'long_degrees': 115, - 'long_minutes': 49, - 'long_seconds': 11.7, - 'long_direction': 'E', - 'altitude': 20, - 'size': 10, - 'precision_horz': 10, - 'precision_vert': 2, - } - }) - - loc_record_contents = provider._gen_data(loc_record) - self.assertEquals({ - 'name': 'example.unit.tests', - 'ttl': 300, - 'type': 'LOC', - 'data': { - 'lat_degrees': 31, - 'lat_minutes': 58, - 'lat_seconds': 52.1, - 'lat_direction': 'S', - 'long_degrees': 115, - 'long_minutes': 49, - 'long_seconds': 11.7, - 'long_direction': 'E', - 'altitude': 20, - 'size': 10, - 'precision_horz': 10, - 'precision_vert': 2, - } - }, list(loc_record_contents)[0]) - - def test_srv(self): - provider = CloudflareProvider('test', 'email', 'token') - - zone = Zone('unit.tests.', []) - # SRV record not under a sub-domain - srv_record = Record.new(zone, '_example._tcp', { - 'ttl': 300, - 'type': 'SRV', - 'value': { - 'port': 1234, - 'priority': 0, - 'target': 'nc.unit.tests.', - 'weight': 5 - } - }) - # SRV record under a sub-domain - srv_record_with_sub = Record.new(zone, '_example._tcp.sub', { - 'ttl': 300, - 'type': 'SRV', - 'value': { - 'port': 1234, - 'priority': 0, - 'target': 'nc.unit.tests.', - 'weight': 5 - } - }) - - srv_record_contents = provider._gen_data(srv_record) - srv_record_with_sub_contents = provider._gen_data(srv_record_with_sub) - self.assertEquals({ - 'name': '_example._tcp.unit.tests', - 'ttl': 300, - 'type': 'SRV', - 'data': { - 'service': '_example', - 'proto': '_tcp', - 'name': 'unit.tests.', - 'priority': 0, - 'weight': 5, - 'port': 1234, - 'target': 'nc.unit.tests' - } - }, list(srv_record_contents)[0]) - self.assertEquals({ - 'name': '_example._tcp.sub.unit.tests', - 'ttl': 300, - 'type': 'SRV', - 'data': { - 'service': '_example', - 'proto': '_tcp', - 'name': 'sub', - 'priority': 0, - 'weight': 5, - 'port': 1234, - 'target': 'nc.unit.tests' - } - }, list(srv_record_with_sub_contents)[0]) - - def test_alias(self): - provider = CloudflareProvider('test', 'email', 'token') - - # A CNAME for us to transform to ALIAS - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - ]) - - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals(1, len(zone.records)) - record = list(zone.records)[0] - self.assertEquals('', record.name) - self.assertEquals('unit.tests.', record.fqdn) - self.assertEquals('ALIAS', record._type) - self.assertEquals('www.unit.tests.', record.value) - - # Make sure we transform back to CNAME going the other way - contents = provider._gen_data(record) - self.assertEquals({ - 'content': 'www.unit.tests.', - 'name': 'unit.tests', - 'proxied': False, - 'ttl': 300, - 'type': 'CNAME' - }, list(contents)[0]) - - def test_gen_key(self): - provider = CloudflareProvider('test', 'email', 'token') - - for expected, data in ( - ('foo.bar.com.', { - 'content': 'foo.bar.com.', - 'type': 'CNAME', - }), - ('10 foo.bar.com.', { - 'content': 'foo.bar.com.', - 'priority': 10, - 'type': 'MX', - }), - ('0 tag some-value', { - 'data': { - 'flags': 0, - 'tag': 'tag', - 'value': 'some-value', - }, - 'type': 'CAA', - }), - ('42 100 thing-were-pointed.at 101', { - 'data': { - 'port': 42, - 'priority': 100, - 'target': 'thing-were-pointed.at', - 'weight': 101, - }, - 'type': 'SRV', - }), - ('31 58 52.1 S 115 49 11.7 E 20 10 10 2', { - 'data': { - 'lat_degrees': 31, - 'lat_minutes': 58, - 'lat_seconds': 52.1, - 'lat_direction': 'S', - 'long_degrees': 115, - 'long_minutes': 49, - 'long_seconds': 11.7, - 'long_direction': 'E', - 'altitude': 20, - 'size': 10, - 'precision_horz': 10, - 'precision_vert': 2, - }, - 'type': 'LOC', - }), - ): - self.assertEqual(expected, provider._gen_key(data)) - - def test_cdn(self): - provider = CloudflareProvider('test', 'email', 'token', True) - - # A CNAME for us to transform to ALIAS - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "cname.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "A", - "name": "a.unit.tests", - "content": "1.1.1.1", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "A", - "name": "a.unit.tests", - "content": "1.1.1.2", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "A", - "name": "multi.unit.tests", - "content": "1.1.1.3", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "AAAA", - "name": "multi.unit.tests", - "content": "::1", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - ]) - - zone = Zone('unit.tests.', []) - provider.populate(zone) - - # the two A records get merged into one CNAME record pointing to - # the CDN. - self.assertEquals(3, len(zone.records)) - - ordered = sorted(zone.records, key=lambda r: r.name) - - record = ordered[0] - self.assertEquals('a', record.name) - self.assertEquals('a.unit.tests.', record.fqdn) - self.assertEquals('CNAME', record._type) - self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value) - - record = ordered[1] - self.assertEquals('cname', record.name) - self.assertEquals('cname.unit.tests.', record.fqdn) - self.assertEquals('CNAME', record._type) - self.assertEquals('cname.unit.tests.cdn.cloudflare.net.', record.value) - - record = ordered[2] - self.assertEquals('multi', record.name) - self.assertEquals('multi.unit.tests.', record.fqdn) - self.assertEquals('CNAME', record._type) - self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value) - - # CDN enabled records can't be updated, we don't know the real values - # never point a Cloudflare record to itself. - wanted = Zone('unit.tests.', []) - wanted.add_record(Record.new(wanted, 'cname', { - 'ttl': 300, - 'type': 'CNAME', - 'value': 'change.unit.tests.cdn.cloudflare.net.' - })) - wanted.add_record(Record.new(wanted, 'new', { - 'ttl': 300, - 'type': 'CNAME', - 'value': 'new.unit.tests.cdn.cloudflare.net.' - })) - wanted.add_record(Record.new(wanted, 'created', { - 'ttl': 300, - 'type': 'CNAME', - 'value': 'www.unit.tests.' - })) - - plan = provider.plan(wanted) - self.assertEquals(1, len(plan.changes)) - - def test_cdn_alias(self): - provider = CloudflareProvider('test', 'email', 'token', True) - - # A CNAME for us to transform to ALIAS - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - }, - ]) - - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals(1, len(zone.records)) - record = list(zone.records)[0] - self.assertEquals('', record.name) - self.assertEquals('unit.tests.', record.fqdn) - self.assertEquals('ALIAS', record._type) - self.assertEquals('unit.tests.cdn.cloudflare.net.', record.value) - - # CDN enabled records can't be updated, we don't know the real values - # never point a Cloudflare record to itself. - wanted = Zone('unit.tests.', []) - wanted.add_record(Record.new(wanted, '', { - 'ttl': 300, - 'type': 'ALIAS', - 'value': 'change.unit.tests.cdn.cloudflare.net.' - })) - - plan = provider.plan(wanted) - self.assertEquals(False, hasattr(plan, 'changes')) - - def test_unproxiabletype_recordfor_returnsrecordwithnocloudflare(self): - provider = CloudflareProvider('test', 'email', 'token') - name = "unit.tests" - _type = "NS" - zone_records = [ - { - "id": "fc12ab34cd5611334422ab3322997654", - "type": _type, - "name": name, - "content": "ns2.foo.bar", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ] - provider.zone_records = Mock(return_value=zone_records) - zone = Zone('unit.tests.', []) - provider.populate(zone) - - record = provider._record_for(zone, name, _type, zone_records, False) - - self.assertFalse('cloudflare' in record._octodns) - - def test_proxiabletype_recordfor_retrecordwithcloudflareunproxied(self): - provider = CloudflareProvider('test', 'email', 'token') - name = "multi.unit.tests" - _type = "AAAA" - zone_records = [ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": _type, - "name": name, - "content": "::1", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ] - provider.zone_records = Mock(return_value=zone_records) - zone = Zone('unit.tests.', []) - provider.populate(zone) - - record = provider._record_for(zone, name, _type, zone_records, False) - - self.assertFalse(record._octodns['cloudflare']['proxied']) - - def test_proxiabletype_recordfor_returnsrecordwithcloudflareproxied(self): - provider = CloudflareProvider('test', 'email', 'token') - name = "multi.unit.tests" - _type = "AAAA" - zone_records = [ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": _type, - "name": name, - "content": "::1", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ] - provider.zone_records = Mock(return_value=zone_records) - zone = Zone('unit.tests.', []) - provider.populate(zone) - - record = provider._record_for(zone, name, _type, zone_records, False) - - self.assertTrue(record._octodns['cloudflare']['proxied']) - - def test_proxiedrecordandnewttl_includechange_returnsfalse(self): - provider = CloudflareProvider('test', 'email', 'token') - zone = Zone('unit.tests.', []) - existing = set_record_proxied_flag( - Record.new(zone, 'a', { - 'ttl': 1, - 'type': 'A', - 'values': ['1.1.1.1', '2.2.2.2'] - }), True - ) - new = Record.new(zone, 'a', { - 'ttl': 300, - 'type': 'A', - 'values': ['1.1.1.1', '2.2.2.2'] - }) - change = Update(existing, new) - - include_change = provider._include_change(change) - - self.assertFalse(include_change) - - def test_unproxiabletype_gendata_returnsnoproxied(self): - provider = CloudflareProvider('test', 'email', 'token') - zone = Zone('unit.tests.', []) - record = Record.new(zone, 'a', { - 'ttl': 3600, - 'type': 'NS', - 'value': 'ns1.unit.tests.' - }) - - data = next(provider._gen_data(record)) - - self.assertFalse('proxied' in data) - - def test_proxiabletype_gendata_returnsunproxied(self): - provider = CloudflareProvider('test', 'email', 'token') - zone = Zone('unit.tests.', []) - record = set_record_proxied_flag( - Record.new(zone, 'a', { - 'ttl': 300, - 'type': 'A', - 'value': '1.2.3.4' - }), False - ) - - data = next(provider._gen_data(record)) - - self.assertFalse(data['proxied']) - - def test_proxiabletype_gendata_returnsproxied(self): - provider = CloudflareProvider('test', 'email', 'token') - zone = Zone('unit.tests.', []) - record = set_record_proxied_flag( - Record.new(zone, 'a', { - 'ttl': 300, - 'type': 'A', - 'value': '1.2.3.4' - }), True - ) - - data = next(provider._gen_data(record)) - - self.assertTrue(data['proxied']) - - def test_createrecord_extrachanges_returnsemptylist(self): - provider = CloudflareProvider('test', 'email', 'token') - provider.zone_records = Mock(return_value=[]) - existing = Zone('unit.tests.', []) - provider.populate(existing) - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - desired = Zone('unit.tests.', []) - provider.populate(desired) - changes = existing.changes(desired, provider) - - extra_changes = provider._extra_changes(existing, desired, changes) - - self.assertFalse(extra_changes) - - def test_updaterecord_extrachanges_returnsemptylist(self): - provider = CloudflareProvider('test', 'email', 'token') - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 120, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - existing = Zone('unit.tests.', []) - provider.populate(existing) - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - desired = Zone('unit.tests.', []) - provider.populate(desired) - changes = existing.changes(desired, provider) - - extra_changes = provider._extra_changes(existing, desired, changes) - - self.assertFalse(extra_changes) - - def test_deleterecord_extrachanges_returnsemptylist(self): - provider = CloudflareProvider('test', 'email', 'token') - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - existing = Zone('unit.tests.', []) - provider.populate(existing) - provider.zone_records = Mock(return_value=[]) - desired = Zone('unit.tests.', []) - provider.populate(desired) - changes = existing.changes(desired, provider) - - extra_changes = provider._extra_changes(existing, desired, changes) - - self.assertFalse(extra_changes) - - def test_proxify_extrachanges_returnsupdatelist(self): - provider = CloudflareProvider('test', 'email', 'token') - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - existing = Zone('unit.tests.', []) - provider.populate(existing) - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - desired = Zone('unit.tests.', []) - provider.populate(desired) - changes = existing.changes(desired, provider) - - extra_changes = provider._extra_changes(existing, desired, changes) - - self.assertEquals(1, len(extra_changes)) - self.assertFalse( - extra_changes[0].existing._octodns['cloudflare']['proxied'] - ) - self.assertTrue( - extra_changes[0].new._octodns['cloudflare']['proxied'] - ) - - def test_unproxify_extrachanges_returnsupdatelist(self): - provider = CloudflareProvider('test', 'email', 'token') - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": True, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - existing = Zone('unit.tests.', []) - provider.populate(existing) - provider.zone_records = Mock(return_value=[ - { - "id": "fc12ab34cd5611334422ab3322997642", - "type": "CNAME", - "name": "a.unit.tests", - "content": "www.unit.tests", - "proxiable": True, - "proxied": False, - "ttl": 300, - "locked": False, - "zone_id": "ff12ab34cd5611334422ab3322997650", - "zone_name": "unit.tests", - "modified_on": "2017-03-11T18:01:43.420689Z", - "created_on": "2017-03-11T18:01:43.420689Z", - "meta": { - "auto_added": False - } - } - ]) - desired = Zone('unit.tests.', []) - provider.populate(desired) - changes = existing.changes(desired, provider) - - extra_changes = provider._extra_changes(existing, desired, changes) - - self.assertEquals(1, len(extra_changes)) - self.assertTrue( - extra_changes[0].existing._octodns['cloudflare']['proxied'] - ) - self.assertFalse( - extra_changes[0].new._octodns['cloudflare']['proxied'] - ) - - def test_emailless_auth(self): - provider = CloudflareProvider('test', token='token 123', - email='email 234') - headers = provider._sess.headers - self.assertEquals('email 234', headers['X-Auth-Email']) - self.assertEquals('token 123', headers['X-Auth-Key']) - - provider = CloudflareProvider('test', token='token 123') - headers = provider._sess.headers - self.assertEquals('Bearer token 123', headers['Authorization']) - - def test_retry_behavior(self): - provider = CloudflareProvider('test', token='token 123', - email='email 234', retry_period=0) - result = { - "success": True, - "errors": [], - "messages": [], - "result": [], - "result_info": { - "count": 1, - "per_page": 50 - } - } - zone = Zone('unit.tests.', []) - provider._request = Mock() - - # No retry required, just calls and is returned - provider._zones = None - provider._request.reset_mock() - provider._request.side_effect = [result] - self.assertEquals([], provider.zone_records(zone)) - provider._request.assert_has_calls([call('GET', '/zones', - params={'page': 1, - 'per_page': 50})]) - - # One retry required - provider._zones = None - provider._request.reset_mock() - provider._request.side_effect = [ - CloudflareRateLimitError('{}'), - result - ] - self.assertEquals([], provider.zone_records(zone)) - provider._request.assert_has_calls([call('GET', '/zones', - params={'page': 1, - 'per_page': 50})]) - - # Two retries required - provider._zones = None - provider._request.reset_mock() - provider._request.side_effect = [ - CloudflareRateLimitError('{}'), - CloudflareRateLimitError('{}'), - result - ] - self.assertEquals([], provider.zone_records(zone)) - provider._request.assert_has_calls([call('GET', '/zones', - params={'page': 1, - 'per_page': 50})]) - - # # Exhaust our retries - provider._zones = None - provider._request.reset_mock() - provider._request.side_effect = [ - CloudflareRateLimitError({"errors": [{"message": "first"}]}), - CloudflareRateLimitError({"errors": [{"message": "boo"}]}), - CloudflareRateLimitError({"errors": [{"message": "boo"}]}), - CloudflareRateLimitError({"errors": [{"message": "boo"}]}), - CloudflareRateLimitError({"errors": [{"message": "last"}]}), - ] - with self.assertRaises(CloudflareRateLimitError) as ctx: - provider.zone_records(zone) - self.assertEquals('last', str(ctx.exception)) - - def test_ttl_mapping(self): - provider = CloudflareProvider('test', 'email', 'token') - - self.assertEquals(120, provider._ttl_data(120)) - self.assertEquals(120, provider._ttl_data(120)) - self.assertEquals(3600, provider._ttl_data(3600)) - self.assertEquals(300, provider._ttl_data(1)) + def test_missing(self): + with self.assertRaises(ModuleNotFoundError): + from octodns.provider.cloudflare import CloudflareProvider + CloudflareProvider