Browse Source

Merge remote-tracking branch 'origin/master' into extract-azure

pull/833/head
Ross McFarland 4 years ago
parent
commit
93ac64d65c
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
40 changed files with 190 additions and 19998 deletions
  1. +8
    -0
      CHANGELOG.md
  2. +15
    -8
      README.md
  3. +13
    -848
      octodns/provider/cloudflare.py
  4. +15
    -1110
      octodns/provider/constellix.py
  5. +15
    -342
      octodns/provider/digitalocean.py
  6. +15
    -414
      octodns/provider/dnsmadeeasy.py
  7. +12
    -1394
      octodns/provider/dyn.py
  8. +17
    -434
      octodns/provider/easydns.py
  9. +14
    -510
      octodns/provider/edgedns.py
  10. +16
    -108
      octodns/provider/etc_hosts.py
  11. +6
    -7
      octodns/provider/fastdns.py
  12. +0
    -2
      requirements.txt
  13. +0
    -188
      tests/fixtures/cloudflare-dns_records-page-1.json
  14. +0
    -238
      tests/fixtures/cloudflare-dns_records-page-2.json
  15. +0
    -128
      tests/fixtures/cloudflare-dns_records-page-3.json
  16. +0
    -103
      tests/fixtures/cloudflare-pagerules.json
  17. +0
    -140
      tests/fixtures/cloudflare-zones-page-1.json
  18. +0
    -140
      tests/fixtures/cloudflare-zones-page-2.json
  19. +0
    -28
      tests/fixtures/constellix-domains.json
  20. +0
    -34
      tests/fixtures/constellix-geofilters.json
  21. +0
    -62
      tests/fixtures/constellix-pools.json
  22. +0
    -696
      tests/fixtures/constellix-records.json
  23. +0
    -188
      tests/fixtures/digitalocean-page-1.json
  24. +0
    -111
      tests/fixtures/digitalocean-page-2.json
  25. +0
    -16
      tests/fixtures/dnsmadeeasy-domains.json
  26. +0
    -344
      tests/fixtures/dnsmadeeasy-records.json
  27. +0
    -4190
      tests/fixtures/dyn-traffic-director-get.json
  28. +0
    -296
      tests/fixtures/easydns-records.json
  29. +0
    -35
      tests/fixtures/edgedns-invalid-content.json
  30. +0
    -166
      tests/fixtures/edgedns-records-prev-other.json
  31. +0
    -166
      tests/fixtures/edgedns-records-prev.json
  32. +0
    -173
      tests/fixtures/edgedns-records.json
  33. +5
    -1624
      tests/test_octodns_provider_cloudflare.py
  34. +5
    -1885
      tests/test_octodns_provider_constellix.py
  35. +5
    -263
      tests/test_octodns_provider_digitalocean.py
  36. +5
    -217
      tests/test_octodns_provider_dnsmadeeasy.py
  37. +5
    -2644
      tests/test_octodns_provider_dyn.py
  38. +5
    -434
      tests/test_octodns_provider_easydns.py
  39. +9
    -145
      tests/test_octodns_provider_edgedns.py
  40. +5
    -167
      tests/test_octodns_provider_etc_hosts.py

+ 8
- 0
CHANGELOG.md View File

@ -7,7 +7,15 @@
https://github.com/octodns/octodns/pull/822 for more information. Providers
that have been extracted in this release include:
* [AzureProvider](https://github.com/octodns/octodns-azure/)
* [AkamaiProvider](https://github.com/octodns/octodns-edgedns/)
* [CloudflareProvider](https://github.com/octodns/octodns-cloudflare/)
* [ConstellixProvider](https://github.com/octodns/octodns-constellix/)
* [DigitalOceanProvider](https://github.com/octodns/octodns-digitalocean/)
* [DnsimpleProvider](https://github.com/octodns/octodns-dnsimple/)
* [DnsMadeEasyProvider](https://github.com/octodns/octodns-dnsmadeeasy/)
* [DynProvider](https://github.com/octodns/octodns-dynprovider/)
* [EasyDnsProvider](https://github.com/octodns/octodns-easydns/)
* [EtcHostsProvider](https://github.com/octodns/octodns-etchosts/)
* [Ns1Provider](https://github.com/octodns/octodns-ns1/)
* [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/)
* [Route53Provider](https://github.com/octodns/octodns-route53/) also


+ 15
- 8
README.md View File

@ -193,15 +193,15 @@ The table below lists the providers octoDNS supports. We're currently in the pro
| Provider | Module | Requirements | Record Support | Dynamic | Notes |
|--|--|--|--|--|--|
| [AzureProvider](https://github.com/octodns/octodns-azure/) | [octodns_azure](https://github.com/octodns/octodns-azure/) | | | | |
| [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 |
| [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 |
| [AkamaiProvider](https://github.com/octodns/octodns-edgedns/) | [octodns_edgedns](https://github.com/octodns/octodns-edgedns/) | | | | |
| [CloudflareProvider](https://github.com/octodns/octodns-cloudflare/) | [octodns_cloudflare](https://github.com/octodns/octodns-cloudflare/) | | | | |
| [ConstellixProvider](https://github.com/octodns/octodns-constellix/) | [octodns_constellix](https://github.com/octodns/octodns-constellix/) | | | | |
| [DigitalOceanProvider](https://github.com/octodns/octodns-digitalocean/) | [octodns_digitalocean](https://github.com/octodns/octodns-digitalocean/) | | | | |
| [DnsMadeEasyProvider](https://github.com/octodns/octodns-dnsmadeeasy/) | [octodns_dnsmadeeasy](https://github.com/octodns/octodns-dnsmadeeasy/) | | | | |
| [DnsimpleProvider](https://github.com/octodns/octodns-dnsimple/) | [octodns_dnsimple](https://github.com/octodns/octodns-dnsimple/) | | | | |
| [DynProvider](/octodns/provider/dyn.py) | | dyn | All | Both | |
| [EasyDNSProvider](/octodns/provider/easydns.py) | | | A, AAAA, CAA, CNAME, MX, NAPTR, NS, SRV, TXT | No | |
| [EtcHostsProvider](/octodns/provider/etc_hosts.py) | | | A, AAAA, ALIAS, CNAME | No | |
| [DynProvider](https://github.com/octodns/octodns-dyn/) (deprecated) | [octodns_dyn](https://github.com/octodns/octodns-dyn/) | | | | |
| [EasyDnsProvider](https://github.com/octodns/octodns-easydns/) | [octodns_easydns](https://github.com/octodns/octodns-easydns/) | | | | |
| [EtcHostsProvider](https://github.com/octodns/octodns-etchosts/) | [octodns_etchosts](https://github.com/octodns/octodns-etchosts/) | | | | |
| [EnvVarSource](/octodns/source/envvar.py) | | | TXT | No | read-only environment variable injection |
| [GandiProvider](/octodns/provider/gandi.py) | | | A, AAAA, ALIAS, CAA, CNAME, DNAME, MX, NS, PTR, SPF, SRV, SSHFP, TXT | No | |
| [GCoreProvider](/octodns/provider/gcore.py) | | | A, AAAA, NS, MX, TXT, SRV, CNAME, PTR | Dynamic | |
@ -221,6 +221,13 @@ The table below lists the providers octoDNS supports. We're currently in the pro
| [TinyDnsFileSource](/octodns/source/tinydns.py) | | | A, CNAME, MX, NS, PTR | No | read-only |
| [YamlProvider](/octodns/provider/yaml.py) | | | All | Yes | config |
### Updating to use extracted providers
1. Include the extracted module in your python environment, e.g. if using Route53 that would require adding the `octodns_route53` module to your requirements.txt, setup.py, or similar.
1. Update the `class` value for your provider to the new path, e.g. again for Route53 that would be replacing `octodns.provider.route53.Route53Provider` with `octodns_route53.Route53Provider`
The module required and provider class path for extracted providers can be found in the table above.
#### Notes
* ALIAS support varies a lot from provider to provider care should be taken to verify that your needs are met in detail.


+ 13
- 848
octodns/provider/cloudflare.py View File

@ -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

+ 15
- 1110
octodns/provider/constellix.py
File diff suppressed because it is too large
View File


+ 15
- 342
octodns/provider/digitalocean.py View File

@ -5,345 +5,18 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from collections import defaultdict
from requests import Session
import logging
from ..record import Record
from . import ProviderException
from .base import BaseProvider
class DigitalOceanClientException(ProviderException):
pass
class DigitalOceanClientNotFound(DigitalOceanClientException):
def __init__(self):
super(DigitalOceanClientNotFound, self).__init__('Not Found')
class DigitalOceanClientUnauthorized(DigitalOceanClientException):
def __init__(self):
super(DigitalOceanClientUnauthorized, self).__init__('Unauthorized')
class DigitalOceanClient(object):
BASE = 'https://api.digitalocean.com/v2'
def __init__(self, token):
sess = Session()
sess.headers.update({'Authorization': f'Bearer {token}'})
self._sess = sess
def _request(self, method, path, params=None, data=None):
url = f'{self.BASE}{path}'
resp = self._sess.request(method, url, params=params, json=data)
if resp.status_code == 401:
raise DigitalOceanClientUnauthorized()
if resp.status_code == 404:
raise DigitalOceanClientNotFound()
resp.raise_for_status()
return resp
def domain(self, name):
path = f'/domains/{name}'
return self._request('GET', path).json()
def domain_create(self, name):
# Digitalocean requires an IP on zone creation
self._request('POST', '/domains', data={'name': name,
'ip_address': '192.0.2.1'})
# After the zone is created, immediately delete the record
records = self.records(name)
for record in records:
if record['name'] == '' and record['type'] == 'A':
self.record_delete(name, record['id'])
def records(self, zone_name):
path = f'/domains/{zone_name}/records'
ret = []
page = 1
while True:
data = self._request('GET', path, {'page': page}).json()
ret += data['domain_records']
links = data['links']
# https://developers.digitalocean.com/documentation/v2/#links
# pages exists if there is more than 1 page
# last doesn't exist if you're on the last page
try:
links['pages']['last']
page += 1
except KeyError:
break
for record in ret:
# change any apex record to empty string
if record['name'] == '@':
record['name'] = ''
# change any apex value to zone name
if record['data'] == '@':
record['data'] = zone_name
return ret
def record_create(self, zone_name, params):
path = f'/domains/{zone_name}/records'
# change empty name string to @, DO uses @ for apex record names
if params['name'] == '':
params['name'] = '@'
self._request('POST', path, data=params)
def record_delete(self, zone_name, record_id):
path = f'/domains/{zone_name}/records/{record_id}'
self._request('DELETE', path)
class DigitalOceanProvider(BaseProvider):
'''
DigitalOcean DNS provider using API v2
digitalocean:
class: octodns.provider.digitalocean.DigitalOceanProvider
# Your DigitalOcean API token (required)
token: foo
'''
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV'))
def __init__(self, id, token, *args, **kwargs):
self.log = logging.getLogger(f'DigitalOceanProvider[{id}]')
self.log.debug('__init__: id=%s, token=***', id)
super(DigitalOceanProvider, self).__init__(id, *args, **kwargs)
self._client = DigitalOceanClient(token)
self._zone_records = {}
def _data_for_multiple(self, _type, records):
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': [r['data'] for r in records]
}
_data_for_A = _data_for_multiple
_data_for_AAAA = _data_for_multiple
def _data_for_CAA(self, _type, records):
values = []
for record in records:
values.append({
'flags': record['flags'],
'tag': record['tag'],
'value': record['data'],
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def _data_for_CNAME(self, _type, records):
record = records[0]
return {
'ttl': record['ttl'],
'type': _type,
'value': f'{record["data"]}.'
}
def _data_for_MX(self, _type, records):
values = []
for record in records:
values.append({
'preference': record['priority'],
'exchange': f'{record["data"]}.'
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def _data_for_NS(self, _type, records):
values = []
for record in records:
values.append(f'{record["data"]}.')
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values,
}
def _data_for_SRV(self, _type, records):
values = []
for record in records:
target = f'{record["data"]}.' if record['data'] != "." else "."
values.append({
'port': record['port'],
'priority': record['priority'],
'target': target,
'weight': record['weight']
})
return {
'type': _type,
'ttl': records[0]['ttl'],
'values': values
}
def _data_for_TXT(self, _type, records):
values = [value['data'].replace(';', '\\;') for value in records]
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def zone_records(self, zone):
if zone.name not in self._zone_records:
try:
self._zone_records[zone.name] = \
self._client.records(zone.name[:-1])
except DigitalOceanClientNotFound:
return []
return self._zone_records[zone.name]
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
values = defaultdict(lambda: defaultdict(list))
for record in self.zone_records(zone):
_type = record['type']
if _type not in self.SUPPORTS:
self.log.warning('populate: skipping unsupported %s record',
_type)
continue
values[record['name']][record['type']].append(record)
before = len(zone.records)
for name, types in values.items():
for _type, records in types.items():
data_for = getattr(self, f'_data_for_{_type}')
record = Record.new(zone, name, data_for(_type, records),
source=self, lenient=lenient)
zone.add_record(record, lenient=lenient)
exists = zone.name in self._zone_records
self.log.info('populate: found %s records, exists=%s',
len(zone.records) - before, exists)
return exists
def _params_for_multiple(self, record):
for value in record.values:
yield {
'data': value,
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
_params_for_A = _params_for_multiple
_params_for_AAAA = _params_for_multiple
_params_for_NS = _params_for_multiple
def _params_for_CAA(self, record):
for value in record.values:
yield {
'data': value.value,
'flags': value.flags,
'name': record.name,
'tag': value.tag,
'ttl': record.ttl,
'type': record._type
}
def _params_for_single(self, record):
yield {
'data': record.value,
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
_params_for_CNAME = _params_for_single
def _params_for_MX(self, record):
for value in record.values:
yield {
'data': value.exchange,
'name': record.name,
'priority': value.preference,
'ttl': record.ttl,
'type': record._type
}
def _params_for_SRV(self, record):
for value in record.values:
yield {
'data': value.target,
'name': record.name,
'port': value.port,
'priority': value.priority,
'ttl': record.ttl,
'type': record._type,
'weight': value.weight
}
def _params_for_TXT(self, record):
# DigitalOcean doesn't want things escaped in values so we
# have to strip them here and add them when going the other way
for value in record.values:
yield {
'data': value.replace('\\;', ';'),
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
def _apply_Create(self, change):
new = change.new
params_for = getattr(self, f'_params_for_{new._type}')
for params in params_for(new):
self._client.record_create(new.zone.name[:-1], params)
def _apply_Update(self, change):
self._apply_Delete(change)
self._apply_Create(change)
def _apply_Delete(self, change):
existing = change.existing
zone = existing.zone
for record in self.zone_records(zone):
if existing.name == record['name'] and \
existing._type == record['type']:
self._client.record_delete(zone.name[:-1], record['id'])
def _apply(self, plan):
desired = plan.desired
changes = plan.changes
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes))
domain_name = desired.name[:-1]
try:
self._client.domain(domain_name)
except DigitalOceanClientNotFound:
self.log.debug('_apply: no matching zone, creating domain')
self._client.domain_create(domain_name)
for change in changes:
class_name = change.__class__.__name__
getattr(self, f'_apply_{class_name}')(change)
# Clear out the cache if any
self._zone_records.pop(desired.name, None)
from logging import getLogger
logger = getLogger('DigitalOcean')
try:
logger.warn('octodns_digitalocean shimmed. Update your provider class to '
'octodns_digitalocean.DigitalOceanProvider. '
'Shim will be removed in 1.0')
from octodns_digitalocean import DigitalOceanProvider
DigitalOceanProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('DigitalOceanProvider has been moved into a seperate '
'module, octodns_digitalocean is now required. Provider '
'class should be updated to '
'octodns_digitalocean.DigitalOceanProvider')
raise

+ 15
- 414
octodns/provider/dnsmadeeasy.py View File

@ -5,417 +5,18 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from collections import defaultdict
from requests import Session
from time import strftime, gmtime, sleep
import hashlib
import hmac
import logging
from ..record import Record
from . import ProviderException
from .base import BaseProvider
class DnsMadeEasyClientException(ProviderException):
pass
class DnsMadeEasyClientBadRequest(DnsMadeEasyClientException):
def __init__(self, resp):
errors = '\n - '.join(resp.json()['error'])
super(DnsMadeEasyClientBadRequest, self).__init__(f'\n - {errors}')
class DnsMadeEasyClientUnauthorized(DnsMadeEasyClientException):
def __init__(self):
super(DnsMadeEasyClientUnauthorized, self).__init__('Unauthorized')
class DnsMadeEasyClientNotFound(DnsMadeEasyClientException):
def __init__(self):
super(DnsMadeEasyClientNotFound, self).__init__('Not Found')
class DnsMadeEasyClient(object):
PRODUCTION = 'https://api.dnsmadeeasy.com/V2.0/dns/managed'
SANDBOX = 'https://api.sandbox.dnsmadeeasy.com/V2.0/dns/managed'
def __init__(self, api_key, secret_key, sandbox=False,
ratelimit_delay=0.0):
self.api_key = api_key
self.secret_key = secret_key
self._base = self.SANDBOX if sandbox else self.PRODUCTION
self.ratelimit_delay = ratelimit_delay
self._sess = Session()
self._sess.headers.update({'x-dnsme-apiKey': self.api_key})
self._domains = None
def _current_time(self):
return strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
def _hmac_hash(self, now):
return hmac.new(self.secret_key.encode(), now.encode(),
hashlib.sha1).hexdigest()
def _request(self, method, path, params=None, data=None):
now = self._current_time()
hmac_hash = self._hmac_hash(now)
headers = {
'x-dnsme-hmac': hmac_hash,
'x-dnsme-requestDate': now
}
url = f'{self._base}{path}'
resp = self._sess.request(method, url, headers=headers,
params=params, json=data)
if resp.status_code == 400:
raise DnsMadeEasyClientBadRequest(resp)
if resp.status_code in [401, 403]:
raise DnsMadeEasyClientUnauthorized()
if resp.status_code == 404:
raise DnsMadeEasyClientNotFound()
resp.raise_for_status()
sleep(self.ratelimit_delay)
return resp
@property
def domains(self):
if self._domains is None:
zones = []
# has pages in resp, do we need paging?
resp = self._request('GET', '/').json()
zones += resp['data']
self._domains = {f'{z["name"]}.': z['id'] for z in zones}
return self._domains
def domain(self, name):
path = f'/id/{name}'
return self._request('GET', path).json()
def domain_create(self, name):
self._request('POST', '/', data={'name': name})
def records(self, zone_name):
zone_id = self.domains.get(zone_name, False)
path = f'/{zone_id}/records'
ret = []
# has pages in resp, do we need paging?
resp = self._request('GET', path).json()
ret += resp['data']
for record in ret:
# change ANAME records to ALIAS
if record['type'] == 'ANAME':
record['type'] = 'ALIAS'
# change relative values to absolute
value = record['value']
if record['type'] in ['ALIAS', 'CNAME', 'MX', 'NS', 'SRV']:
if value == '':
record['value'] = zone_name
elif not value.endswith('.'):
record['value'] = f'{value}.{zone_name}'
return ret
def record_create(self, zone_name, params):
zone_id = self.domains.get(zone_name, False)
path = f'/{zone_id}/records'
# change ALIAS records to ANAME
if params['type'] == 'ALIAS':
params['type'] = 'ANAME'
self._request('POST', path, data=params)
def record_delete(self, zone_name, record_id):
zone_id = self.domains.get(zone_name, False)
path = f'/{zone_id}/records/{record_id}'
self._request('DELETE', path)
class DnsMadeEasyProvider(BaseProvider):
'''
DNSMadeEasy DNS provider using v2.0 API
dnsmadeeasy:
class: octodns.provider.dnsmadeeasy.DnsMadeEasyProvider
# Your DnsMadeEasy api key (required)
api_key: env/DNSMADEEASY_API_KEY
# Your DnsMadeEasy secret key (required)
secret_key: env/DNSMADEEASY_SECRET_KEY
# Whether or not to use Sandbox environment
# (optional, default is false)
sandbox: true
'''
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX',
'NS', 'PTR', 'SPF', 'SRV', 'TXT'))
def __init__(self, id, api_key, secret_key, sandbox=False,
ratelimit_delay=0.0, *args, **kwargs):
self.log = logging.getLogger(f'DnsMadeEasyProvider[{id}]')
self.log.debug('__init__: id=%s, api_key=***, secret_key=***, '
'sandbox=%s', id, sandbox)
super(DnsMadeEasyProvider, self).__init__(id, *args, **kwargs)
self._client = DnsMadeEasyClient(api_key, secret_key, sandbox,
ratelimit_delay)
self._zone_records = {}
def _data_for_multiple(self, _type, records):
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': [r['value'] for r in records]
}
_data_for_A = _data_for_multiple
_data_for_AAAA = _data_for_multiple
_data_for_NS = _data_for_multiple
def _data_for_CAA(self, _type, records):
values = []
for record in records:
values.append({
'flags': record['issuerCritical'],
'tag': record['caaType'],
'value': record['value'][1:-1]
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def _data_for_TXT(self, _type, records):
values = [value['value'].replace(';', '\\;') for value in records]
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
_data_for_SPF = _data_for_TXT
def _data_for_MX(self, _type, records):
values = []
for record in records:
values.append({
'preference': record['mxLevel'],
'exchange': record['value']
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def _data_for_single(self, _type, records):
record = records[0]
return {
'ttl': record['ttl'],
'type': _type,
'value': record['value']
}
_data_for_CNAME = _data_for_single
_data_for_PTR = _data_for_single
_data_for_ALIAS = _data_for_single
def _data_for_SRV(self, _type, records):
values = []
for record in records:
values.append({
'port': record['port'],
'priority': record['priority'],
'target': record['value'],
'weight': record['weight']
})
return {
'type': _type,
'ttl': records[0]['ttl'],
'values': values
}
def zone_records(self, zone):
if zone.name not in self._zone_records:
try:
self._zone_records[zone.name] = \
self._client.records(zone.name)
except DnsMadeEasyClientNotFound:
return []
return self._zone_records[zone.name]
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
values = defaultdict(lambda: defaultdict(list))
for record in self.zone_records(zone):
_type = record['type']
if _type not in self.SUPPORTS:
self.log.warning('populate: skipping unsupported %s record',
_type)
continue
values[record['name']][record['type']].append(record)
before = len(zone.records)
for name, types in values.items():
for _type, records in types.items():
data_for = getattr(self, f'_data_for_{_type}')
record = Record.new(zone, name, data_for(_type, records),
source=self, lenient=lenient)
zone.add_record(record, lenient=lenient)
exists = zone.name in self._zone_records
self.log.info('populate: found %s records, exists=%s',
len(zone.records) - before, exists)
return exists
def supports(self, record):
# DNS Made Easy does not support empty/NULL SRV records
#
# Attempting to sync such a record would generate the following error
#
# octodns.provider.dnsmadeeasy.DnsMadeEasyClientBadRequest:
# - Record value may not be a standalone dot.
#
# Skip the record and continue
if record._type == "SRV":
if 'value' in record.data:
targets = (record.data['value']['target'],)
else:
targets = [value['target'] for value in record.data['values']]
if "." in targets:
self.log.warning(
'supports: unsupported %s record with target (%s)',
record._type, targets
)
return False
return super(DnsMadeEasyProvider, self).supports(record)
def _params_for_multiple(self, record):
for value in record.values:
yield {
'value': value,
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
_params_for_A = _params_for_multiple
_params_for_AAAA = _params_for_multiple
# An A record with this name must exist in this domain for
# this NS record to be valid. Need to handle checking if
# there is an A record before creating NS
_params_for_NS = _params_for_multiple
def _params_for_single(self, record):
yield {
'value': record.value,
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
_params_for_CNAME = _params_for_single
_params_for_PTR = _params_for_single
_params_for_ALIAS = _params_for_single
def _params_for_MX(self, record):
for value in record.values:
yield {
'value': value.exchange,
'name': record.name,
'mxLevel': value.preference,
'ttl': record.ttl,
'type': record._type
}
def _params_for_SRV(self, record):
for value in record.values:
yield {
'value': value.target,
'name': record.name,
'port': value.port,
'priority': value.priority,
'ttl': record.ttl,
'type': record._type,
'weight': value.weight
}
def _params_for_TXT(self, record):
# DNSMadeEasy does not want values escaped
for value in record.chunked_values:
yield {
'value': value.replace('\\;', ';'),
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
_params_for_SPF = _params_for_TXT
def _params_for_CAA(self, record):
for value in record.values:
yield {
'value': value.value,
'issuerCritical': value.flags,
'name': record.name,
'caaType': value.tag,
'ttl': record.ttl,
'type': record._type
}
def _apply_Create(self, change):
new = change.new
params_for = getattr(self, f'_params_for_{new._type}')
for params in params_for(new):
self._client.record_create(new.zone.name, params)
def _apply_Update(self, change):
self._apply_Delete(change)
self._apply_Create(change)
def _apply_Delete(self, change):
existing = change.existing
zone = existing.zone
for record in self.zone_records(zone):
if existing.name == record['name'] and \
existing._type == record['type']:
self._client.record_delete(zone.name, record['id'])
def _apply(self, plan):
desired = plan.desired
changes = plan.changes
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes))
domain_name = desired.name[:-1]
try:
self._client.domain(domain_name)
except DnsMadeEasyClientNotFound:
self.log.debug('_apply: no matching zone, creating domain')
self._client.domain_create(domain_name)
for change in changes:
class_name = change.__class__.__name__
getattr(self, f'_apply_{class_name}')(change)
# Clear out the cache if any
self._zone_records.pop(desired.name, None)
from logging import getLogger
logger = getLogger('DnsMadeEasy')
try:
logger.warn('octodns_dnsmadeeasy shimmed. Update your provider class to '
'octodns_dnsmadeeasy.DnsMadeEasyProvider. '
'Shim will be removed in 1.0')
from octodns_dnsmadeeasy import DnsMadeEasyProvider
DnsMadeEasyProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('DnsMadeEasyProvider has been moved into a seperate '
'module, octodns_dnsmadeeasy is now required. Provider '
'class should be updated to '
'octodns_dnsmadeeasy.DnsMadeEasyProvider')
raise

+ 12
- 1394
octodns/provider/dyn.py
File diff suppressed because it is too large
View File


+ 17
- 434
octodns/provider/easydns.py View File

@ -5,437 +5,20 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from collections import defaultdict
from requests import Session
from time import sleep
import logging
import base64
from ..record import Record
from . import ProviderException
from .base import BaseProvider
class EasyDNSClientException(ProviderException):
pass
class EasyDNSClientBadRequest(EasyDNSClientException):
def __init__(self):
super(EasyDNSClientBadRequest, self).__init__('Bad request')
class EasyDNSClientNotFound(EasyDNSClientException):
def __init__(self):
super(EasyDNSClientNotFound, self).__init__('Not Found')
class EasyDNSClientUnauthorized(EasyDNSClientException):
def __init__(self):
super(EasyDNSClientUnauthorized, self).__init__('Unauthorized')
class EasyDNSClient(object):
# EasyDNS Sandbox API
SANDBOX = 'https://sandbox.rest.easydns.net'
# EasyDNS Live API
LIVE = 'https://rest.easydns.net'
# Default Currency CAD
default_currency = 'CAD'
# Domain Portfolio
domain_portfolio = 'myport'
def __init__(self, token, api_key, currency, portfolio, sandbox,
domain_create_sleep):
self.log = logging.getLogger(f'EasyDNSProvider[{id}]')
self.default_currency = currency
self.domain_portfolio = portfolio
self.domain_create_sleep = domain_create_sleep
auth_key = f'{token}:{api_key}'
auth_key = base64.b64encode(auth_key.encode("utf-8"))
auth_key = auth_key.decode('utf-8')
self.base_path = self.SANDBOX if sandbox else self.LIVE
sess = Session()
sess.headers.update({'Authorization': f'Basic {auth_key}'})
sess.headers.update({'accept': 'application/json'})
self._sess = sess
def _request(self, method, path, params=None, data=None):
url = f'{self.base_path}{path}'
resp = self._sess.request(method, url, params=params, json=data)
if resp.status_code == 400:
self.log.debug('Response code 400, path=%s', path)
if method == 'GET' and path[:8] == '/domain/':
raise EasyDNSClientNotFound()
raise EasyDNSClientBadRequest()
if resp.status_code == 401:
raise EasyDNSClientUnauthorized()
if resp.status_code == 403 or resp.status_code == 404:
raise EasyDNSClientNotFound()
resp.raise_for_status()
return resp
def domain(self, name):
path = f'/domain/{name}'
return self._request('GET', path).json()
def domain_create(self, name):
# EasyDNS allows for new domains to be created for the purpose of DNS
# only, or with domain registration. This function creates a DNS only
# record expectig the domain to be registered already
path = f'/domains/add/{name}'
domain_data = {'service': 'dns',
'term': 1,
'dns_only': 1,
'portfolio': self.domain_portfolio,
'currency': self.default_currency}
self._request('PUT', path, data=domain_data).json()
# EasyDNS creates default records for MX, A and CNAME for new domains,
# we need to delete those default record so we can sync with the source
# records, first we'll sleep for a second before gathering new records
# We also create default NS records, but they won't be deleted
sleep(self.domain_create_sleep)
records = self.records(name, True)
for record in records:
if record['host'] in ('', 'www') \
and record['type'] in ('A', 'MX', 'CNAME'):
self.record_delete(name, record['id'])
def records(self, zone_name, raw=False):
if raw:
path = f'/zones/records/all/{zone_name}'
else:
path = f'/zones/records/parsed/{zone_name}'
ret = []
resp = self._request('GET', path).json()
ret += resp['data']
for record in ret:
# change any apex record to empty string
if record['host'] == '@':
record['host'] = ''
# change any apex value to zone name
if record['rdata'] == '@':
record['rdata'] = f'{zone_name}.'
return ret
def record_create(self, zone_name, params):
path = f'/zones/records/add/{zone_name}/{params["type"]}'
# change empty name string to @, EasyDNS uses @ for apex record names
params['host'] = params['name']
if params['host'] == '':
params['host'] = '@'
self._request('PUT', path, data=params)
def record_delete(self, zone_name, record_id):
path = f'/zones/records/{zone_name}/{record_id}'
self._request('DELETE', path)
class EasyDNSProvider(BaseProvider):
'''
EasyDNS provider using API v3
easydns:
class: octodns.provider.easydns.EasyDNSProvider
# Your EasyDNS API token (required)
token: foo
# Your EasyDNS API Key (required)
api_key: bar
# Use SandBox or Live environment, optional, defaults to live
sandbox: False
# Currency to use for creating domains, default CAD
default_currency: CAD
# Domain Portfolio under which to create domains
portfolio: myport
'''
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'TXT',
'SRV', 'NAPTR'))
def __init__(self, id, token, api_key, currency='CAD', portfolio='myport',
sandbox=False, domain_create_sleep=1, *args, **kwargs):
self.log = logging.getLogger(f'EasyDNSProvider[{id}]')
self.log.debug('__init__: id=%s, token=***', id)
super(EasyDNSProvider, self).__init__(id, *args, **kwargs)
self._client = EasyDNSClient(token, api_key, currency, portfolio,
sandbox, domain_create_sleep)
self._zone_records = {}
def _data_for_multiple(self, _type, records):
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': [r['rdata'] for r in records]
}
_data_for_A = _data_for_multiple
_data_for_AAAA = _data_for_multiple
def _data_for_CAA(self, _type, records):
values = []
for record in records:
try:
flags, tag, value = record['rdata'].split(' ', 2)
except ValueError:
continue
values.append({
'flags': flags,
'tag': tag,
'value': value,
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def _data_for_NAPTR(self, _type, records):
values = []
for record in records:
try:
order, preference, flags, service, regexp, replacement = \
record['rdata'].split(' ', 5)
except ValueError:
continue
values.append({
'flags': flags[1:-1],
'order': order,
'preference': preference,
'regexp': regexp[1:-1],
'replacement': replacement,
'service': service[1:-1],
})
return {
'type': _type,
'ttl': records[0]['ttl'],
'values': values
}
def _data_for_CNAME(self, _type, records):
record = records[0]
return {
'ttl': record['ttl'],
'type': _type,
'value': str(record['rdata'])
}
def _data_for_MX(self, _type, records):
values = []
for record in records:
values.append({
'preference': record['prio'],
'exchange': str(record['rdata'])
})
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def _data_for_NS(self, _type, records):
values = []
for record in records:
data = str(record['rdata'])
values.append(data)
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values,
}
def _data_for_SRV(self, _type, records):
values = []
record = records[0]
for record in records:
try:
priority, weight, port, target = record['rdata'].split(' ', 3)
except ValueError:
rdata = record['rdata'].split(' ', 3)
priority = 0
weight = 0
port = 0
target = ''
if len(rdata) != 0 and rdata[0] != '':
priority = rdata[0]
if len(rdata) >= 2:
weight = rdata[1]
if len(rdata) >= 3:
port = rdata[2]
values.append({
'port': int(port),
'priority': int(priority),
'target': target,
'weight': int(weight)
})
return {
'type': _type,
'ttl': records[0]['ttl'],
'values': values
}
def _data_for_TXT(self, _type, records):
values = ['"' + value['rdata'].replace(';', '\\;') +
'"' for value in records]
return {
'ttl': records[0]['ttl'],
'type': _type,
'values': values
}
def zone_records(self, zone):
if zone.name not in self._zone_records:
try:
self._zone_records[zone.name] = \
self._client.records(zone.name[:-1])
except EasyDNSClientNotFound:
return []
return self._zone_records[zone.name]
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
values = defaultdict(lambda: defaultdict(list))
for record in self.zone_records(zone):
_type = record['type']
if _type not in self.SUPPORTS:
self.log.warning('populate: skipping unsupported %s record',
_type)
continue
values[record['host']][record['type']].append(record)
before = len(zone.records)
for name, types in values.items():
for _type, records in types.items():
data_for = getattr(self, f'_data_for_{_type}')
record = Record.new(zone, name, data_for(_type, records),
source=self, lenient=lenient)
zone.add_record(record, lenient=lenient)
exists = zone.name in self._zone_records
self.log.info('populate: found %s records, exists=%s',
len(zone.records) - before, exists)
return exists
def _params_for_multiple(self, record):
for value in record.values:
yield {
'rdata': value,
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
_params_for_A = _params_for_multiple
_params_for_AAAA = _params_for_multiple
_params_for_NS = _params_for_multiple
def _params_for_CAA(self, record):
for value in record.values:
yield {
'rdata': f"{value.flags} {value.tag} {value.value}",
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
def _params_for_NAPTR(self, record):
for value in record.values:
content = f'{value.order} {value.preference} "{value.flags}" ' \
f'"{value.service}" "{value.regexp}" {value.replacement}'
yield {
'rdata': content,
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
def _params_for_single(self, record):
yield {
'rdata': record.value,
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
_params_for_CNAME = _params_for_single
def _params_for_MX(self, record):
for value in record.values:
yield {
'rdata': value.exchange,
'name': record.name,
'prio': value.preference,
'ttl': record.ttl,
'type': record._type
}
def _params_for_SRV(self, record):
for value in record.values:
yield {
'rdata': f"{value.priority} {value.port} {value.weight} "
f"{value.target}",
'name': record.name,
'ttl': record.ttl,
'type': record._type,
}
def _params_for_TXT(self, record):
for value in record.values:
yield {
'rdata': '"' + value.replace('\\;', ';') + '"',
'name': record.name,
'ttl': record.ttl,
'type': record._type
}
def _apply_Create(self, change):
new = change.new
params_for = getattr(self, f'_params_for_{new._type}')
for params in params_for(new):
self._client.record_create(new.zone.name[:-1], params)
def _apply_Update(self, change):
self._apply_Delete(change)
self._apply_Create(change)
def _apply_Delete(self, change):
existing = change.existing
zone = existing.zone
for record in self.zone_records(zone):
self.log.debug('apply_Delete: zone=%s, type=%s, host=%s', zone,
record['type'], record['host'])
if existing.name == record['host'] and \
existing._type == record['type']:
self._client.record_delete(zone.name[:-1], record['id'])
def _apply(self, plan):
desired = plan.desired
changes = plan.changes
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes))
domain_name = desired.name[:-1]
try:
self._client.domain(domain_name)
except EasyDNSClientNotFound:
self.log.debug('_apply: no matching zone, creating domain')
self._client.domain_create(domain_name)
for change in changes:
class_name = change.__class__.__name__
getattr(self, f'_apply_{class_name}')(change)
# Clear out the cache if any
self._zone_records.pop(desired.name, None)
from logging import getLogger
logger = getLogger('EasyDns')
try:
logger.warn('octodns_easydns shimmed. Update your provider class to '
'octodns_easydns.EasyDnsProvider. '
'Shim will be removed in 1.0')
from octodns_easydns import EasyDnsProvider, EasyDNSProvider
EasyDnsProvider # pragma: no cover
EasyDNSProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('EasyDNSProvider has been moved into a seperate module, '
'octodns_easydns is now required. Provider class should '
'be updated to octodns_easydns.EasyDnsProvider. See '
'https://github.com/octodns/octodns/README.md#updating-'
'to-use-extracted-providers for more information.')
raise

+ 14
- 510
octodns/provider/edgedns.py View File

@ -5,515 +5,19 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from requests import Session
from akamai.edgegrid import EdgeGridAuth
from collections import defaultdict
from urllib.parse import urljoin
from logging import getLogger
from ..record import Record
from . import ProviderException
from .base import BaseProvider
class AkamaiClientNotFound(ProviderException):
def __init__(self, resp):
message = "404: Resource not found"
super(AkamaiClientNotFound, self).__init__(message)
class AkamaiClient(object):
'''
Client for making calls to Akamai Fast DNS API using Python Requests
Edge DNS Zone Management API V2, found here:
https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html
Info on Python Requests library:
https://2.python-requests.org/en/master/
'''
def __init__(self, client_secret, host, access_token, client_token):
self.base = "https://" + host + "/config-dns/v2/"
sess = Session()
sess.auth = EdgeGridAuth(
client_token=client_token,
client_secret=client_secret,
access_token=access_token
)
self._sess = sess
def _request(self, method, path, params=None, data=None, v1=False):
url = urljoin(self.base, path)
resp = self._sess.request(method, url, params=params, json=data)
if resp.status_code == 404:
raise AkamaiClientNotFound(resp)
resp.raise_for_status()
return resp
def record_create(self, zone, name, record_type, content):
path = f'zones/{zone}/names/{name}/types/{record_type}'
result = self._request('POST', path, data=content)
return result
def record_delete(self, zone, name, record_type):
path = f'zones/{zone}/names/{name}/types/{record_type}'
result = self._request('DELETE', path)
return result
def record_replace(self, zone, name, record_type, content):
path = f'zones/{zone}/names/{name}/types/{record_type}'
result = self._request('PUT', path, data=content)
return result
def zone_get(self, zone):
path = f'zones/{zone}'
result = self._request('GET', path)
return result
def zone_create(self, contractId, params, gid=None):
path = f'zones?contractId={contractId}'
if gid is not None:
path += f'&gid={gid}'
result = self._request('POST', path, data=params)
return result
def zone_recordset_get(self, zone, page=None, pageSize=None, search=None,
showAll="true", sortBy="name", types=None):
params = {
'page': page,
'pageSize': pageSize,
'search': search,
'showAll': showAll,
'sortBy': sortBy,
'types': types
}
path = f'zones/{zone}/recordsets'
result = self._request('GET', path, params=params)
return result
class AkamaiProvider(BaseProvider):
'''
Akamai Edge DNS Provider
edgedns.py:
Example config file with variables:
"
---
providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: ./config (example path to directory of zone files)
edgedns:
class: octodns.provider.edgedns.AkamaiProvider
client_secret: env/AKAMAI_CLIENT_SECRET
host: env/AKAMAI_HOST
access_token: env/AKAMAI_ACCESS_TOKEN
client_token: env/AKAMAI_CLIENT_TOKEN
contract_id: env/AKAMAI_CONTRACT_ID (optional)
zones:
example.com.:
sources:
- config
targets:
- edgedns
"
The first four variables above can be hidden in environment variables
and octoDNS will automatically search for them in the shell. It is
possible to also hard-code into the config file: eg, contract_id.
The first four values can be found by generating credentials:
https://control.akamai.com/
Configure > Organization > Manage APIs > New API Client for me
Select appropriate group, and fill relevant fields.
For API Service Name, select DNS-Zone Record Management
and then set appropriate Access level (Read-Write to make changes).
Then select the "New Credential" button to generate values for above
The contract_id paramater is optional, and only required for creating
a new zone. If the zone being managed already exists in Akamai for the
user in question, then this paramater is not needed.
'''
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SPF',
'SRV', 'SSHFP', 'TXT'))
def __init__(self, id, client_secret, host, access_token, client_token,
contract_id=None, gid=None, *args, **kwargs):
self.log = getLogger(f'AkamaiProvider[{id}]')
self.log.debug('__init__: id=%s, ')
super(AkamaiProvider, self).__init__(id, *args, **kwargs)
self._dns_client = AkamaiClient(client_secret, host, access_token,
client_token)
self._zone_records = {}
self._contractId = contract_id
self._gid = gid
def zone_records(self, zone):
""" returns records for a zone, looks for it if not present, or
returns empty [] if can't find a match
"""
if zone.name not in self._zone_records:
try:
name = zone.name[:-1]
response = self._dns_client.zone_recordset_get(name)
self._zone_records[zone.name] = response.json()["recordsets"]
except (AkamaiClientNotFound, KeyError):
return []
return self._zone_records[zone.name]
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s', zone.name)
values = defaultdict(lambda: defaultdict(list))
for record in self.zone_records(zone):
_type = record.get('type')
# Akamai sends down prefix.zonename., while octodns expects prefix
_name = record.get('name').split("." + zone.name[:-1], 1)[0]
if _name == zone.name[:-1]:
_name = '' # root / @
if _type not in self.SUPPORTS:
continue
values[_name][_type].append(record)
before = len(zone.records)
for name, types in values.items():
for _type, records in types.items():
data_for = getattr(self, f'_data_for_{_type}')
record = Record.new(zone, name, data_for(_type, records[0]),
source=self, lenient=lenient)
zone.add_record(record, lenient=lenient)
exists = zone.name in self._zone_records
found = len(zone.records) - before
self.log.info('populate: found %s records, exists=%s', found, exists)
return exists
def _apply(self, plan):
desired = plan.desired
changes = plan.changes
self.log.debug('apply: zone=%s, chnges=%d', desired.name, len(changes))
zone_name = desired.name[:-1]
try:
self._dns_client.zone_get(zone_name)
except AkamaiClientNotFound:
self.log.info("zone not found, creating zone")
params = self._build_zone_config(zone_name)
self._dns_client.zone_create(self._contractId, params, self._gid)
for change in changes:
class_name = change.__class__.__name__
getattr(self, f'_apply_{class_name}')(change)
# Clear out the cache if any
self._zone_records.pop(desired.name, None)
def _apply_Create(self, change):
new = change.new
record_type = new._type
params_for = getattr(self, f'_params_for_{record_type}')
values = self._get_values(new.data)
rdata = params_for(values)
zone = new.zone.name[:-1]
name = self._set_full_name(new.name, zone)
content = {
"name": name,
"type": record_type,
"ttl": new.ttl,
"rdata": rdata
}
self._dns_client.record_create(zone, name, record_type, content)
return
def _apply_Delete(self, change):
zone = change.existing.zone.name[:-1]
name = self._set_full_name(change.existing.name, zone)
record_type = change.existing._type
self._dns_client.record_delete(zone, name, record_type)
return
def _apply_Update(self, change):
new = change.new
record_type = new._type
params_for = getattr(self, f'_params_for_{record_type}')
values = self._get_values(new.data)
rdata = params_for(values)
zone = new.zone.name[:-1]
name = self._set_full_name(new.name, zone)
content = {
"name": name,
"type": record_type,
"ttl": new.ttl,
"rdata": rdata
}
self._dns_client.record_replace(zone, name, record_type, content)
return
def _data_for_multiple(self, _type, records):
return {
'ttl': records['ttl'],
'type': _type,
'values': [r for r in records['rdata']]
}
_data_for_A = _data_for_multiple
_data_for_AAAA = _data_for_multiple
_data_for_NS = _data_for_multiple
_data_for_SPF = _data_for_multiple
def _data_for_CNAME(self, _type, records):
value = records['rdata'][0]
if (value[-1] != '.'):
value = f'{value}.'
return {
'ttl': records['ttl'],
'type': _type,
'value': value
}
def _data_for_MX(self, _type, records):
values = []
for r in records['rdata']:
preference, exchange = r.split(" ", 1)
values.append({
'preference': preference,
'exchange': exchange
})
return {
'ttl': records['ttl'],
'type': _type,
'values': values
}
def _data_for_NAPTR(self, _type, records):
values = []
for r in records['rdata']:
order, preference, flags, service, regexp, repl = r.split(' ', 5)
values.append({
'flags': flags[1:-1],
'order': order,
'preference': preference,
'regexp': regexp[1:-1],
'replacement': repl,
'service': service[1:-1]
})
return {
'type': _type,
'ttl': records['ttl'],
'values': values
}
def _data_for_PTR(self, _type, records):
return {
'ttl': records['ttl'],
'type': _type,
'value': records['rdata'][0]
}
def _data_for_SRV(self, _type, records):
values = []
for r in records['rdata']:
priority, weight, port, target = r.split(' ', 3)
values.append({
'port': port,
'priority': priority,
'target': target,
'weight': weight
})
return {
'type': _type,
'ttl': records['ttl'],
'values': values
}
def _data_for_SSHFP(self, _type, records):
values = []
for r in records['rdata']:
algorithm, fp_type, fingerprint = r.split(' ', 2)
values.append({
'algorithm': algorithm,
'fingerprint': fingerprint.lower(),
'fingerprint_type': fp_type
})
return {
'type': _type,
'ttl': records['ttl'],
'values': values
}
def _data_for_TXT(self, _type, records):
values = []
for r in records['rdata']:
r = r[1:-1]
values.append(r.replace(';', '\\;'))
return {
'ttl': records['ttl'],
'type': _type,
'values': values
}
def _params_for_multiple(self, values):
return [r for r in values]
def _params_for_single(self, values):
return values
_params_for_A = _params_for_multiple
_params_for_AAAA = _params_for_multiple
_params_for_NS = _params_for_multiple
_params_for_CNAME = _params_for_single
_params_for_PTR = _params_for_single
def _params_for_MX(self, values):
rdata = []
for r in values:
preference = r['preference']
exchange = r['exchange']
rdata.append(f'{preference} {exchange}')
return rdata
def _params_for_NAPTR(self, values):
rdata = []
for r in values:
ordr = r['order']
prf = r['preference']
flg = "\"" + r['flags'] + "\""
srvc = "\"" + r['service'] + "\""
rgx = "\"" + r['regexp'] + "\""
rpl = r['replacement']
rdata.append(f'{ordr} {prf} {flg} {srvc} {rgx} {rpl}')
return rdata
def _params_for_SPF(self, values):
rdata = []
for r in values:
txt = "\"" + r.replace('\\;', ';') + "\""
rdata.append(txt)
return rdata
def _params_for_SRV(self, values):
rdata = []
for r in values:
priority = r['priority']
weight = r['weight']
port = r['port']
target = r['target']
rdata.append(f'{priority} {weight} {port} {target}')
return rdata
def _params_for_SSHFP(self, values):
rdata = []
for r in values:
algorithm = r['algorithm']
fp_type = r['fingerprint_type']
fp = r['fingerprint']
rdata.append(f'{algorithm} {fp_type} {fp}')
return rdata
def _params_for_TXT(self, values):
rdata = []
for r in values:
txt = "\"" + r.replace('\\;', ';') + "\""
rdata.append(txt)
return rdata
def _build_zone_config(self, zone, _type="primary", comment=None,
masters=[]):
if self._contractId is None:
raise NameError("contractId not specified to create zone")
return {
"zone": zone,
"type": _type,
"comment": comment,
"masters": masters
}
def _get_values(self, data):
try:
vals = data['values']
except KeyError:
vals = [data['value']]
return vals
def _set_full_name(self, name, zone):
name = name + '.' + zone
# octodns's name for root is ''
if (name[0] == '.'):
name = name[1:]
return name
logger = getLogger('Akamai')
try:
logger.warn('octodns_edgedns shimmed. Update your provider class to '
'octodns_edgedns.AkamaiProvider. '
'Shim will be removed in 1.0')
from octodns_edgedns import AkamaiProvider
AkamaiProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('AkamaiProvider has been moved into a seperate module, '
'octodns_edgedns is now required. Provider class should '
'be updated to octodns_edgedns.AkamaiProvider. See '
'https://github.com/octodns/octodns/README.md#updating-'
'to-use-extracted-providers for more information.')
raise

+ 16
- 108
octodns/provider/etc_hosts.py View File

@ -5,111 +5,19 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from os import makedirs, path
from os.path import isdir
import logging
from .base import BaseProvider
class EtcHostsProvider(BaseProvider):
'''
Provider that creates a "best effort" static/emergency content that can be
used in /etc/hosts to resolve things. A, AAAA records are supported and
ALIAS and CNAME records will be included when they can be mapped within the
zone.
config:
class: octodns.provider.etc_hosts.EtcHostsProvider
# The output director for the hosts file <zone>.hosts
directory: ./hosts
'''
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME'))
def __init__(self, id, directory, *args, **kwargs):
self.log = logging.getLogger(f'EtcHostsProvider[{id}]')
self.log.debug('__init__: id=%s, directory=%s', id, directory)
super(EtcHostsProvider, self).__init__(id, *args, **kwargs)
self.directory = directory
def populate(self, zone, target=False, lenient=False):
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
target, lenient)
# We never act as a source, at least for now, if/when we do we still
# need to noop `if target`
return False
def _apply(self, plan):
desired = plan.desired
changes = plan.changes
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes))
cnames = {}
values = {}
for record in sorted([c.new for c in changes]):
# Since we don't have existing we'll only see creates
fqdn = record.fqdn[:-1]
if record._type in ('ALIAS', 'CNAME'):
# Store cnames so we can try and look them up in a minute
cnames[fqdn] = record.value[:-1]
elif record._type == 'AAAA' and fqdn in values:
# We'll prefer A over AAAA, skipping rather than replacing an
# existing A
pass
else:
# If we're here it's and A or AAAA and we want to record it's
# value (maybe replacing if it's an A and we have a AAAA
values[fqdn] = record.values[0]
if not isdir(self.directory):
makedirs(self.directory)
filepath = path.join(self.directory, desired.name)
filename = f'{filepath}hosts'
self.log.info('_apply: filename=%s', filename)
with open(filename, 'w') as fh:
fh.write('##################################################\n')
fh.write(f'# octoDNS {self.id} {desired.name}\n')
fh.write('##################################################\n\n')
if values:
fh.write('## A & AAAA\n\n')
for fqdn, value in sorted(values.items()):
if fqdn[0] == '*':
fh.write('# ')
fh.write(f'{value}\t{fqdn}\n\n')
if cnames:
fh.write('\n## CNAME (mapped)\n\n')
for fqdn, value in sorted(cnames.items()):
# Print out a comment of the first level
fh.write(f'# {fqdn} -> {value}\n')
seen = set()
while True:
seen.add(value)
try:
value = values[value]
# If we're here we've found the target, print it
# and break the loop
fh.write(f'{value}\t{fqdn}\n')
break
except KeyError:
# Try and step down one level
orig = value
value = cnames.get(value, None)
# Print out this step
if value:
if value in seen:
# We'd loop here, break it
fh.write(f'# {orig} -> {value} **loop**\n')
break
else:
fh.write(f'# {orig} -> {value}\n')
else:
# Don't have anywhere else to go
fh.write(f'# {orig} -> **unknown**\n')
break
fh.write('\n')
from logging import getLogger
logger = getLogger('EtcHosts')
try:
logger.warn('octodns_etchosts shimmed. Update your provider class to '
'octodns_etchosts.EtcHostsProvider. '
'Shim will be removed in 1.0')
from octodns_etchosts import EtcHostsProvider
EtcHostsProvider # pragma: no cover
except ModuleNotFoundError:
logger.exception('EtcHostsProvider has been moved into a seperate module, '
'octodns_etchosts is now required. Provider class should '
'be updated to octodns_etchosts.EtcHostsProvider. See '
'https://github.com/octodns/octodns/README.md#updating-'
'to-use-extracted-providers for more information.')
raise

+ 6
- 7
octodns/provider/fastdns.py View File

@ -5,12 +5,11 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from .edgedns import AkamaiProvider
from logging import getLogger
# Quell unused warning
AkamaiProvider
log = getLogger('octodns.provider.fastdns.AkamaiProvider')
log.warn('DEPRECATION NOTICE: AkamaiProvider has been moved to '
'octodns.provider.fastdns.AkamaiProvider')
logger = getLogger('Akamai')
logger.warn('AkamaiProvider has been moved into a seperate module, '
'octodns_edgedns is now required. Provider class should '
'be updated to octodns_edgedns.AkamaiProvider. See '
'https://github.com/octodns/octodns/README.md#updating-'
'to-use-extracted-providers for more information.')

+ 0
- 2
requirements.txt View File

@ -1,8 +1,6 @@
PyYaml==5.4
dnspython==1.16.0
docutils==0.16
dyn==1.8.1
edgegrid-python==1.1.1
fqdn==1.5.0
google-cloud-core==1.4.1
google-cloud-dns==0.32.0


+ 0
- 188
tests/fixtures/cloudflare-dns_records-page-1.json View File

@ -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": []
}

+ 0
- 238
tests/fixtures/cloudflare-dns_records-page-2.json View File

@ -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": []
}

+ 0
- 128
tests/fixtures/cloudflare-dns_records-page-3.json View File

@ -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": []
}

+ 0
- 103
tests/fixtures/cloudflare-pagerules.json View File

@ -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": []
}

+ 0
- 140
tests/fixtures/cloudflare-zones-page-1.json View File

@ -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": []
}

+ 0
- 140
tests/fixtures/cloudflare-zones-page-2.json View File

@ -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": []
}

+ 0
- 28
tests/fixtures/constellix-domains.json View File

@ -1,28 +0,0 @@
[{
"id": 123123,
"name": "unit.tests",
"soa": {
"primaryNameserver": "ns11.constellix.com.",
"email": "dns.constellix.com.",
"ttl": 86400,
"serial": 2015010102,
"refresh": 43200,
"retry": 3600,
"expire": 1209600,
"negCache": 180
},
"createdTs": "2019-08-07T03:36:02Z",
"modifiedTs": "2019-08-07T03:36:02Z",
"typeId": 1,
"domainTags": [],
"folder": null,
"hasGtdRegions": false,
"hasGeoIP": false,
"nameserverGroup": 1,
"nameservers": ["ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net."],
"note": "",
"version": 0,
"status": "ACTIVE",
"tags": [],
"contactIds": []
}]

+ 0
- 34
tests/fixtures/constellix-geofilters.json View File

@ -1,34 +0,0 @@
[
{
"id": 6303,
"name": "some.other",
"filterRulesLimit": 100,
"createdTs": "2021-08-19T14:47:47Z",
"modifiedTs": "2021-08-19T14:47:47Z",
"geoipContinents": ["AS", "OC"],
"geoipCountries": ["ES", "SE", "UA"],
"regions": [
{
"continentCode": "NA",
"countryCode": "CA",
"regionCode": "NL"
}
]
},
{
"id": 5303,
"name": "unit.tests.:www.dynamic:A:one",
"filterRulesLimit": 100,
"createdTs": "2021-08-19T14:47:47Z",
"modifiedTs": "2021-08-19T14:47:47Z",
"geoipContinents": ["AS", "OC"],
"geoipCountries": ["ES", "SE", "UA"],
"regions": [
{
"continentCode": "NA",
"countryCode": "CA",
"regionCode": "NL"
}
]
}
]

+ 0
- 62
tests/fixtures/constellix-pools.json View File

@ -1,62 +0,0 @@
[
{
"id": 1808521,
"name": "unit.tests.:www.dynamic:A:two",
"type": "A",
"numReturn": 1,
"minAvailableFailover": 1,
"createdTs": "2020-09-12T00:44:35Z",
"modifiedTs": "2020-09-12T00:44:35Z",
"appliedDomains": [
{
"id": 123123,
"name": "unit.tests",
"recordOption": "pools"
}
],
"appliedTemplates": null,
"unlinkedDomains": [],
"unlinkedTemplates": null,
"itoEnabled": false,
"values": [
{
"value": "1.2.3.4",
"weight": 1
},
{
"value": "1.2.3.5",
"weight": 1
}
]
},
{
"id": 1808522,
"name": "unit.tests.:www.dynamic:A:one",
"type": "A",
"numReturn": 1,
"minAvailableFailover": 1,
"createdTs": "2020-09-12T00:44:35Z",
"modifiedTs": "2020-09-12T00:44:35Z",
"appliedDomains": [
{
"id": 123123,
"name": "unit.tests",
"recordOption": "pools"
}
],
"appliedTemplates": null,
"unlinkedDomains": [],
"unlinkedTemplates": null,
"itoEnabled": false,
"values": [
{
"value": "1.2.3.6",
"weight": 1
},
{
"value": "1.2.3.7",
"weight": 1
}
]
}
]

+ 0
- 696
tests/fixtures/constellix-records.json View File

@ -1,696 +0,0 @@
[{
"id": 1808529,
"type": "CAA",
"recordType": "caa",
"name": "",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 3600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149569216,
"value": [{
"flag": 0,
"tag": "issue",
"data": "ca.unit.tests",
"caaProviderId": 1,
"disableFlag": false
}],
"roundRobin": [{
"flag": 0,
"tag": "issue",
"data": "ca.unit.tests",
"caaProviderId": 1,
"disableFlag": false
}]
}, {
"id": 1808516,
"type": "A",
"recordType": "a",
"name": "",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149623640,
"value": ["1.2.3.4", "1.2.3.5"],
"roundRobin": [{
"value": "1.2.3.4",
"disableFlag": false
}, {
"value": "1.2.3.5",
"disableFlag": false
}],
"geolocation": null,
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"roundRobinFailover": [],
"pools": [],
"poolsDetail": []
}, {
"id": 1808527,
"type": "SRV",
"recordType": "srv",
"name": "_srv._tcp",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149714387,
"value": [{
"value": "foo-1.unit.tests.",
"priority": 10,
"weight": 20,
"port": 30,
"disableFlag": false
}, {
"value": "foo-2.unit.tests.",
"priority": 12,
"weight": 20,
"port": 30,
"disableFlag": false
}],
"roundRobin": [{
"value": "foo-1.unit.tests.",
"priority": 10,
"weight": 20,
"port": 30,
"disableFlag": false
}, {
"value": "foo-2.unit.tests.",
"priority": 12,
"weight": 20,
"port": 30,
"disableFlag": false
}]
}, {
"id": 1808527,
"type": "SRV",
"recordType": "srv",
"name": "_imap._tcp",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149714387,
"value": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}],
"roundRobin": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}]
}, {
"id": 1808527,
"type": "SRV",
"recordType": "srv",
"name": "_pop3._tcp",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149714387,
"value": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}],
"roundRobin": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}]
}, {
"id": 1808515,
"type": "AAAA",
"recordType": "aaaa",
"name": "aaaa",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149739464,
"value": ["2601:644:500:e210:62f8:1dff:feb8:947a"],
"roundRobin": [{
"value": "2601:644:500:e210:62f8:1dff:feb8:947a",
"disableFlag": false
}],
"geolocation": null,
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"pools": [],
"poolsDetail": [],
"roundRobinFailover": []
}, {
"id": 1808530,
"type": "ANAME",
"recordType": "aname",
"name": "",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 1800,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150251379,
"value": [{
"value": "aname.unit.tests.",
"disableFlag": false
}],
"roundRobin": [{
"value": "aname.unit.tests.",
"disableFlag": false
}],
"geolocation": null,
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"pools": [],
"poolsDetail": []
}, {
"id": 1808521,
"type": "CNAME",
"recordType": "cname",
"name": "cname",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565152113825,
"value": "",
"roundRobin": [{
"value": "",
"disableFlag": false
}],
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": [{
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 1,
"markedActive": false
}, {
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 2,
"markedActive": false
}]
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": [{
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 1,
"markedActive": false
}, {
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 2,
"markedActive": false
}]
},
"pools": [],
"poolsDetail": [],
"geolocation": null,
"host": ""
}, {
"id": 1808522,
"type": "CNAME",
"recordType": "cname",
"name": "included",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 3600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565152119137,
"value": "",
"roundRobin": [{
"value": "",
"disableFlag": false
}],
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": [{
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 1,
"markedActive": false
}, {
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 2,
"markedActive": false
}]
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": [{
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 1,
"markedActive": false
}, {
"id": null,
"value": "",
"disableFlag": false,
"failedFlag": false,
"status": "N/A",
"sortOrder": 2,
"markedActive": false
}]
},
"pools": [],
"poolsDetail": [],
"geolocation": null,
"host": ""
}, {
"id": 1808523,
"type": "MX",
"recordType": "mx",
"name": "mx",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149879856,
"value": [{
"value": "smtp-3.unit.tests.",
"level": 30,
"disableFlag": false
}, {
"value": "smtp-2.unit.tests.",
"level": 20,
"disableFlag": false
}, {
"value": "smtp-4.unit.tests.",
"level": 10,
"disableFlag": false
}, {
"value": "smtp-1.unit.tests.",
"level": 40,
"disableFlag": false
}],
"roundRobin": [{
"value": "smtp-3.unit.tests.",
"level": 30,
"disableFlag": false
}, {
"value": "smtp-2.unit.tests.",
"level": 20,
"disableFlag": false
}, {
"value": "smtp-4.unit.tests.",
"level": 10,
"disableFlag": false
}, {
"value": "smtp-1.unit.tests.",
"level": 40,
"disableFlag": false
}]
}, {
"id": 1808525,
"type": "PTR",
"recordType": "ptr",
"name": "ptr",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150115139,
"value": [{
"value": "foo.bar.com.",
"disableFlag": false
}],
"roundRobin": [{
"value": "foo.bar.com.",
"disableFlag": false
}]
}, {
"id": 1808526,
"type": "SPF",
"recordType": "spf",
"name": "spf",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149916132,
"value": [{
"value": "\"v=spf1 ip4:192.168.0.1/16-all\"",
"disableFlag": false
}],
"roundRobin": [{
"value": "\"v=spf1 ip4:192.168.0.1/16-all\"",
"disableFlag": false
}]
}, {
"id": 1808528,
"type": "TXT",
"recordType": "txt",
"name": "txt",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149966915,
"value": [{
"value": "\"Bah bah black sheep\"",
"disableFlag": false
}, {
"value": "\"have you any wool.\"",
"disableFlag": false
}, {
"value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"",
"disableFlag": false
}],
"roundRobin": [{
"value": "\"Bah bah black sheep\"",
"disableFlag": false
}, {
"value": "\"have you any wool.\"",
"disableFlag": false
}, {
"value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"",
"disableFlag": false
}]
}, {
"id": 1808524,
"type": "NS",
"recordType": "ns",
"name": "under",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 3600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150062850,
"value": [{
"value": "ns1.unit.tests.",
"disableFlag": false
}, {
"value": "ns2",
"disableFlag": false
}],
"roundRobin": [{
"value": "ns1.unit.tests.",
"disableFlag": false
}, {
"value": "ns2",
"disableFlag": false
}]
}, {
"id": 1808531,
"type": "HTTPRedirection",
"recordType": "httpredirection",
"name": "unsupported",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150348154,
"value": "https://redirect.unit.tests",
"roundRobin": [{
"value": "https://redirect.unit.tests"
}],
"title": "Unsupported Record",
"keywords": "unsupported",
"description": "unsupported record",
"hardlinkFlag": false,
"redirectTypeId": 1,
"url": "https://redirect.unit.tests"
}, {
"id": 1808519,
"type": "A",
"recordType": "a",
"name": "www",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150079027,
"value": ["2.2.3.6"],
"roundRobin": [{
"value": "2.2.3.6",
"disableFlag": false
}],
"geolocation": null,
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"roundRobinFailover": [],
"pools": [],
"poolsDetail": []
}, {
"id": 1808520,
"type": "A",
"recordType": "a",
"name": "www.sub",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150090588,
"value": ["2.2.3.6"],
"roundRobin": [{
"value": "2.2.3.6",
"disableFlag": false
}],
"geolocation": null,
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"roundRobinFailover": [],
"pools": [],
"poolsDetail": []
}, {
"id": 1808520,
"type": "A",
"recordType": "a",
"name": "www.dynamic",
"recordOption": "pools",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150090588,
"value": [],
"roundRobin": [],
"geolocation": {
"geoipFilter": 1
},
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"roundRobinFailover": [],
"pools": [
1808521
],
"poolsDetail": [{
"id": 1808521,
"name": "unit.tests.:www.dynamic:A:two"
}]
},
{
"id": 1808521,
"type": "A",
"recordType": "a",
"name": "www.dynamic",
"recordOption": "pools",
"noAnswer": false,
"note": "",
"ttl": 300,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565150090588,
"value": [],
"roundRobin": [],
"geolocation": {
"geoipFilter": 5303
},
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"roundRobinFailover": [],
"pools": [
1808522
],
"poolsDetail": [{
"id": 1808522,
"name": "unit.tests.:www.dynamic:A:one"
}]
}]

+ 0
- 188
tests/fixtures/digitalocean-page-1.json View File

@ -1,188 +0,0 @@
{
"domain_records": [{
"id": null,
"type": "SOA",
"name": "@",
"data": null,
"priority": null,
"port": null,
"ttl": null,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189874,
"type": "NS",
"name": "@",
"data": "ns1.digitalocean.com",
"priority": null,
"port": null,
"ttl": 3600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189875,
"type": "NS",
"name": "@",
"data": "ns2.digitalocean.com",
"priority": null,
"port": null,
"ttl": 3600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189876,
"type": "NS",
"name": "@",
"data": "ns3.digitalocean.com",
"priority": null,
"port": null,
"ttl": 3600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189877,
"type": "NS",
"name": "under",
"data": "ns1.unit.tests",
"priority": null,
"port": null,
"ttl": 3600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189878,
"type": "NS",
"name": "under",
"data": "ns2.unit.tests",
"priority": null,
"port": null,
"ttl": 3600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189879,
"type": "SRV",
"name": "_srv._tcp",
"data": "foo-1.unit.tests",
"priority": 10,
"port": 30,
"ttl": 600,
"weight": 20,
"flags": null,
"tag": null
}, {
"id": 11189880,
"type": "SRV",
"name": "_srv._tcp",
"data": "foo-2.unit.tests",
"priority": 12,
"port": 30,
"ttl": 600,
"weight": 20,
"flags": null,
"tag": null
}, {
"id": 11189881,
"type": "TXT",
"name": "txt",
"data": "Bah bah black sheep",
"priority": null,
"port": null,
"ttl": 600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189882,
"type": "TXT",
"name": "txt",
"data": "have you any wool.",
"priority": null,
"port": null,
"ttl": 600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189883,
"type": "A",
"name": "@",
"data": "1.2.3.4",
"priority": null,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189884,
"type": "A",
"name": "@",
"data": "1.2.3.5",
"priority": null,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189885,
"type": "A",
"name": "www",
"data": "2.2.3.6",
"priority": null,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189886,
"type": "MX",
"name": "mx",
"data": "smtp-4.unit.tests",
"priority": 10,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189887,
"type": "MX",
"name": "mx",
"data": "smtp-2.unit.tests",
"priority": 20,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189888,
"type": "MX",
"name": "mx",
"data": "smtp-3.unit.tests",
"priority": 30,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}],
"links": {
"pages": {
"last": "https://api.digitalocean.com/v2/domains/unit.tests/records?page=2",
"next": "https://api.digitalocean.com/v2/domains/unit.tests/records?page=2"
}
},
"meta": {
"total": 21
}
}

+ 0
- 111
tests/fixtures/digitalocean-page-2.json View File

@ -1,111 +0,0 @@
{
"domain_records": [{
"id": 11189889,
"type": "MX",
"name": "mx",
"data": "smtp-1.unit.tests",
"priority": 40,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189890,
"type": "AAAA",
"name": "aaaa",
"data": "2601:644:500:e210:62f8:1dff:feb8:947a",
"priority": null,
"port": null,
"ttl": 600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189891,
"type": "CNAME",
"name": "cname",
"data": "@",
"priority": null,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189892,
"type": "A",
"name": "www.sub",
"data": "2.2.3.6",
"priority": null,
"port": null,
"ttl": 300,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189893,
"type": "TXT",
"name": "txt",
"data": "v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs",
"priority": null,
"port": null,
"ttl": 600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189894,
"type": "CAA",
"name": "@",
"data": "ca.unit.tests",
"priority": null,
"port": null,
"ttl": 3600,
"weight": null,
"flags": 0,
"tag": "issue"
}, {
"id": 11189895,
"type": "CNAME",
"name": "included",
"data": "@",
"priority": null,
"port": null,
"ttl": 3600,
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189896,
"type": "SRV",
"name": "_imap._tcp",
"data": ".",
"priority": 0,
"port": 0,
"ttl": 600,
"weight": 0,
"flags": null,
"tag": null
}, {
"id": 11189897,
"type": "SRV",
"name": "_pop3._tcp",
"data": ".",
"priority": 0,
"port": 0,
"ttl": 600,
"weight": 0,
"flags": null,
"tag": null
}],
"links": {
"pages": {
"first": "https://api.digitalocean.com/v2/domains/unit.tests/records?page=1",
"prev": "https://api.digitalocean.com/v2/domains/unit.tests/records?page=1"
}
},
"meta": {
"total": 21
}
}

+ 0
- 16
tests/fixtures/dnsmadeeasy-domains.json View File

@ -1,16 +0,0 @@
{
"totalPages": 1,
"totalRecords": 1,
"data": [{
"created": 1511740800000,
"folderId": 1990,
"gtdEnabled": false,
"pendingActionId": 0,
"updated": 1511766661574,
"processMulti": false,
"activeThirdParties": [],
"name": "unit.tests",
"id": 123123
}],
"page": 0
}

+ 0
- 344
tests/fixtures/dnsmadeeasy-records.json View File

@ -1,344 +0,0 @@
{
"totalPages": 1,
"totalRecords": 23,
"data": [{
"failover": false,
"monitor": false,
"sourceId": 123123,
"caaType": "issue",
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"issuerCritical": 0,
"ttl": 3600,
"source": 1,
"name": "",
"value": "\"ca.unit.tests\"",
"id": 11189874,
"type": "CAA"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 300,
"source": 1,
"name": "",
"value": "1.2.3.4",
"id": 11189875,
"type": "A"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 300,
"source": 1,
"name": "",
"value": "1.2.3.5",
"id": 11189876,
"type": "A"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 600,
"weight": 20,
"source": 1,
"name": "_srv._tcp",
"value": "foo-1.unit.tests.",
"id": 11189877,
"priority": 10,
"type": "SRV",
"port": 30
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 600,
"weight": 20,
"source": 1,
"name": "_srv._tcp",
"value": "foo-2.unit.tests.",
"id": 11189878,
"priority": 12,
"type": "SRV",
"port": 30
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 600,
"source": 1,
"name": "aaaa",
"value": "2601:644:500:e210:62f8:1dff:feb8:947a",
"id": 11189879,
"type": "AAAA"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 300,
"source": 1,
"name": "cname",
"value": "",
"id": 11189880,
"type": "CNAME"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 3600,
"source": 1,
"name": "included",
"value": "",
"id": 11189881,
"type": "CNAME"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"mxLevel": 30,
"ttl": 300,
"source": 1,
"name": "mx",
"value": "smtp-3.unit.tests.",
"id": 11189882,
"type": "MX"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"mxLevel": 20,
"ttl": 300,
"source": 1,
"name": "mx",
"value": "smtp-2.unit.tests.",
"id": 11189883,
"type": "MX"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"mxLevel": 10,
"ttl": 300,
"source": 1,
"name": "mx",
"value": "smtp-4.unit.tests.",
"id": 11189884,
"type": "MX"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"mxLevel": 40,
"ttl": 300,
"source": 1,
"name": "mx",
"value": "smtp-1.unit.tests.",
"id": 11189885,
"type": "MX"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 600,
"source": 1,
"name": "spf",
"value": "\"v=spf1 ip4:192.168.0.1/16-all\"",
"id": 11189886,
"type": "SPF"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 600,
"source": 1,
"name": "txt",
"value": "\"Bah bah black sheep\"",
"id": 11189887,
"type": "TXT"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 600,
"source": 1,
"name": "txt",
"value": "\"have you any wool.\"",
"id": 11189888,
"type": "TXT"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 600,
"source": 1,
"name": "txt",
"value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"",
"id": 11189889,
"type": "TXT"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 3600,
"source": 1,
"name": "under",
"value": "ns1.unit.tests.",
"id": 11189890,
"type": "NS"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 3600,
"source": 1,
"name": "under",
"value": "ns2",
"id": 11189891,
"type": "NS"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 300,
"source": 1,
"name": "www",
"value": "2.2.3.6",
"id": 11189892,
"type": "A"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 300,
"source": 1,
"name": "www.sub",
"value": "2.2.3.6",
"id": 11189893,
"type": "A"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 300,
"source": 1,
"name": "ptr",
"value": "foo.bar.com.",
"id": 11189894,
"type": "PTR"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 1800,
"source": 1,
"name": "",
"value": "aname.unit.tests.",
"id": 11189895,
"type": "ANAME"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": true,
"ttl": 1800,
"source": 1,
"name": "unsupported",
"value": "https://redirect.unit.tests",
"id": 11189897,
"title": "Unsupported Record",
"keywords": "unsupported",
"redirectType": "Standard - 302",
"description": "unsupported record",
"type": "HTTPRED"
}],
"page": 0
}

+ 0
- 4190
tests/fixtures/dyn-traffic-director-get.json
File diff suppressed because it is too large
View File


+ 0
- 296
tests/fixtures/easydns-records.json View File

@ -1,296 +0,0 @@
{
"tm": 1000000000,
"data": [
{
"id": "12340001",
"domain": "unit.tests",
"host": "@",
"ttl": "3600",
"prio": "0",
"type": "SOA",
"rdata": "dns1.easydns.com. zone.easydns.com. 2020010101 3600 600 604800 0",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340002",
"domain": "unit.tests",
"host": "@",
"ttl": "300",
"prio": "0",
"type": "A",
"rdata": "1.2.3.4",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340003",
"domain": "unit.tests",
"host": "@",
"ttl": "300",
"prio": "0",
"type": "A",
"rdata": "1.2.3.5",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340004",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": null,
"type": "NS",
"rdata": "6.2.3.4.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340005",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": null,
"type": "NS",
"rdata": "7.2.3.4.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340006",
"domain": "unit.tests",
"host": "@",
"ttl": "3600",
"prio": "0",
"type": "CAA",
"rdata": "0 issue ca.unit.tests",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340007",
"domain": "unit.tests",
"host": "_srv._tcp",
"ttl": "600",
"prio": "12",
"type": "SRV",
"rdata": "12 20 30 foo-2.unit.tests.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340008",
"domain": "unit.tests",
"host": "_srv._tcp",
"ttl": "600",
"prio": "12",
"type": "SRV",
"rdata": "10 20 30 foo-1.unit.tests.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340009",
"domain": "unit.tests",
"host": "aaaa",
"ttl": "600",
"prio": "0",
"type": "AAAA",
"rdata": "2601:644:500:e210:62f8:1dff:feb8:947a",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340010",
"domain": "unit.tests",
"host": "cname",
"ttl": "300",
"prio": null,
"type": "CNAME",
"rdata": "@",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340012",
"domain": "unit.tests",
"host": "mx",
"ttl": "300",
"prio": "10",
"type": "MX",
"rdata": "smtp-4.unit.tests.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340013",
"domain": "unit.tests",
"host": "mx",
"ttl": "300",
"prio": "20",
"type": "MX",
"rdata": "smtp-2.unit.tests.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340014",
"domain": "unit.tests",
"host": "mx",
"ttl": "300",
"prio": "30",
"type": "MX",
"rdata": "smtp-3.unit.tests.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340015",
"domain": "unit.tests",
"host": "mx",
"ttl": "300",
"prio": "40",
"type": "MX",
"rdata": "smtp-1.unit.tests.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340016",
"domain": "unit.tests",
"host": "naptr",
"ttl": "600",
"prio": null,
"type": "NAPTR",
"rdata": "100 100 'U' 'SIP+D2U' '!^.*$!sip:info@bar.example.com!' .",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340017",
"domain": "unit.tests",
"host": "naptr",
"ttl": "600",
"prio": null,
"type": "NAPTR",
"rdata": "10 100 'S' 'SIP+D2U' '!^.*$!sip:info@bar.example.com!' .",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340018",
"domain": "unit.tests",
"host": "sub",
"ttl": "3600",
"prio": null,
"type": "NS",
"rdata": "6.2.3.4.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340019",
"domain": "unit.tests",
"host": "sub",
"ttl": "0",
"prio": null,
"type": "NS",
"rdata": "7.2.3.4.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340020",
"domain": "unit.tests",
"host": "www",
"ttl": "300",
"prio": "0",
"type": "A",
"rdata": "2.2.3.6",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340021",
"domain": "unit.tests",
"host": "www.sub",
"ttl": "300",
"prio": "0",
"type": "A",
"rdata": "2.2.3.6",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340022",
"domain": "unit.tests",
"host": "included",
"ttl": "3600",
"prio": null,
"type": "CNAME",
"rdata": "unit.tests.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340011",
"domain": "unit.tests",
"host": "txt",
"ttl": "600",
"prio": "0",
"type": "TXT",
"rdata": "Bah bah black sheep",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340023",
"domain": "unit.tests",
"host": "txt",
"ttl": "600",
"prio": "0",
"type": "TXT",
"rdata": "have you any wool.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340024",
"domain": "unit.tests",
"host": "txt",
"ttl": "600",
"prio": "0",
"type": "TXT",
"rdata": "v=DKIM1;k=rsa;s=email;h=sha256;p=A\/kinda+of\/long\/string+with+numb3rs",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340025",
"domain": "unit.tests",
"host": "_imap._tcp",
"ttl": "600",
"prio": "0",
"type": "SRV",
"rdata": "0 0 0 .",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340026",
"domain": "unit.tests",
"host": "_pop3._tcp",
"ttl": "600",
"prio": "0",
"type": "SRV",
"rdata": "0 0 0 .",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}
],
"count": 26,
"total": 26,
"start": 0,
"max": 1000,
"status": 200
}

+ 0
- 35
tests/fixtures/edgedns-invalid-content.json View File

@ -1,35 +0,0 @@
{
"recordsets": [
{
"rdata": [
"",
"12 20 foo-2.unit.tests."
],
"type": "SRV",
"name": "_srv._tcp.unit.tests",
"ttl": 600
},
{
"rdata": [
"",
"1 1"
],
"type": "SSHFP",
"name": "unit.tests",
"ttl": 3600
},
{
"rdata": [
"",
"100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ."
],
"type": "NAPTR",
"name": "naptr.unit.tests",
"ttl": 600
}
],
"metadata": {
"totalElements": 3,
"showAll": true
}
}

+ 0
- 166
tests/fixtures/edgedns-records-prev-other.json View File

@ -1,166 +0,0 @@
{
"recordsets": [
{
"rdata": [
"10 20 30 foo-1.other.tests.",
"12 20 30 foo-2.other.tests."
],
"type": "SRV",
"name": "_srv._tcp.old.other.tests",
"ttl": 600
},
{
"rdata": [
"10 20 30 foo-1.other.tests.",
"12 20 30 foo-2.other.tests."
],
"type": "SRV",
"name": "_srv._tcp.old.other.tests",
"ttl": 600
},
{
"rdata": [
"2601:644:500:e210:62f8:1dff:feb8:9471"
],
"type": "AAAA",
"name": "aaaa.old.other.tests",
"ttl": 600
},
{
"rdata": [
"ns1.akam.net.",
"ns2.akam.net.",
"ns3.akam.net.",
"ns4.akam.net."
],
"type": "NS",
"name": "old.other.tests",
"ttl": 3600
},
{
"rdata": [
"1.2.3.4",
"1.2.3.5"
],
"type": "A",
"name": "old.other.tests",
"ttl": 300
},
{
"rdata": [
"ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300"
],
"type": "SOA",
"name": "other.tests",
"ttl": 3600
},
{
"rdata": [
"1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49",
"1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73"
],
"type": "SSHFP",
"name": "old.other.tests",
"ttl": 3600
},
{
"rdata": [
"other.tests."
],
"type": "CNAME",
"name": "old.cname.other.tests",
"ttl": 300
},
{
"rdata": [
"other.tests."
],
"type": "CNAME",
"name": "excluded.old.other.tests",
"ttl": 3600
},
{
"rdata": [
"other.tests."
],
"type": "CNAME",
"name": "included.old.other.tests",
"ttl": 3600
},
{
"rdata": [
"10 smtp-4.other.tests.",
"20 smtp-2.other.tests.",
"30 smtp-3.other.tests.",
"40 smtp-1.other.tests."
],
"type": "MX",
"name": "mx.old.other.tests",
"ttl": 300
},
{
"rdata": [
"10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .",
"100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ."
],
"type": "NAPTR",
"name": "naptr.old.other.tests",
"ttl": 600
},
{
"rdata": [
"foo.bar.com."
],
"type": "PTR",
"name": "ptr.old.other.tests",
"ttl": 300
},
{
"rdata": [
"\"v=spf1 ip4:192.168.0.1/16-all\""
],
"type": "SPF",
"name": "spf.old.other.tests",
"ttl": 600
},
{
"rdata": [
"ns1.other.tests.",
"ns2.other.tests."
],
"type": "NS",
"name": "under.old.other.tests",
"ttl": 3600
},
{
"rdata": [
"\"Bah bah black sheep\"",
"\"have you any wool.\"",
"\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\""
],
"type": "TXT",
"name": "txt.old.other.tests",
"ttl": 600
},
{
"rdata": [
"2.2.3.7"
],
"type": "A",
"name": "www.other.tests",
"ttl": 300
},
{
"rdata": [
"2.2.3.6"
],
"type": "A",
"name": "www.sub.old.other.tests",
"ttl": 300
}
],
"metadata": {
"totalElements": 16,
"showAll": true
}
}

+ 0
- 166
tests/fixtures/edgedns-records-prev.json View File

@ -1,166 +0,0 @@
{
"recordsets": [
{
"rdata": [
"10 20 30 foo-1.unit.tests.",
"12 20 30 foo-2.unit.tests."
],
"type": "SRV",
"name": "_srv._tcp.old.unit.tests",
"ttl": 600
},
{
"rdata": [
"10 20 30 foo-1.unit.tests.",
"12 20 30 foo-2.unit.tests."
],
"type": "SRV",
"name": "_srv._tcp.old.unit.tests",
"ttl": 600
},
{
"rdata": [
"2601:644:500:e210:62f8:1dff:feb8:9471"
],
"type": "AAAA",
"name": "aaaa.old.unit.tests",
"ttl": 600
},
{
"rdata": [
"ns1.akam.net.",
"ns2.akam.net.",
"ns3.akam.net.",
"ns4.akam.net."
],
"type": "NS",
"name": "old.unit.tests",
"ttl": 3600
},
{
"rdata": [
"1.2.3.4",
"1.2.3.5"
],
"type": "A",
"name": "old.unit.tests",
"ttl": 300
},
{
"rdata": [
"ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300"
],
"type": "SOA",
"name": "unit.tests",
"ttl": 3600
},
{
"rdata": [
"1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49",
"1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73"
],
"type": "SSHFP",
"name": "old.unit.tests",
"ttl": 3600
},
{
"rdata": [
"unit.tests"
],
"type": "CNAME",
"name": "old.cname.unit.tests",
"ttl": 300
},
{
"rdata": [
"unit.tests."
],
"type": "CNAME",
"name": "excluded.old.unit.tests",
"ttl": 3600
},
{
"rdata": [
"unit.tests."
],
"type": "CNAME",
"name": "included.old.unit.tests",
"ttl": 3600
},
{
"rdata": [
"10 smtp-4.unit.tests.",
"20 smtp-2.unit.tests.",
"30 smtp-3.unit.tests.",
"40 smtp-1.unit.tests."
],
"type": "MX",
"name": "mx.old.unit.tests",
"ttl": 300
},
{
"rdata": [
"10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .",
"100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ."
],
"type": "NAPTR",
"name": "naptr.old.unit.tests",
"ttl": 600
},
{
"rdata": [
"foo.bar.com."
],
"type": "PTR",
"name": "ptr.old.unit.tests",
"ttl": 300
},
{
"rdata": [
"\"v=spf1 ip4:192.168.0.1/16-all\""
],
"type": "SPF",
"name": "spf.old.unit.tests",
"ttl": 600
},
{
"rdata": [
"ns1.unit.tests.",
"ns2.unit.tests."
],
"type": "NS",
"name": "under.old.unit.tests",
"ttl": 3600
},
{
"rdata": [
"\"Bah bah black sheep\"",
"\"have you any wool.\"",
"\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\""
],
"type": "TXT",
"name": "txt.old.unit.tests",
"ttl": 600
},
{
"rdata": [
"2.2.3.7"
],
"type": "A",
"name": "www.unit.tests",
"ttl": 300
},
{
"rdata": [
"2.2.3.6"
],
"type": "A",
"name": "www.sub.old.unit.tests",
"ttl": 300
}
],
"metadata": {
"totalElements": 16,
"showAll": true
}
}

+ 0
- 173
tests/fixtures/edgedns-records.json View File

@ -1,173 +0,0 @@
{
"recordsets": [
{
"rdata": [
"10 20 30 foo-1.unit.tests.",
"12 20 30 foo-2.unit.tests."
],
"type": "SRV",
"name": "_srv._tcp.unit.tests",
"ttl": 600
},
{
"rdata": [
"0 0 0 ."
],
"type": "SRV",
"name": "_imap._tcp.unit.tests",
"ttl": 600
},
{
"rdata": [
"0 0 0 ."
],
"type": "SRV",
"name": "_pop3._tcp.unit.tests",
"ttl": 600
},
{
"rdata": [
"2601:644:500:e210:62f8:1dff:feb8:947a"
],
"type": "AAAA",
"name": "aaaa.unit.tests",
"ttl": 600
},
{
"rdata": [
"ns1.akam.net.",
"ns2.akam.net.",
"ns3.akam.net.",
"ns4.akam.net."
],
"type": "NS",
"name": "unit.tests",
"ttl": 3600
},
{
"rdata": [
"1.2.3.4",
"1.2.3.5"
],
"type": "A",
"name": "unit.tests",
"ttl": 300
},
{
"rdata": [
"ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300"
],
"type": "SOA",
"name": "unit.tests",
"ttl": 3600
},
{
"rdata": [
"1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49",
"1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73"
],
"type": "SSHFP",
"name": "unit.tests",
"ttl": 3600
},
{
"rdata": [
"unit.tests."
],
"type": "CNAME",
"name": "cname.unit.tests",
"ttl": 300
},
{
"rdata": [
"unit.tests."
],
"type": "CNAME",
"name": "excluded.unit.tests",
"ttl": 3600
},
{
"rdata": [
"unit.tests."
],
"type": "CNAME",
"name": "included.unit.tests",
"ttl": 3600
},
{
"rdata": [
"10 smtp-4.unit.tests.",
"20 smtp-2.unit.tests.",
"30 smtp-3.unit.tests.",
"40 smtp-1.unit.tests."
],
"type": "MX",
"name": "mx.unit.tests",
"ttl": 300
},
{
"rdata": [
"10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .",
"100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" ."
],
"type": "NAPTR",
"name": "naptr.unit.tests",
"ttl": 600
},
{
"rdata": [
"foo.bar.com."
],
"type": "PTR",
"name": "ptr.unit.tests",
"ttl": 300
},
{
"rdata": [
"\"v=spf1 ip4:192.168.0.1/16-all\""
],
"type": "SPF",
"name": "spf.unit.tests",
"ttl": 600
},
{
"rdata": [
"ns1.unit.tests.",
"ns2.unit.tests."
],
"type": "NS",
"name": "under.unit.tests",
"ttl": 3600
},
{
"rdata": [
"\"Bah bah black sheep\"",
"\"have you any wool.\"",
"\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\""
],
"type": "TXT",
"name": "txt.unit.tests",
"ttl": 600
},
{
"rdata": [
"2.2.3.6"
],
"type": "A",
"name": "www.unit.tests",
"ttl": 300
},
{
"rdata": [
"2.2.3.6"
],
"type": "A",
"name": "www.sub.unit.tests",
"ttl": 300
}
],
"metadata": {
"totalElements": 18,
"showAll": true
}
}

+ 5
- 1624
tests/test_octodns_provider_cloudflare.py
File diff suppressed because it is too large
View File


+ 5
- 1885
tests/test_octodns_provider_constellix.py
File diff suppressed because it is too large
View File


+ 5
- 263
tests/test_octodns_provider_digitalocean.py View File

@ -2,273 +2,15 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from mock import Mock, call
from os.path import dirname, join
from requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from unittest import TestCase
from octodns.record import Record
from octodns.provider.digitalocean import DigitalOceanClientNotFound, \
DigitalOceanProvider
from octodns.provider.yaml import YamlProvider
from octodns.zone import Zone
class TestDigitalOceanProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
# Our test suite differs a bit, add our NS and remove the simple one
expected.add_record(Record.new(expected, 'under', {
'ttl': 3600,
'type': 'NS',
'values': [
'ns1.unit.tests.',
'ns2.unit.tests.',
]
}))
for record in list(expected.records):
if record.name == 'sub' and record._type == 'NS':
expected._remove_record(record)
break
def test_populate(self):
provider = DigitalOceanProvider('test', 'token')
# Bad auth
with requests_mock() as mock:
mock.get(ANY, status_code=401,
text='{"id":"unauthorized",'
'"message":"Unable to authenticate you."}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', str(ctx.exception))
# General error
with requests_mock() as mock:
mock.get(ANY, status_code=502, text='Things caught fire')
with self.assertRaises(HTTPError) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(502, ctx.exception.response.status_code)
# Non-existent zone doesn't populate anything
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='{"id":"not_found","message":"The resource you '
'were accessing could not be found."}')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
# No diffs == no changes
with requests_mock() as mock:
base = 'https://api.digitalocean.com/v2/domains/unit.tests/' \
'records?page='
with open('tests/fixtures/digitalocean-page-1.json') as fh:
mock.get(f'{base}1', text=fh.read())
with open('tests/fixtures/digitalocean-page-2.json') as fh:
mock.get(f'{base}2', text=fh.read())
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(14, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(14, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
def test_apply(self):
provider = DigitalOceanProvider('test', 'token')
resp = Mock()
resp.json = Mock()
provider._client._request = Mock(return_value=resp)
domain_after_creation = {
"domain_records": [{
"id": 11189874,
"type": "NS",
"name": "@",
"data": "ns1.digitalocean.com",
"priority": None,
"port": None,
"ttl": 3600,
"weight": None,
"flags": None,
"tag": None
}, {
"id": 11189875,
"type": "NS",
"name": "@",
"data": "ns2.digitalocean.com",
"priority": None,
"port": None,
"ttl": 3600,
"weight": None,
"flags": None,
"tag": None
}, {
"id": 11189876,
"type": "NS",
"name": "@",
"data": "ns3.digitalocean.com",
"priority": None,
"port": None,
"ttl": 3600,
"weight": None,
"flags": None,
"tag": None
}, {
"id": 11189877,
"type": "A",
"name": "@",
"data": "192.0.2.1",
"priority": None,
"port": None,
"ttl": 3600,
"weight": None,
"flags": None,
"tag": None
}],
"links": {},
"meta": {
"total": 4
}
}
# non-existent domain, create everything
resp.json.side_effect = [
DigitalOceanClientNotFound, # no zone in populate
DigitalOceanClientNotFound, # no domain during apply
domain_after_creation
]
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 10
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
self.assertFalse(plan.exists)
provider._client._request.assert_has_calls([
# created the domain
call('POST', '/domains', data={'ip_address': '192.0.2.1',
'name': 'unit.tests'}),
# get all records in newly created zone
call('GET', '/domains/unit.tests/records', {'page': 1}),
# delete the initial A record
call('DELETE', '/domains/unit.tests/records/11189877'),
# created at least some of the record with expected data
call('POST', '/domains/unit.tests/records', data={
'data': '1.2.3.4',
'name': '@',
'ttl': 300, 'type': 'A'}),
call('POST', '/domains/unit.tests/records', data={
'data': '1.2.3.5',
'name': '@',
'ttl': 300, 'type': 'A'}),
call('POST', '/domains/unit.tests/records', data={
'data': 'ca.unit.tests',
'flags': 0, 'name': '@',
'tag': 'issue',
'ttl': 3600, 'type': 'CAA'}),
call('POST', '/domains/unit.tests/records', data={
'name': '_imap._tcp',
'weight': 0,
'data': '.',
'priority': 0,
'ttl': 600,
'type': 'SRV',
'port': 0
}),
call('POST', '/domains/unit.tests/records', data={
'name': '_pop3._tcp',
'weight': 0,
'data': '.',
'priority': 0,
'ttl': 600,
'type': 'SRV',
'port': 0
}),
call('POST', '/domains/unit.tests/records', data={
'name': '_srv._tcp',
'weight': 20,
'data': 'foo-1.unit.tests.',
'priority': 10,
'ttl': 600,
'type': 'SRV',
'port': 30
}),
])
self.assertEquals(26, provider._client._request.call_count)
provider._client._request.reset_mock()
# delete 1 and update 1
provider._client.records = Mock(return_value=[
{
'id': 11189897,
'name': 'www',
'data': '1.2.3.4',
'ttl': 300,
'type': 'A',
},
{
'id': 11189898,
'name': 'www',
'data': '2.2.3.4',
'ttl': 300,
'type': 'A',
},
{
'id': 11189899,
'name': 'ttl',
'data': '3.2.3.4',
'ttl': 600,
'type': 'A',
}
])
# Domain exists, we don't care about return
resp.json.side_effect = ['{}']
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'ttl', {
'ttl': 300,
'type': 'A',
'value': '3.2.3.4'
}))
class TestDigitalOceanShim(TestCase):
plan = provider.plan(wanted)
self.assertTrue(plan.exists)
self.assertEquals(2, len(plan.changes))
self.assertEquals(2, provider.apply(plan))
# recreate for update, and delete for the 2 parts of the other
provider._client._request.assert_has_calls([
call('POST', '/domains/unit.tests/records', data={
'data': '3.2.3.4',
'type': 'A',
'name': 'ttl',
'ttl': 300
}),
call('DELETE', '/domains/unit.tests/records/11189899'),
call('DELETE', '/domains/unit.tests/records/11189897'),
call('DELETE', '/domains/unit.tests/records/11189898')
], any_order=True)
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.digitalocean import DigitalOceanProvider
DigitalOceanProvider

+ 5
- 217
tests/test_octodns_provider_dnsmadeeasy.py View File

@ -2,227 +2,15 @@
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from mock import Mock, call
from os.path import dirname, join
from requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from unittest import TestCase
from octodns.record import Record
from octodns.provider.dnsmadeeasy import DnsMadeEasyClientNotFound, \
DnsMadeEasyProvider
from octodns.provider.yaml import YamlProvider
from octodns.zone import Zone
import json
class TestDnsMadeEasyProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
# Our test suite differs a bit, add our NS and remove the simple one
expected.add_record(Record.new(expected, 'under', {
'ttl': 3600,
'type': 'NS',
'values': [
'ns1.unit.tests.',
'ns2.unit.tests.',
]
}))
# Add some ALIAS records
expected.add_record(Record.new(expected, '', {
'ttl': 1800,
'type': 'ALIAS',
'value': 'aname.unit.tests.'
}))
for record in list(expected.records):
if record.name == 'sub' and record._type == 'NS':
expected._remove_record(record)
break
def test_populate(self):
provider = DnsMadeEasyProvider('test', 'api', 'secret')
# Bad auth
with requests_mock() as mock:
mock.get(ANY, status_code=401,
text='{"error": ["API key not found"]}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', str(ctx.exception))
# Bad request
with requests_mock() as mock:
mock.get(ANY, status_code=400,
text='{"error": ["Rate limit exceeded"]}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('\n - Rate limit exceeded', str(ctx.exception))
# General error
with requests_mock() as mock:
mock.get(ANY, status_code=502, text='Things caught fire')
with self.assertRaises(HTTPError) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(502, ctx.exception.response.status_code)
# Non-existent zone doesn't populate anything
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='<html><head></head><body></body></html>')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
# No diffs == no changes
with requests_mock() as mock:
base = 'https://api.dnsmadeeasy.com/V2.0/dns/managed'
with open('tests/fixtures/dnsmadeeasy-domains.json') as fh:
mock.get(f'{base}/', text=fh.read())
with open('tests/fixtures/dnsmadeeasy-records.json') as fh:
mock.get(f'{base}/123123/records', text=fh.read())
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(14, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(14, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
def test_apply(self):
# Create provider with sandbox enabled
provider = DnsMadeEasyProvider('test', 'api', 'secret', True)
resp = Mock()
resp.json = Mock()
provider._client._request = Mock(return_value=resp)
with open('tests/fixtures/dnsmadeeasy-domains.json') as fh:
domains = json.load(fh)
# non-existent domain, create everything
resp.json.side_effect = [
DnsMadeEasyClientNotFound, # no zone in populate
DnsMadeEasyClientNotFound, # no domain during apply
domains
]
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 10
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
provider._client._request.assert_has_calls([
# created the domain
call('POST', '/', data={'name': 'unit.tests'}),
# get all domains to build the cache
call('GET', '/'),
# created at least some of the record with expected data
call('POST', '/123123/records', data={
'type': 'A',
'name': '',
'value': '1.2.3.4',
'ttl': 300}),
call('POST', '/123123/records', data={
'type': 'A',
'name': '',
'value': '1.2.3.5',
'ttl': 300}),
call('POST', '/123123/records', data={
'type': 'ANAME',
'name': '',
'value': 'aname.unit.tests.',
'ttl': 1800}),
call('POST', '/123123/records', data={
'name': '',
'value': 'ca.unit.tests',
'issuerCritical': 0, 'caaType': 'issue',
'ttl': 3600, 'type': 'CAA'}),
call('POST', '/123123/records', data={
'name': '_srv._tcp',
'weight': 20,
'value': 'foo-1.unit.tests.',
'priority': 10,
'ttl': 600,
'type': 'SRV',
'port': 30
}),
])
self.assertEquals(26, provider._client._request.call_count)
provider._client._request.reset_mock()
# delete 1 and update 1
provider._client.records = Mock(return_value=[
{
'id': 11189897,
'name': 'www',
'value': '1.2.3.4',
'ttl': 300,
'type': 'A',
},
{
'id': 11189898,
'name': 'www',
'value': '2.2.3.4',
'ttl': 300,
'type': 'A',
},
{
'id': 11189899,
'name': 'ttl',
'value': '3.2.3.4',
'ttl': 600,
'type': 'A',
}
])
# Domain exists, we don't care about return
resp.json.side_effect = ['{}']
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'ttl', {
'ttl': 300,
'type': 'A',
'value': '3.2.3.4'
}))
plan = provider.plan(wanted)
self.assertEquals(2, len(plan.changes))
self.assertEquals(2, provider.apply(plan))
class TestDnsMadeEasyShim(TestCase):
# recreate for update, and deletes for the 2 parts of the other
provider._client._request.assert_has_calls([
call('POST', '/123123/records', data={
'value': '3.2.3.4',
'type': 'A',
'name': 'ttl',
'ttl': 300
}),
call('DELETE', '/123123/records/11189899'),
call('DELETE', '/123123/records/11189897'),
call('DELETE', '/123123/records/11189898')
], any_order=True)
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.dnsmadeeasy import DnsMadeEasyProvider
DnsMadeEasyProvider

+ 5
- 2644
tests/test_octodns_provider_dyn.py
File diff suppressed because it is too large
View File


+ 5
- 434
tests/test_octodns_provider_easydns.py View File

@ -5,441 +5,12 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
import json
from mock import Mock, call
from os.path import dirname, join
from requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from unittest import TestCase
from octodns.record import Record
from octodns.provider.easydns import EasyDNSClientNotFound, \
EasyDNSProvider
from octodns.provider.yaml import YamlProvider
from octodns.zone import Zone
class TestEasyDnsShim(TestCase):
class TestEasyDNSProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
def test_populate(self):
provider = EasyDNSProvider('test', 'token', 'apikey')
# Bad auth
with requests_mock() as mock:
mock.get(ANY, status_code=401,
text='{"id":"unauthorized",'
'"message":"Unable to authenticate you."}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', str(ctx.exception))
# Bad request
with requests_mock() as mock:
mock.get(ANY, status_code=400,
text='{"id":"invalid",'
'"message":"Bad request"}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Bad request', str(ctx.exception))
# General error
with requests_mock() as mock:
mock.get(ANY, status_code=502, text='Things caught fire')
with self.assertRaises(HTTPError) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(502, ctx.exception.response.status_code)
# Non-existent zone doesn't populate anything
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='{"id":"not_found","message":"The resource you '
'were accessing could not be found."}')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
# No diffs == no changes
with requests_mock() as mock:
base = 'https://rest.easydns.net/zones/records/'
with open('tests/fixtures/easydns-records.json') as fh:
mock.get(f'{base}parsed/unit.tests', text=fh.read())
with open('tests/fixtures/easydns-records.json') as fh:
mock.get(f'{base}all/unit.tests', text=fh.read())
provider.populate(zone)
self.assertEquals(15, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(15, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
def test_domain(self):
provider = EasyDNSProvider('test', 'token', 'apikey')
with requests_mock() as mock:
base = 'https://rest.easydns.net/'
mock.get(f'{base}domain/unit.tests', status_code=400,
text='{"id":"not_found","message":"The resource you '
'were accessing could not be found."}')
with self.assertRaises(Exception) as ctx:
provider._client.domain('unit.tests')
self.assertEquals('Not Found', str(ctx.exception))
def test_apply_not_found(self):
provider = EasyDNSProvider('test', 'token', 'apikey',
domain_create_sleep=0)
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'test1', {
"name": "test1",
"ttl": 300,
"type": "A",
"value": "1.2.3.4",
}))
with requests_mock() as mock:
base = 'https://rest.easydns.net/'
mock.get(f'{base}domain/unit.tests', status_code=404,
text='{"id":"not_found","message":"The resource you '
'were accessing could not be found."}')
mock.put(f'{base}domains/add/unit.tests', status_code=200,
text='{"id":"OK","message":"Zone created."}')
mock.get(f'{base}zones/records/parsed/unit.tests',
status_code=404,
text='{"id":"not_found","message":"The resource you '
'were accessing could not be found."}')
mock.get(f'{base}zones/records/all/unit.tests', status_code=404,
text='{"id":"not_found","message":"The resource you '
'were accessing could not be found."}')
plan = provider.plan(wanted)
self.assertFalse(plan.exists)
self.assertEquals(1, len(plan.changes))
with self.assertRaises(Exception) as ctx:
provider.apply(plan)
self.assertEquals('Not Found', str(ctx.exception))
def test_domain_create(self):
provider = EasyDNSProvider('test', 'token', 'apikey',
domain_create_sleep=0)
domain_after_creation = {
"tm": 1000000000,
"data": [{
"id": "12341001",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": "0",
"type": "SOA",
"rdata": "dns1.easydns.com. zone.easydns.com. "
"2020010101 3600 600 604800 0",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}, {
"id": "12341002",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": "0",
"type": "NS",
"rdata": "LOCAL.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}, {
"id": "12341003",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": "0",
"type": "MX",
"rdata": "LOCAL.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}],
"count": 3,
"total": 3,
"start": 0,
"max": 1000,
"status": 200
}
with requests_mock() as mock:
base = 'https://rest.easydns.net/'
mock.put(f'{base}domains/add/unit.tests',
status_code=201, text='{"id":"OK"}')
mock.get(f'{base}zones/records/all/unit.tests',
text=json.dumps(domain_after_creation))
mock.delete(ANY, text='{"id":"OK"}')
provider._client.domain_create('unit.tests')
def test_caa(self):
provider = EasyDNSProvider('test', 'token', 'apikey')
# Invalid rdata records
caa_record_invalid = [{
"domain": "unit.tests",
"host": "@",
"ttl": "3600",
"prio": "0",
"type": "CAA",
"rdata": "0",
}]
# Valid rdata records
caa_record_valid = [{
"domain": "unit.tests",
"host": "@",
"ttl": "3600",
"prio": "0",
"type": "CAA",
"rdata": "0 issue ca.unit.tests",
}]
provider._data_for_CAA('CAA', caa_record_invalid)
provider._data_for_CAA('CAA', caa_record_valid)
def test_naptr(self):
provider = EasyDNSProvider('test', 'token', 'apikey')
# Invalid rdata records
naptr_record_invalid = [{
"domain": "unit.tests",
"host": "naptr",
"ttl": "600",
"prio": "10",
"type": "NAPTR",
"rdata": "100",
}]
# Valid rdata records
naptr_record_valid = [{
"domain": "unit.tests",
"host": "naptr",
"ttl": "600",
"prio": "10",
"type": "NAPTR",
"rdata": "10 10 'U' 'SIP+D2U' '!^.*$!sip:info@bar.example.com!' .",
}]
provider._data_for_NAPTR('NAPTR', naptr_record_invalid)
provider._data_for_NAPTR('NAPTR', naptr_record_valid)
def test_srv(self):
provider = EasyDNSProvider('test', 'token', 'apikey')
# Invalid rdata records
srv_invalid = [{
"domain": "unit.tests",
"host": "_srv._tcp",
"ttl": "600",
"type": "SRV",
"rdata": "",
}]
srv_invalid2 = [{
"domain": "unit.tests",
"host": "_srv._tcp",
"ttl": "600",
"type": "SRV",
"rdata": "11",
}]
srv_invalid3 = [{
"domain": "unit.tests",
"host": "_srv._tcp",
"ttl": "600",
"type": "SRV",
"rdata": "12 30",
}]
srv_invalid4 = [{
"domain": "unit.tests",
"host": "_srv._tcp",
"ttl": "600",
"type": "SRV",
"rdata": "13 40 1234",
}]
# Valid rdata
srv_valid = [{
"domain": "unit.tests",
"host": "_srv._tcp",
"ttl": "600",
"type": "SRV",
"rdata": "100 20 5678 foo-2.unit.tests.",
}]
srv_invalid_content = provider._data_for_SRV('SRV', srv_invalid)
srv_invalid_content2 = provider._data_for_SRV('SRV', srv_invalid2)
srv_invalid_content3 = provider._data_for_SRV('SRV', srv_invalid3)
srv_invalid_content4 = provider._data_for_SRV('SRV', srv_invalid4)
srv_valid_content = provider._data_for_SRV('SRV', srv_valid)
self.assertEqual(srv_valid_content['values'][0]['priority'], 100)
self.assertEqual(srv_invalid_content['values'][0]['priority'], 0)
self.assertEqual(srv_invalid_content2['values'][0]['priority'], 11)
self.assertEqual(srv_invalid_content3['values'][0]['priority'], 12)
self.assertEqual(srv_invalid_content4['values'][0]['priority'], 13)
self.assertEqual(srv_valid_content['values'][0]['weight'], 20)
self.assertEqual(srv_invalid_content['values'][0]['weight'], 0)
self.assertEqual(srv_invalid_content2['values'][0]['weight'], 0)
self.assertEqual(srv_invalid_content3['values'][0]['weight'], 30)
self.assertEqual(srv_invalid_content4['values'][0]['weight'], 40)
self.assertEqual(srv_valid_content['values'][0]['port'], 5678)
self.assertEqual(srv_invalid_content['values'][0]['port'], 0)
self.assertEqual(srv_invalid_content2['values'][0]['port'], 0)
self.assertEqual(srv_invalid_content3['values'][0]['port'], 0)
self.assertEqual(srv_invalid_content4['values'][0]['port'], 1234)
self.assertEqual(srv_valid_content['values'][0]['target'],
'foo-2.unit.tests.')
self.assertEqual(srv_invalid_content['values'][0]['target'], '')
self.assertEqual(srv_invalid_content2['values'][0]['target'], '')
self.assertEqual(srv_invalid_content3['values'][0]['target'], '')
self.assertEqual(srv_invalid_content4['values'][0]['target'], '')
def test_apply(self):
provider = EasyDNSProvider('test', 'token', 'apikey')
resp = Mock()
resp.json = Mock()
provider._client._request = Mock(return_value=resp)
domain_after_creation = {
"tm": 1000000000,
"data": [{
"id": "12341001",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": "0",
"type": "SOA",
"rdata": "dns1.easydns.com. zone.easydns.com. 2020010101"
" 3600 600 604800 0",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}, {
"id": "12341002",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": "0",
"type": "NS",
"rdata": "LOCAL.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}, {
"id": "12341003",
"domain": "unit.tests",
"host": "@",
"ttl": "0",
"prio": "0",
"type": "MX",
"rdata": "LOCAL.",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}],
"count": 3,
"total": 3,
"start": 0,
"max": 1000,
"status": 200
}
# non-existent domain, create everything
resp.json.side_effect = [
EasyDNSClientNotFound, # no zone in populate
domain_after_creation
]
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 9
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
self.assertFalse(plan.exists)
self.assertEquals(25, provider._client._request.call_count)
provider._client._request.reset_mock()
# delete 1 and update 1
provider._client.records = Mock(return_value=[
{
"id": "12342001",
"domain": "unit.tests",
"host": "www",
"ttl": "300",
"prio": "0",
"type": "A",
"rdata": "2.2.3.9",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}, {
"id": "12342002",
"domain": "unit.tests",
"host": "www",
"ttl": "300",
"prio": "0",
"type": "A",
"rdata": "2.2.3.8",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}, {
"id": "12342003",
"domain": "unit.tests",
"host": "test1",
"ttl": "3600",
"prio": "0",
"type": "A",
"rdata": "1.2.3.4",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}
])
# Domain exists, we don't care about return
resp.json.side_effect = ['{}']
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'test1', {
"name": "test1",
"ttl": 300,
"type": "A",
"value": "1.2.3.4",
}))
plan = provider.plan(wanted)
self.assertTrue(plan.exists)
self.assertEquals(2, len(plan.changes))
self.assertEquals(2, provider.apply(plan))
# recreate for update, and delete for the 2 parts of the other
provider._client._request.assert_has_calls([
call('PUT', '/zones/records/add/unit.tests/A', data={
'rdata': '1.2.3.4',
'name': 'test1',
'ttl': 300,
'type': 'A',
'host': 'test1',
}),
call('DELETE', '/zones/records/unit.tests/12342001'),
call('DELETE', '/zones/records/unit.tests/12342002'),
call('DELETE', '/zones/records/unit.tests/12342003')
], any_order=True)
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.easydns import EasyDnsProvider
EasyDnsProvider

+ 9
- 145
tests/test_octodns_provider_edgedns.py View File

@ -5,153 +5,17 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
# from mock import Mock, call
from os.path import dirname, join
from requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from unittest import TestCase
from octodns.record import Record
from octodns.provider.edgedns import AkamaiProvider
from octodns.provider.fastdns import AkamaiProvider as LegacyAkamaiProvider
from octodns.provider.yaml import YamlProvider
from octodns.zone import Zone
# Just for coverage
import octodns.provider.fastdns
# Quell warnings
octodns.provider.fastdns
class TestEdgeDnsProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
class TestAkamaiShim(TestCase):
# Our test suite differs a bit, add our NS and remove the simple one
expected.add_record(Record.new(expected, 'under', {
'ttl': 3600,
'type': 'NS',
'values': [
'ns1.unit.tests.',
'ns2.unit.tests.',
]
}))
for record in list(expected.records):
if record.name == 'sub' and record._type == 'NS':
expected._remove_record(record)
break
def test_populate(self):
provider = AkamaiProvider("test", "secret", "akam.com", "atok", "ctok")
# Bad Auth
with requests_mock() as mock:
mock.get(ANY, status_code=401, text='{"message": "Unauthorized"}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(401, ctx.exception.response.status_code)
# general error
with requests_mock() as mock:
mock.get(ANY, status_code=502, text='Things caught fire')
with self.assertRaises(HTTPError) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(502, ctx.exception.response.status_code)
# Non-existant zone doesn't populate anything
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='{"message": "Domain `foo.bar` not found"}')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
# No diffs == no changes
with requests_mock() as mock:
with open('tests/fixtures/edgedns-records.json') as fh:
mock.get(ANY, text=fh.read())
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(18, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(18, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
def test_apply(self):
provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok",
"cid", "gid")
# tests create update delete through previous state config json
with requests_mock() as mock:
with open('tests/fixtures/edgedns-records-prev.json') as fh:
mock.get(ANY, text=fh.read())
plan = provider.plan(self.expected)
mock.post(ANY, status_code=201)
mock.put(ANY, status_code=200)
mock.delete(ANY, status_code=204)
changes = provider.apply(plan)
self.assertEquals(31, changes)
# Test against a zone that doesn't exist yet
with requests_mock() as mock:
with open('tests/fixtures/edgedns-records-prev-other.json') as fh:
mock.get(ANY, status_code=404)
plan = provider.plan(self.expected)
mock.post(ANY, status_code=201)
mock.put(ANY, status_code=200)
mock.delete(ANY, status_code=204)
changes = provider.apply(plan)
self.assertEquals(16, changes)
# Test against a zone that doesn't exist yet, but gid not provided
with requests_mock() as mock:
with open('tests/fixtures/edgedns-records-prev-other.json') as fh:
mock.get(ANY, status_code=404)
provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok",
"cid")
plan = provider.plan(self.expected)
mock.post(ANY, status_code=201)
mock.put(ANY, status_code=200)
mock.delete(ANY, status_code=204)
changes = provider.apply(plan)
self.assertEquals(16, changes)
# Test against a zone that doesn't exist, but cid not provided
with requests_mock() as mock:
mock.get(ANY, status_code=404)
provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok")
plan = provider.plan(self.expected)
mock.post(ANY, status_code=201)
mock.put(ANY, status_code=200)
mock.delete(ANY, status_code=204)
try:
changes = provider.apply(plan)
except NameError as e:
expected = "contractId not specified to create zone"
self.assertEquals(str(e), expected)
class TestDeprecatedAkamaiProvider(TestCase):
def test_equivilent(self):
self.assertEquals(LegacyAkamaiProvider, AkamaiProvider)
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.edgedns import AkamaiProvider
AkamaiProvider

+ 5
- 167
tests/test_octodns_provider_etc_hosts.py View File

@ -5,174 +5,12 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from os import path
from os.path import dirname, isfile
from unittest import TestCase
from octodns.provider.etc_hosts import EtcHostsProvider
from octodns.provider.plan import Plan
from octodns.record import Record
from octodns.zone import Zone
from helpers import TemporaryDirectory
class TestEtcHostsShim(TestCase):
class TestEtcHostsProvider(TestCase):
def test_provider(self):
source = EtcHostsProvider('test', path.join(dirname(__file__),
'config'))
zone = Zone('unit.tests.', [])
# We never populate anything, when acting as a source
source.populate(zone, target=source)
self.assertEquals(0, len(zone.records))
# Same if we're acting as a target
source.populate(zone)
self.assertEquals(0, len(zone.records))
record = Record.new(zone, '', {
'ttl': 60,
'type': 'ALIAS',
'value': 'www.unit.tests.'
})
zone.add_record(record)
record = Record.new(zone, 'www', {
'ttl': 60,
'type': 'AAAA',
'value': '2001:4860:4860::8888',
})
zone.add_record(record)
record = Record.new(zone, 'www', {
'ttl': 60,
'type': 'A',
'values': ['1.1.1.1', '2.2.2.2'],
})
zone.add_record(record)
record = record.new(zone, 'v6', {
'ttl': 60,
'type': 'AAAA',
'value': '2001:4860:4860::8844',
})
zone.add_record(record)
record = record.new(zone, 'start', {
'ttl': 60,
'type': 'CNAME',
'value': 'middle.unit.tests.',
})
zone.add_record(record)
record = record.new(zone, 'middle', {
'ttl': 60,
'type': 'CNAME',
'value': 'unit.tests.',
})
zone.add_record(record)
record = record.new(zone, 'ext', {
'ttl': 60,
'type': 'CNAME',
'value': 'github.com.',
})
zone.add_record(record)
record = record.new(zone, '*', {
'ttl': 60,
'type': 'A',
'value': '3.3.3.3',
})
zone.add_record(record)
with TemporaryDirectory() as td:
# Add some subdirs to make sure that it can create them
directory = path.join(td.dirname, 'sub', 'dir')
hosts_file = path.join(directory, 'unit.tests.hosts')
target = EtcHostsProvider('test', directory)
# We add everything
plan = target.plan(zone)
self.assertEquals(len(zone.records), len(plan.changes))
self.assertFalse(isfile(hosts_file))
# Now actually do it
self.assertEquals(len(zone.records), target.apply(plan))
self.assertTrue(isfile(hosts_file))
with open(hosts_file) as fh:
data = fh.read()
# v6
self.assertTrue('2001:4860:4860::8844\tv6.unit.tests' in data)
# www
self.assertTrue('1.1.1.1\twww.unit.tests' in data)
# root ALIAS
self.assertTrue('# unit.tests -> www.unit.tests' in data)
self.assertTrue('1.1.1.1\tunit.tests' in data)
self.assertTrue('# start.unit.tests -> middle.unit.tests' in
data)
self.assertTrue('# middle.unit.tests -> unit.tests' in data)
self.assertTrue('# unit.tests -> www.unit.tests' in data)
self.assertTrue('1.1.1.1 start.unit.tests' in data)
# second empty run that won't create dirs and overwrites file
plan = Plan(zone, zone, [], True)
self.assertEquals(0, target.apply(plan))
def test_cname_loop(self):
source = EtcHostsProvider('test', path.join(dirname(__file__),
'config'))
zone = Zone('unit.tests.', [])
# We never populate anything, when acting as a source
source.populate(zone, target=source)
self.assertEquals(0, len(zone.records))
# Same if we're acting as a target
source.populate(zone)
self.assertEquals(0, len(zone.records))
record = Record.new(zone, 'start', {
'ttl': 60,
'type': 'CNAME',
'value': 'middle.unit.tests.',
})
zone.add_record(record)
record = Record.new(zone, 'middle', {
'ttl': 60,
'type': 'CNAME',
'value': 'loop.unit.tests.',
})
zone.add_record(record)
record = Record.new(zone, 'loop', {
'ttl': 60,
'type': 'CNAME',
'value': 'start.unit.tests.',
})
zone.add_record(record)
with TemporaryDirectory() as td:
# Add some subdirs to make sure that it can create them
directory = path.join(td.dirname, 'sub', 'dir')
hosts_file = path.join(directory, 'unit.tests.hosts')
target = EtcHostsProvider('test', directory)
# We add everything
plan = target.plan(zone)
self.assertEquals(len(zone.records), len(plan.changes))
self.assertFalse(isfile(hosts_file))
# Now actually do it
self.assertEquals(len(zone.records), target.apply(plan))
self.assertTrue(isfile(hosts_file))
with open(hosts_file) as fh:
data = fh.read()
self.assertTrue('# loop.unit.tests -> start.unit.tests '
'**loop**' in data)
self.assertTrue('# middle.unit.tests -> loop.unit.tests '
'**loop**' in data)
self.assertTrue('# start.unit.tests -> middle.unit.tests '
'**loop**' in data)
def test_missing(self):
with self.assertRaises(ModuleNotFoundError):
from octodns.provider.etc_hosts import EtcHostsProvider
EtcHostsProvider

Loading…
Cancel
Save