mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge remote-tracking branch 'origin/master' into show-zone-create
This commit is contained in:
@@ -60,7 +60,7 @@ better in the future :fingers_crossed:
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
* Use a 3rd party lib for nautrual sorting of keys, rather than my old
|
||||
* Use a 3rd party lib for natural sorting of keys, rather than my old
|
||||
implementation. Sorting can be disabled in the YamlProvider with
|
||||
`enforce_order: False`.
|
||||
* Semi-colon/escaping fixes and improvements.
|
||||
|
||||
@@ -150,8 +150,9 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| Provider | Record Support | GeoDNS Support | Notes |
|
||||
|--|--|--|--|
|
||||
| [AzureProvider](/octodns/provider/azuredns.py) | A, AAAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | A, AAAA, ALIAS, CAA, CNAME, MX, NS, SPF, TXT | No | CAA tags restricted |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | A, AAAA, ALIAS, CAA, CNAME, MX, NS, SPF, SRV, TXT | No | 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, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [DnsimpleProvider](/octodns/provider/dnsimple.py) | All | No | CAA tags restricted |
|
||||
| [DynProvider](/octodns/provider/dyn.py) | All | Yes | |
|
||||
| [GoogleCloudProvider](/octodns/provider/googlecloud.py) | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | No | |
|
||||
@@ -168,6 +169,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
* ALIAS support varies a lot from provider to provider care should be taken to verify that your needs are met in detail.
|
||||
* Dyn's UI doesn't allow editing or view of TTL, but the API accepts and stores the value provided, this value does not appear to be used when served
|
||||
* Dnsimple's uses the configured TTL when serving things through the ALIAS, there's also a secondary TXT record created alongside the ALIAS that octoDNS ignores
|
||||
* octoDNS itself supports non-ASCII character sets, but in testing Cloudflare is the only provider where that is currently functional end-to-end. Others have failures either in the client libraries or API calls
|
||||
|
||||
## Custom Sources and Providers
|
||||
|
||||
|
||||
@@ -53,11 +53,11 @@ The geo labels breakdown based on:
|
||||
|
||||
1.
|
||||
- 'AF': 14, # Continental Africa
|
||||
- 'AN': 17, # Continental Antartica
|
||||
- 'AS': 15, # Contentinal Asia
|
||||
- 'EU': 13, # Contentinal Europe
|
||||
- 'AN': 17, # Continental Antarctica
|
||||
- 'AS': 15, # Continental Asia
|
||||
- 'EU': 13, # Continental Europe
|
||||
- 'NA': 11, # Continental North America
|
||||
- 'OC': 16, # Contentinal Austrailia/Oceania
|
||||
- 'OC': 16, # Continental Australia/Oceania
|
||||
- 'SA': 12, # Continental South America
|
||||
|
||||
2. ISO Country Code https://en.wikipedia.org/wiki/ISO_3166-2
|
||||
|
||||
@@ -65,7 +65,7 @@ def main():
|
||||
resolver = AsyncResolver(configure=False,
|
||||
num_workers=int(args.num_workers))
|
||||
if not ip_addr_re.match(server):
|
||||
server = str(query(server, 'A')[0])
|
||||
server = unicode(query(server, 'A')[0])
|
||||
log.info('server=%s', server)
|
||||
resolver.nameservers = [server]
|
||||
resolver.lifetime = int(args.timeout)
|
||||
@@ -81,12 +81,12 @@ def main():
|
||||
stdout.write(',')
|
||||
stdout.write(record._type)
|
||||
stdout.write(',')
|
||||
stdout.write(str(record.ttl))
|
||||
stdout.write(unicode(record.ttl))
|
||||
compare = {}
|
||||
for future in futures:
|
||||
stdout.write(',')
|
||||
try:
|
||||
answers = [str(r) for r in future.result()]
|
||||
answers = [unicode(r) for r in future.result()]
|
||||
except (NoAnswer, NoNameservers):
|
||||
answers = ['*no answer*']
|
||||
except NXDOMAIN:
|
||||
|
||||
@@ -26,7 +26,7 @@ def main():
|
||||
help='Limit sync to the specified zone(s)')
|
||||
|
||||
# --sources isn't an option here b/c filtering sources out would be super
|
||||
# dangerous since you could eaily end up with an empty zone and delete
|
||||
# dangerous since you could easily end up with an empty zone and delete
|
||||
# everything, or even just part of things when there are multiple sources
|
||||
|
||||
parser.add_argument('--target', default=[], action='append',
|
||||
|
||||
@@ -51,7 +51,7 @@ class MakeThreadFuture(object):
|
||||
|
||||
class MainThreadExecutor(object):
|
||||
'''
|
||||
Dummy executor that runs things on the main thread during the involcation
|
||||
Dummy executor that runs things on the main thread during the invocation
|
||||
of submit, but still returns a future object with the result. This allows
|
||||
code to be written to handle async, even in the case where we don't want to
|
||||
use multiple threads/workers and would prefer that things flow as if
|
||||
|
||||
@@ -39,7 +39,7 @@ class _AzureRecord(object):
|
||||
}
|
||||
|
||||
def __init__(self, resource_group, record, delete=False):
|
||||
'''Contructor for _AzureRecord.
|
||||
'''Constructor for _AzureRecord.
|
||||
|
||||
Notes on Azure records: An Azure record set has the form
|
||||
RecordSet(name=<...>, type=<...>, arecords=[...], aaaa_records, ..)
|
||||
@@ -222,7 +222,7 @@ class AzureProvider(BaseProvider):
|
||||
azuredns:
|
||||
class: octodns.provider.azuredns.AzureProvider
|
||||
client_id: env/AZURE_APPLICATION_ID
|
||||
key: env/AZURE_AUTHENICATION_KEY
|
||||
key: env/AZURE_AUTHENTICATION_KEY
|
||||
directory_id: env/AZURE_DIRECTORY_ID
|
||||
sub_id: env/AZURE_SUBSCRIPTION_ID
|
||||
resource_group: 'TestResource1'
|
||||
|
||||
@@ -17,7 +17,8 @@ class BaseProvider(BaseSource):
|
||||
delete_pcent_threshold=Plan.MAX_SAFE_DELETE_PCENT):
|
||||
super(BaseProvider, self).__init__(id)
|
||||
self.log.debug('__init__: id=%s, apply_disabled=%s, '
|
||||
'update_pcent_threshold=%d, delete_pcent_threshold=%d',
|
||||
'update_pcent_threshold=%.2f'
|
||||
'delete_pcent_threshold=%.2f',
|
||||
id,
|
||||
apply_disabled,
|
||||
update_pcent_threshold,
|
||||
@@ -29,14 +30,14 @@ class BaseProvider(BaseSource):
|
||||
def _include_change(self, change):
|
||||
'''
|
||||
An opportunity for providers to filter out false positives due to
|
||||
pecularities in their implementation. E.g. minimum TTLs.
|
||||
peculiarities in their implementation. E.g. minimum TTLs.
|
||||
'''
|
||||
return True
|
||||
|
||||
def _extra_changes(self, existing, changes):
|
||||
'''
|
||||
An opportunity for providers to add extra changes to the plan that are
|
||||
necessary to update ancilary record data or configure the zone. E.g.
|
||||
necessary to update ancillary record data or configure the zone. E.g.
|
||||
base NS records.
|
||||
'''
|
||||
return []
|
||||
@@ -66,7 +67,7 @@ class BaseProvider(BaseSource):
|
||||
extra = self._extra_changes(existing, changes)
|
||||
if extra:
|
||||
self.log.info('plan: extra changes\n %s', '\n '
|
||||
.join([str(c) for c in extra]))
|
||||
.join([unicode(c) for c in extra]))
|
||||
changes += extra
|
||||
|
||||
if changes:
|
||||
|
||||
@@ -14,14 +14,18 @@ from ..record import Record, Update
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
class CloudflareAuthenticationError(Exception):
|
||||
|
||||
class CloudflareError(Exception):
|
||||
def __init__(self, data):
|
||||
try:
|
||||
message = data['errors'][0]['message']
|
||||
except (IndexError, KeyError):
|
||||
message = 'Authentication error'
|
||||
super(CloudflareAuthenticationError, self).__init__(message)
|
||||
message = 'Cloudflare error'
|
||||
super(CloudflareError, self).__init__(message)
|
||||
|
||||
|
||||
class CloudflareAuthenticationError(CloudflareError):
|
||||
def __init__(self, data):
|
||||
CloudflareError.__init__(self, data)
|
||||
|
||||
|
||||
class CloudflareProvider(BaseProvider):
|
||||
@@ -34,18 +38,24 @@ class CloudflareProvider(BaseProvider):
|
||||
email: dns-manager@example.com
|
||||
# The api key (required)
|
||||
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
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
# TODO: support SRV
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SPF',
|
||||
'TXT'))
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SRV',
|
||||
'SPF', 'TXT'))
|
||||
|
||||
MIN_TTL = 120
|
||||
TIMEOUT = 15
|
||||
|
||||
def __init__(self, id, email, token, *args, **kwargs):
|
||||
def __init__(self, id, email, token, cdn=False, *args, **kwargs):
|
||||
self.log = getLogger('CloudflareProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, email=%s, token=***', id, email)
|
||||
self.log.debug('__init__: id=%s, email=%s, token=***, cdn=%s', id,
|
||||
email, cdn)
|
||||
super(CloudflareProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
sess = Session()
|
||||
@@ -53,6 +63,7 @@ class CloudflareProvider(BaseProvider):
|
||||
'X-Auth-Email': email,
|
||||
'X-Auth-Key': token,
|
||||
})
|
||||
self.cdn = cdn
|
||||
self._sess = sess
|
||||
|
||||
self._zones = None
|
||||
@@ -65,8 +76,11 @@ class CloudflareProvider(BaseProvider):
|
||||
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:
|
||||
raise CloudflareError(resp.json())
|
||||
if resp.status_code == 403:
|
||||
raise CloudflareAuthenticationError(resp.json())
|
||||
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
@@ -88,6 +102,18 @@ class CloudflareProvider(BaseProvider):
|
||||
|
||||
return self._zones
|
||||
|
||||
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': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'value': '{}.cdn.cloudflare.net.'.format(records[0]['name']),
|
||||
}
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
@@ -147,6 +173,21 @@ class CloudflareProvider(BaseProvider):
|
||||
'values': ['{}.'.format(r['content']) for r in records],
|
||||
}
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
for r in records:
|
||||
values.append({
|
||||
'priority': r['data']['priority'],
|
||||
'weight': r['data']['weight'],
|
||||
'port': r['data']['port'],
|
||||
'target': '{}.'.format(r['data']['target']),
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records[0]['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def zone_records(self, zone):
|
||||
if zone.name not in self._zone_records:
|
||||
zone_id = self.zones.get(zone.name, False)
|
||||
@@ -169,6 +210,20 @@ class CloudflareProvider(BaseProvider):
|
||||
|
||||
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, '_data_for_{}'.format(_type))
|
||||
data = data_for(_type, records)
|
||||
|
||||
return Record.new(zone, name, data, source=self, lenient=lenient)
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
@@ -187,15 +242,17 @@ class CloudflareProvider(BaseProvider):
|
||||
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
record = self._record_for(zone, name, _type, records,
|
||||
lenient)
|
||||
|
||||
# Cloudflare supports ALIAS semantics with root CNAMEs
|
||||
if _type == 'CNAME' and name == '':
|
||||
_type = 'ALIAS'
|
||||
# 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
|
||||
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
data = data_for(_type, records)
|
||||
record = Record.new(zone, name, data, source=self,
|
||||
lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
@@ -206,9 +263,16 @@ class CloudflareProvider(BaseProvider):
|
||||
if isinstance(change, Update):
|
||||
existing = change.existing.data
|
||||
new = change.new.data
|
||||
new['ttl'] = max(120, new['ttl'])
|
||||
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):
|
||||
@@ -244,6 +308,21 @@ class CloudflareProvider(BaseProvider):
|
||||
'content': value.exchange
|
||||
}
|
||||
|
||||
def _contents_for_SRV(self, record):
|
||||
service, proto = record.name.split('.', 2)
|
||||
for value in record.values:
|
||||
yield {
|
||||
'data': {
|
||||
'service': service,
|
||||
'proto': proto,
|
||||
'name': record.zone.name,
|
||||
'priority': value.priority,
|
||||
'weight': value.weight,
|
||||
'port': value.port,
|
||||
'target': value.target[:-1],
|
||||
}
|
||||
}
|
||||
|
||||
def _gen_contents(self, record):
|
||||
name = record.fqdn[:-1]
|
||||
_type = record._type
|
||||
@@ -299,10 +378,6 @@ class CloudflareProvider(BaseProvider):
|
||||
for c in self._gen_contents(change.new)
|
||||
}
|
||||
|
||||
# We need a list of keys to consider for diffs, use the first content
|
||||
# before we muck with anything
|
||||
keys = existing_contents.values()[0].keys()
|
||||
|
||||
# Find the things we need to add
|
||||
adds = []
|
||||
for k, content in new_contents.items():
|
||||
@@ -312,22 +387,25 @@ class CloudflareProvider(BaseProvider):
|
||||
except KeyError:
|
||||
adds.append(content)
|
||||
|
||||
zone_id = self.zones[change.new.zone.name]
|
||||
zone = change.new.zone
|
||||
zone_id = self.zones[zone.name]
|
||||
|
||||
# Find things we need to remove
|
||||
name = change.new.fqdn[:-1]
|
||||
hostname = zone.hostname_from_fqdn(change.new.fqdn[:-1])
|
||||
_type = change.new._type
|
||||
# OK, work through each record from the zone
|
||||
for record in self.zone_records(change.new.zone):
|
||||
if name == record['name'] and _type == record['type']:
|
||||
# This is match for our name and type, we need to look at
|
||||
# contents now, build a dict of the relevant keys and vals
|
||||
content = {}
|
||||
for k in keys:
|
||||
content[k] = record[k]
|
||||
# :-(
|
||||
if _type in ('CNAME', 'MX', 'NS'):
|
||||
content['content'] += '.'
|
||||
for record in self.zone_records(zone):
|
||||
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_contents result that matches what
|
||||
# went in to new_contents
|
||||
content = self._gen_contents(r).next()
|
||||
|
||||
# If the hash of that dict isn't in new this record isn't
|
||||
# needed
|
||||
if self._hash_content(content) not in new_contents:
|
||||
|
||||
@@ -56,7 +56,7 @@ class DigitalOceanClient(object):
|
||||
self._request('POST', '/domains', data={'name': name,
|
||||
'ip_address': '192.0.2.1'})
|
||||
|
||||
# After the zone is created, immeadiately delete the record
|
||||
# After the zone is created, immediately delete the record
|
||||
records = self.records(name)
|
||||
for record in records:
|
||||
if record['name'] == '' and record['type'] == 'A':
|
||||
|
||||
@@ -160,7 +160,7 @@ class DnsimpleProvider(BaseProvider):
|
||||
record['content'].split(' ', 5)
|
||||
except ValueError:
|
||||
# their api will let you create invalid records, this
|
||||
# essnetially handles that by ignoring them for values
|
||||
# essentially handles that by ignoring them for values
|
||||
# purposes. That will cause updates to happen to delete them if
|
||||
# they shouldn't exist or update them if they're wrong
|
||||
continue
|
||||
|
||||
382
octodns/provider/dnsmadeeasy.py
Normal file
382
octodns/provider/dnsmadeeasy.py
Normal file
@@ -0,0 +1,382 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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 .base import BaseProvider
|
||||
|
||||
|
||||
class DnsMadeEasyClientException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DnsMadeEasyClientBadRequest(DnsMadeEasyClientException):
|
||||
|
||||
def __init__(self, resp):
|
||||
errors = resp.json()['error']
|
||||
super(DnsMadeEasyClientBadRequest, self).__init__(
|
||||
'\n - {}'.format('\n - '.join(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 = '{}{}'.format(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 = {'{}.'.format(z['name']): z['id'] for z in zones}
|
||||
|
||||
return self._domains
|
||||
|
||||
def domain(self, name):
|
||||
path = '/id/{}'.format(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 = '/{}/records'.format(zone_id)
|
||||
ret = []
|
||||
|
||||
# has pages in resp, do we need paging?
|
||||
resp = self._request('GET', path).json()
|
||||
ret += resp['data']
|
||||
|
||||
# change relative values to absolute
|
||||
for record in ret:
|
||||
value = record['value']
|
||||
if record['type'] in ['CNAME', 'MX', 'NS', 'SRV']:
|
||||
if value == '':
|
||||
record['value'] = zone_name
|
||||
elif not value.endswith('.'):
|
||||
record['value'] = '{}.{}'.format(value, zone_name)
|
||||
|
||||
return ret
|
||||
|
||||
def record_create(self, zone_name, params):
|
||||
zone_id = self.domains.get(zone_name, False)
|
||||
path = '/{}/records'.format(zone_id)
|
||||
|
||||
self._request('POST', path, data=params)
|
||||
|
||||
def record_delete(self, zone_name, record_id):
|
||||
zone_id = self.domains.get(zone_name, False)
|
||||
path = '/{}/records/{}'.format(zone_id, 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 = set(('A', 'AAAA', '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('DnsMadeEasyProvider[{}]'.format(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
|
||||
|
||||
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']
|
||||
values[record['name']][record['type']].append(record)
|
||||
|
||||
before = len(zone.records)
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
record = Record.new(zone, name, data_for(_type, records),
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
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 {
|
||||
'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
|
||||
|
||||
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, '_params_for_{}'.format(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, '_apply_{}'.format(class_name))(change)
|
||||
|
||||
# Clear out the cache if any
|
||||
self._zone_records.pop(desired.name, None)
|
||||
@@ -40,7 +40,7 @@ class _CachingDynZone(DynZone):
|
||||
cls.log.debug('get: fetched')
|
||||
except DynectGetError:
|
||||
if not create:
|
||||
cls.log.debug("get: does't exist")
|
||||
cls.log.debug("get: doesn't exist")
|
||||
return None
|
||||
# this value shouldn't really matter, it's not tied to
|
||||
# whois or anything
|
||||
@@ -129,11 +129,11 @@ class DynProvider(BaseProvider):
|
||||
REGION_CODES = {
|
||||
'NA': 11, # Continental North America
|
||||
'SA': 12, # Continental South America
|
||||
'EU': 13, # Contentinal Europe
|
||||
'EU': 13, # Continental Europe
|
||||
'AF': 14, # Continental Africa
|
||||
'AS': 15, # Contentinal Asia
|
||||
'OC': 16, # Contentinal Austrailia/Oceania
|
||||
'AN': 17, # Continental Antartica
|
||||
'AS': 15, # Continental Asia
|
||||
'OC': 16, # Continental Australia/Oceania
|
||||
'AN': 17, # Continental Antarctica
|
||||
}
|
||||
|
||||
_sess_create_lock = Lock()
|
||||
@@ -166,7 +166,7 @@ class DynProvider(BaseProvider):
|
||||
if DynectSession.get_session() is None:
|
||||
# We need to create a new session for this thread and DynectSession
|
||||
# creation is not thread-safe so we have to do the locking. If we
|
||||
# don't and multiple sessions start creattion before the the first
|
||||
# don't and multiple sessions start creation before the the first
|
||||
# has finished (long time b/c it makes http calls) the subsequent
|
||||
# creates will blow away DynectSession._instances, potentially
|
||||
# multiple times if there are multiple creates in flight. Only the
|
||||
@@ -291,7 +291,7 @@ class DynProvider(BaseProvider):
|
||||
try:
|
||||
fqdn, _type = td.label.split(':', 1)
|
||||
except ValueError as e:
|
||||
self.log.warn("Failed to load TraficDirector '%s': %s",
|
||||
self.log.warn("Failed to load TrafficDirector '%s': %s",
|
||||
td.label, e.message)
|
||||
continue
|
||||
tds[fqdn][_type] = td
|
||||
|
||||
@@ -127,9 +127,10 @@ class GoogleCloudProvider(BaseProvider):
|
||||
:type return: new google.cloud.dns.ManagedZone
|
||||
"""
|
||||
# Zone name must begin with a letter, end with a letter or digit,
|
||||
# and only contain lowercase letters, digits or dashes
|
||||
zone_name = '{}-{}'.format(
|
||||
dns_name[:-1].replace('.', '-'), uuid4().hex)
|
||||
# and only contain lowercase letters, digits or dashes,
|
||||
# and be 63 characters or less
|
||||
zone_name = 'zone-{}-{}'.format(
|
||||
dns_name.replace('.', '-'), uuid4().hex)[:63]
|
||||
|
||||
gcloud_zone = self.gcloud_client.zone(
|
||||
name=zone_name,
|
||||
|
||||
@@ -75,9 +75,9 @@ class Ns1Provider(BaseProvider):
|
||||
else:
|
||||
values.extend(answer['answer'])
|
||||
codes.append([])
|
||||
values = [str(x) for x in values]
|
||||
values = [unicode(x) for x in values]
|
||||
geo = OrderedDict(
|
||||
{str(k): [str(x) for x in v] for k, v in geo.items()}
|
||||
{unicode(k): [unicode(x) for x in v] for k, v in geo.items()}
|
||||
)
|
||||
data['values'] = values
|
||||
data['geo'] = geo
|
||||
@@ -236,11 +236,10 @@ class Ns1Provider(BaseProvider):
|
||||
},
|
||||
)
|
||||
params['filters'] = []
|
||||
if len(params['answers']) > 1:
|
||||
if has_country:
|
||||
params['filters'].append(
|
||||
{"filter": "shuffle", "config": {}}
|
||||
)
|
||||
if has_country:
|
||||
params['filters'].append(
|
||||
{"filter": "geotarget_country", "config": {}}
|
||||
)
|
||||
|
||||
@@ -269,10 +269,11 @@ class OvhProvider(BaseProvider):
|
||||
def _params_for_SRV(record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'subDomain': '{} {} {} {}'.format(value.priority,
|
||||
value.weight, value.port,
|
||||
value.target),
|
||||
'target': record.name,
|
||||
'target': '{} {} {} {}'.format(value.priority,
|
||||
value.weight,
|
||||
value.port,
|
||||
value.target),
|
||||
'subDomain': record.name,
|
||||
'ttl': record.ttl,
|
||||
'fieldType': record._type
|
||||
}
|
||||
@@ -281,10 +282,10 @@ class OvhProvider(BaseProvider):
|
||||
def _params_for_SSHFP(record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'subDomain': '{} {} {}'.format(value.algorithm,
|
||||
value.fingerprint_type,
|
||||
value.fingerprint),
|
||||
'target': record.name,
|
||||
'target': '{} {} {}'.format(value.algorithm,
|
||||
value.fingerprint_type,
|
||||
value.fingerprint),
|
||||
'subDomain': record.name,
|
||||
'ttl': record.ttl,
|
||||
'fieldType': record._type
|
||||
}
|
||||
|
||||
@@ -61,17 +61,17 @@ class Plan(object):
|
||||
delete_pcent = self.change_counts['Delete'] / existing_record_count
|
||||
|
||||
if update_pcent > self.update_pcent_threshold:
|
||||
raise UnsafePlan('Too many updates, {} is over {} percent'
|
||||
raise UnsafePlan('Too many updates, {:.2f} is over {:.2f} %'
|
||||
'({}/{})'.format(
|
||||
update_pcent,
|
||||
self.MAX_SAFE_UPDATE_PCENT * 100,
|
||||
update_pcent * 100,
|
||||
self.update_pcent_threshold * 100,
|
||||
self.change_counts['Update'],
|
||||
existing_record_count))
|
||||
if delete_pcent > self.delete_pcent_threshold:
|
||||
raise UnsafePlan('Too many deletes, {} is over {} percent'
|
||||
raise UnsafePlan('Too many deletes, {:.2f} is over {:.2f} %'
|
||||
'({}/{})'.format(
|
||||
delete_pcent,
|
||||
self.MAX_SAFE_DELETE_PCENT * 100,
|
||||
delete_pcent * 100,
|
||||
self.delete_pcent_threshold * 100,
|
||||
self.change_counts['Delete'],
|
||||
existing_record_count))
|
||||
|
||||
@@ -147,11 +147,11 @@ class PlanLogger(_PlanOutput):
|
||||
|
||||
def _value_stringifier(record, sep):
|
||||
try:
|
||||
values = [str(v) for v in record.values]
|
||||
values = [unicode(v) for v in record.values]
|
||||
except AttributeError:
|
||||
values = [record.value]
|
||||
for code, gv in sorted(getattr(record, 'geo', {}).items()):
|
||||
vs = ', '.join([str(v) for v in gv.values])
|
||||
vs = ', '.join([unicode(v) for v in gv.values])
|
||||
values.append('{}: {}'.format(code, vs))
|
||||
return sep.join(values)
|
||||
|
||||
@@ -193,7 +193,7 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write(' | ')
|
||||
# TTL
|
||||
if existing:
|
||||
fh.write(str(existing.ttl))
|
||||
fh.write(unicode(existing.ttl))
|
||||
fh.write(' | ')
|
||||
fh.write(_value_stringifier(existing, '; '))
|
||||
fh.write(' | |\n')
|
||||
@@ -201,7 +201,7 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write('| | | | ')
|
||||
|
||||
if new:
|
||||
fh.write(str(new.ttl))
|
||||
fh.write(unicode(new.ttl))
|
||||
fh.write(' | ')
|
||||
fh.write(_value_stringifier(new, '; '))
|
||||
fh.write(' | ')
|
||||
@@ -210,7 +210,7 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write(' |\n')
|
||||
|
||||
fh.write('\nSummary: ')
|
||||
fh.write(str(plan))
|
||||
fh.write(unicode(plan))
|
||||
fh.write('\n\n')
|
||||
else:
|
||||
fh.write('## No changes were planned\n')
|
||||
@@ -261,7 +261,7 @@ class PlanHtml(_PlanOutput):
|
||||
# TTL
|
||||
if existing:
|
||||
fh.write(' <td>')
|
||||
fh.write(str(existing.ttl))
|
||||
fh.write(unicode(existing.ttl))
|
||||
fh.write('</td>\n <td>')
|
||||
fh.write(_value_stringifier(existing, '<br/>'))
|
||||
fh.write('</td>\n <td></td>\n </tr>\n')
|
||||
@@ -270,7 +270,7 @@ class PlanHtml(_PlanOutput):
|
||||
|
||||
if new:
|
||||
fh.write(' <td>')
|
||||
fh.write(str(new.ttl))
|
||||
fh.write(unicode(new.ttl))
|
||||
fh.write('</td>\n <td>')
|
||||
fh.write(_value_stringifier(new, '<br/>'))
|
||||
fh.write('</td>\n <td>')
|
||||
@@ -279,7 +279,7 @@ class PlanHtml(_PlanOutput):
|
||||
fh.write('</td>\n </tr>\n')
|
||||
|
||||
fh.write(' <tr>\n <td colspan=6>Summary: ')
|
||||
fh.write(str(plan))
|
||||
fh.write(unicode(plan))
|
||||
fh.write('</td>\n </tr>\n</table>\n')
|
||||
else:
|
||||
fh.write('<b>No changes were planned</b>')
|
||||
|
||||
@@ -178,7 +178,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
raise Exception('PowerDNS unauthorized host={}'
|
||||
.format(self.host))
|
||||
elif e.response.status_code == 422:
|
||||
# 422 means powerdns doesn't know anything about the requsted
|
||||
# 422 means powerdns doesn't know anything about the requested
|
||||
# domain. We'll just ignore it here and leave the zone
|
||||
# untouched.
|
||||
pass
|
||||
@@ -297,8 +297,8 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
return []
|
||||
|
||||
# sorting mostly to make things deterministic for testing, but in
|
||||
# theory it let us find what we're after quickier (though sorting would
|
||||
# ve more exepensive.)
|
||||
# theory it let us find what we're after quicker (though sorting would
|
||||
# be more expensive.)
|
||||
for record in sorted(existing.records):
|
||||
if record == ns:
|
||||
# We've found the top-level NS record, return any changes
|
||||
@@ -344,7 +344,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
e.response.text)
|
||||
raise
|
||||
self.log.info('_apply: creating zone=%s', desired.name)
|
||||
# 422 means powerdns doesn't know anything about the requsted
|
||||
# 422 means powerdns doesn't know anything about the requested
|
||||
# domain. We'll try to create it with the correct records instead
|
||||
# of update. Hopefully all the mods are creates :-)
|
||||
data = {
|
||||
|
||||
@@ -130,17 +130,9 @@ class RackspaceProvider(BaseProvider):
|
||||
def _delete(self, path, data=None):
|
||||
return self._request('DELETE', path, data=data)
|
||||
|
||||
@staticmethod
|
||||
def _as_unicode(s, codec):
|
||||
if not isinstance(s, unicode):
|
||||
return unicode(s, codec)
|
||||
return s
|
||||
|
||||
@classmethod
|
||||
def _key_for_record(cls, rs_record):
|
||||
return cls._as_unicode(rs_record['type'], 'ascii'), \
|
||||
cls._as_unicode(rs_record['name'], 'utf-8'), \
|
||||
cls._as_unicode(rs_record['data'], 'utf-8')
|
||||
return rs_record['type'], rs_record['name'], rs_record['data']
|
||||
|
||||
def _data_for_multiple(self, rrset):
|
||||
return {
|
||||
|
||||
@@ -61,7 +61,7 @@ class _Route53Record(object):
|
||||
|
||||
# NOTE: we're using __hash__ and __cmp__ methods that consider
|
||||
# _Route53Records equivalent if they have the same class, fqdn, and _type.
|
||||
# Values are ignored. This is usful when computing diffs/changes.
|
||||
# Values are ignored. This is useful when computing diffs/changes.
|
||||
|
||||
def __hash__(self):
|
||||
'sub-classes should never use this method'
|
||||
@@ -682,7 +682,7 @@ class Route53Provider(BaseProvider):
|
||||
.get('CountryCode', False) == '*':
|
||||
# it's a default record
|
||||
continue
|
||||
# we expect a healtcheck now
|
||||
# we expect a healthcheck now
|
||||
try:
|
||||
health_check_id = rrset['HealthCheckId']
|
||||
caller_ref = \
|
||||
@@ -733,7 +733,7 @@ class Route53Provider(BaseProvider):
|
||||
batch_rs_count)
|
||||
# send the batch
|
||||
self._really_apply(batch, zone_id)
|
||||
# start a new batch with the lefovers
|
||||
# start a new batch with the leftovers
|
||||
batch = mods
|
||||
batch_rs_count = mods_rs_count
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class Record(object):
|
||||
self.__class__.__name__, name)
|
||||
self.zone = zone
|
||||
# force everything lower-case just to be safe
|
||||
self.name = str(name).lower() if name else name
|
||||
self.name = unicode(name).lower() if name else name
|
||||
self.source = source
|
||||
self.ttl = int(data['ttl'])
|
||||
|
||||
@@ -151,7 +151,7 @@ class Record(object):
|
||||
|
||||
# NOTE: we're using __hash__ and __cmp__ methods that consider Records
|
||||
# equivalent if they have the same name & _type. Values are ignored. This
|
||||
# is usful when computing diffs/changes.
|
||||
# is useful when computing diffs/changes.
|
||||
|
||||
def __hash__(self):
|
||||
return '{}:{}'.format(self.name, self._type).__hash__()
|
||||
@@ -274,7 +274,8 @@ class _ValuesMixin(object):
|
||||
return ret
|
||||
|
||||
def __repr__(self):
|
||||
values = "['{}']".format("', '".join([str(v) for v in self.values]))
|
||||
values = "['{}']".format("', '".join([unicode(v)
|
||||
for v in self.values]))
|
||||
return '<{} {} {}, {}, {}>'.format(self.__class__.__name__,
|
||||
self._type, self.ttl,
|
||||
self.fqdn, values)
|
||||
@@ -678,8 +679,8 @@ class PtrRecord(_ValueMixin, Record):
|
||||
|
||||
|
||||
class SshfpValue(object):
|
||||
VALID_ALGORITHMS = (1, 2)
|
||||
VALID_FINGERPRINT_TYPES = (1,)
|
||||
VALID_ALGORITHMS = (1, 2, 3)
|
||||
VALID_FINGERPRINT_TYPES = (1, 2)
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
|
||||
@@ -37,10 +37,10 @@ class Zone(object):
|
||||
if not name[-1] == '.':
|
||||
raise Exception('Invalid zone name {}, missing ending dot'
|
||||
.format(name))
|
||||
# Force everyting to lowercase just to be safe
|
||||
self.name = str(name).lower() if name else name
|
||||
# Force everything to lowercase just to be safe
|
||||
self.name = unicode(name).lower() if name else name
|
||||
self.sub_zones = sub_zones
|
||||
# We're grouping by node, it allows us to efficently search for
|
||||
# We're grouping by node, it allows us to efficiently search for
|
||||
# duplicates and detect when CNAMEs co-exist with other records
|
||||
self._records = defaultdict(set)
|
||||
# optional leading . to match empty hostname
|
||||
|
||||
@@ -11,4 +11,4 @@ git tag -s v$VERSION -m "Release $VERSION"
|
||||
git push origin v$VERSION
|
||||
echo "Tagged and pushed v$VERSION"
|
||||
python setup.py sdist upload
|
||||
echo "Updloaded $VERSION"
|
||||
echo "Uploaded $VERSION"
|
||||
|
||||
@@ -156,14 +156,64 @@
|
||||
"meta": {
|
||||
"auto_added": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
],
|
||||
"result_info": {
|
||||
"page": 2,
|
||||
"per_page": 10,
|
||||
"per_page": 11,
|
||||
"total_pages": 2,
|
||||
"count": 9,
|
||||
"total_count": 19
|
||||
"total_count": 21
|
||||
},
|
||||
"success": true,
|
||||
"errors": [],
|
||||
|
||||
16
tests/fixtures/dnsmadeeasy-domains.json
vendored
Normal file
16
tests/fixtures/dnsmadeeasy-domains.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
312
tests/fixtures/dnsmadeeasy-records.json
vendored
Normal file
312
tests/fixtures/dnsmadeeasy-records.json
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
{
|
||||
"totalPages": 1,
|
||||
"totalRecords": 21,
|
||||
"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"
|
||||
}],
|
||||
"page": 0
|
||||
}
|
||||
@@ -63,14 +63,14 @@ class TestBaseProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupportsGeo('hassupportesgeo').populate(zone)
|
||||
HasSupportsGeo('hassupportsgeo').populate(zone)
|
||||
self.assertEquals('Abstract base class, SUPPORTS property missing',
|
||||
ctx.exception.message)
|
||||
|
||||
class HasSupports(HasSupportsGeo):
|
||||
SUPPORTS = set(('A',))
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupports('hassupportes').populate(zone)
|
||||
HasSupports('hassupports').populate(zone)
|
||||
self.assertEquals('Abstract base class, populate method missing',
|
||||
ctx.exception.message)
|
||||
|
||||
@@ -94,7 +94,7 @@ class TestBaseProvider(TestCase):
|
||||
'value': '1.2.3.4'
|
||||
}))
|
||||
|
||||
self.assertTrue(HasSupports('hassupportesgeo')
|
||||
self.assertTrue(HasSupports('hassupportsgeo')
|
||||
.supports(list(zone.records)[0]))
|
||||
|
||||
plan = HasPopulate('haspopulate').plan(zone)
|
||||
@@ -178,7 +178,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, str(i), {
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -210,7 +210,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, str(i), {
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -236,7 +236,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, str(i), {
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -258,7 +258,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, str(i), {
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -284,7 +284,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, str(i), {
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -307,7 +307,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, str(i), {
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -335,7 +335,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, str(i), {
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
|
||||
@@ -42,6 +42,20 @@ class TestCloudflareProvider(TestCase):
|
||||
def test_populate(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
|
||||
# Bad requests
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=400,
|
||||
text='{"success":false,"errors":[{"code":1101,'
|
||||
'"message":"request was invalid"}],'
|
||||
'"messages":[],"result":null}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertEquals('CloudflareError', type(ctx.exception).__name__)
|
||||
self.assertEquals('request was invalid', ctx.exception.message)
|
||||
|
||||
# Bad auth
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=403,
|
||||
@@ -52,6 +66,8 @@ class TestCloudflareProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('CloudflareAuthenticationError',
|
||||
type(ctx.exception).__name__)
|
||||
self.assertEquals('Unknown X-Auth-Key or X-Auth-Email',
|
||||
ctx.exception.message)
|
||||
|
||||
@@ -62,7 +78,9 @@ class TestCloudflareProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('Authentication error', ctx.exception.message)
|
||||
self.assertEquals('CloudflareAuthenticationError',
|
||||
type(ctx.exception).__name__)
|
||||
self.assertEquals('Cloudflare error', ctx.exception.message)
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
@@ -119,15 +137,16 @@ class TestCloudflareProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(11, len(zone.records))
|
||||
self.assertEquals(12, len(zone.records))
|
||||
|
||||
changes = self.expected.changes(zone, provider)
|
||||
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# re-populating the same zone/records comes out of cache, no calls
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(11, len(again.records))
|
||||
self.assertEquals(12, len(again.records))
|
||||
|
||||
def test_apply(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
@@ -141,12 +160,12 @@ class TestCloudflareProvider(TestCase):
|
||||
'id': 42,
|
||||
}
|
||||
}, # zone create
|
||||
] + [None] * 18 # individual record creates
|
||||
] + [None] * 20 # individual record creates
|
||||
|
||||
# non-existant zone, create everything
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(11, len(plan.changes))
|
||||
self.assertEquals(11, provider.apply(plan))
|
||||
self.assertEquals(12, len(plan.changes))
|
||||
self.assertEquals(12, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
@@ -172,7 +191,7 @@ class TestCloudflareProvider(TestCase):
|
||||
}),
|
||||
], True)
|
||||
# expected number of total calls
|
||||
self.assertEquals(20, provider._request.call_count)
|
||||
self.assertEquals(22, provider._request.call_count)
|
||||
|
||||
provider._request.reset_mock()
|
||||
|
||||
@@ -486,3 +505,187 @@ class TestCloudflareProvider(TestCase):
|
||||
'ttl': 300,
|
||||
'type': 'CNAME'
|
||||
}, list(contents)[0])
|
||||
|
||||
def test_cdn(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token', True)
|
||||
|
||||
# A CNAME for us to transform to ALIAS
|
||||
provider.zone_records = Mock(return_value=[
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997642",
|
||||
"type": "CNAME",
|
||||
"name": "cname.unit.tests",
|
||||
"content": "www.unit.tests",
|
||||
"proxiable": True,
|
||||
"proxied": True,
|
||||
"ttl": 300,
|
||||
"locked": False,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.420689Z",
|
||||
"created_on": "2017-03-11T18:01:43.420689Z",
|
||||
"meta": {
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997642",
|
||||
"type": "A",
|
||||
"name": "a.unit.tests",
|
||||
"content": "1.1.1.1",
|
||||
"proxiable": True,
|
||||
"proxied": True,
|
||||
"ttl": 300,
|
||||
"locked": False,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.420689Z",
|
||||
"created_on": "2017-03-11T18:01:43.420689Z",
|
||||
"meta": {
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997642",
|
||||
"type": "A",
|
||||
"name": "a.unit.tests",
|
||||
"content": "1.1.1.2",
|
||||
"proxiable": True,
|
||||
"proxied": True,
|
||||
"ttl": 300,
|
||||
"locked": False,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.420689Z",
|
||||
"created_on": "2017-03-11T18:01:43.420689Z",
|
||||
"meta": {
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997642",
|
||||
"type": "A",
|
||||
"name": "multi.unit.tests",
|
||||
"content": "1.1.1.3",
|
||||
"proxiable": True,
|
||||
"proxied": True,
|
||||
"ttl": 300,
|
||||
"locked": False,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.420689Z",
|
||||
"created_on": "2017-03-11T18:01:43.420689Z",
|
||||
"meta": {
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997642",
|
||||
"type": "AAAA",
|
||||
"name": "multi.unit.tests",
|
||||
"content": "::1",
|
||||
"proxiable": True,
|
||||
"proxied": True,
|
||||
"ttl": 300,
|
||||
"locked": False,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.420689Z",
|
||||
"created_on": "2017-03-11T18:01:43.420689Z",
|
||||
"meta": {
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
|
||||
# the two A records get merged into one CNAME record pointing to
|
||||
# the CDN.
|
||||
self.assertEquals(3, len(zone.records))
|
||||
|
||||
record = list(zone.records)[0]
|
||||
self.assertEquals('multi', record.name)
|
||||
self.assertEquals('multi.unit.tests.', record.fqdn)
|
||||
self.assertEquals('CNAME', record._type)
|
||||
self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
|
||||
record = list(zone.records)[1]
|
||||
self.assertEquals('cname', record.name)
|
||||
self.assertEquals('cname.unit.tests.', record.fqdn)
|
||||
self.assertEquals('CNAME', record._type)
|
||||
self.assertEquals('cname.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
|
||||
record = list(zone.records)[2]
|
||||
self.assertEquals('a', record.name)
|
||||
self.assertEquals('a.unit.tests.', record.fqdn)
|
||||
self.assertEquals('CNAME', record._type)
|
||||
self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
|
||||
# CDN enabled records can't be updated, we don't know the real values
|
||||
# never point a Cloudflare record to itself.
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, 'cname', {
|
||||
'ttl': 300,
|
||||
'type': 'CNAME',
|
||||
'value': 'change.unit.tests.cdn.cloudflare.net.'
|
||||
}))
|
||||
wanted.add_record(Record.new(wanted, 'new', {
|
||||
'ttl': 300,
|
||||
'type': 'CNAME',
|
||||
'value': 'new.unit.tests.cdn.cloudflare.net.'
|
||||
}))
|
||||
wanted.add_record(Record.new(wanted, 'created', {
|
||||
'ttl': 300,
|
||||
'type': 'CNAME',
|
||||
'value': 'www.unit.tests.'
|
||||
}))
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
|
||||
def test_cdn_alias(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token', True)
|
||||
|
||||
# A CNAME for us to transform to ALIAS
|
||||
provider.zone_records = Mock(return_value=[
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997642",
|
||||
"type": "CNAME",
|
||||
"name": "unit.tests",
|
||||
"content": "www.unit.tests",
|
||||
"proxiable": True,
|
||||
"proxied": True,
|
||||
"ttl": 300,
|
||||
"locked": False,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.420689Z",
|
||||
"created_on": "2017-03-11T18:01:43.420689Z",
|
||||
"meta": {
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(1, len(zone.records))
|
||||
record = list(zone.records)[0]
|
||||
self.assertEquals('', record.name)
|
||||
self.assertEquals('unit.tests.', record.fqdn)
|
||||
self.assertEquals('ALIAS', record._type)
|
||||
self.assertEquals('unit.tests.cdn.cloudflare.net.', record.value)
|
||||
|
||||
# CDN enabled records can't be updated, we don't know the real values
|
||||
# never point a Cloudflare record to itself.
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, '', {
|
||||
'ttl': 300,
|
||||
'type': 'ALIAS',
|
||||
'value': 'change.unit.tests.cdn.cloudflare.net.'
|
||||
}))
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
self.assertEquals(False, hasattr(plan, 'changes'))
|
||||
|
||||
202
tests/test_octodns_provider_dnsmadeeasy.py
Normal file
202
tests/test_octodns_provider_dnsmadeeasy.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
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.',
|
||||
]
|
||||
}))
|
||||
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', ctx.exception.message)
|
||||
|
||||
# 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',
|
||||
ctx.exception.message)
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=502, text='Things caught fire')
|
||||
|
||||
with self.assertRaises(HTTPError) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existant 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('{}{}'.format(base, '/'), text=fh.read())
|
||||
with open('tests/fixtures/dnsmadeeasy-records.json') as fh:
|
||||
mock.get('{}{}'.format(base, '/123123/records'),
|
||||
text=fh.read())
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(13, 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(13, 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-existant 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) - 5
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
|
||||
provider._client._request.assert_has_calls([
|
||||
# created the domain
|
||||
call('POST', '/', data={'name': 'unit.tests'}),
|
||||
# get all domains to build the cache
|
||||
call('GET', '/'),
|
||||
# created at least one of the record with expected data
|
||||
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(25, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
# delete 1 and update 1
|
||||
provider._client.records = Mock(return_value=[
|
||||
{
|
||||
'id': 11189897,
|
||||
'name': 'www',
|
||||
'value': '1.2.3.4',
|
||||
'ttl': 300,
|
||||
'type': 'A',
|
||||
},
|
||||
{
|
||||
'id': 11189898,
|
||||
'name': 'www',
|
||||
'value': '2.2.3.4',
|
||||
'ttl': 300,
|
||||
'type': 'A',
|
||||
},
|
||||
{
|
||||
'id': 11189899,
|
||||
'name': 'ttl',
|
||||
'value': '3.2.3.4',
|
||||
'ttl': 600,
|
||||
'type': 'A',
|
||||
}
|
||||
])
|
||||
|
||||
# Domain exists, we don't care about return
|
||||
resp.json.side_effect = ['{}']
|
||||
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, 'ttl', {
|
||||
'ttl': 300,
|
||||
'type': 'A',
|
||||
'value': '3.2.3.4'
|
||||
}))
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
self.assertEquals(2, len(plan.changes))
|
||||
self.assertEquals(2, provider.apply(plan))
|
||||
|
||||
# recreate for update, and deletes for the 2 parts of the other
|
||||
provider._client._request.assert_has_calls([
|
||||
call('POST', '/123123/records', data={
|
||||
'value': '3.2.3.4',
|
||||
'type': 'A',
|
||||
'name': 'ttl',
|
||||
'ttl': 300
|
||||
}),
|
||||
call('DELETE', '/123123/records/11189899'),
|
||||
call('DELETE', '/123123/records/11189897'),
|
||||
call('DELETE', '/123123/records/11189898')
|
||||
], any_order=True)
|
||||
@@ -493,7 +493,7 @@ class TestDynProviderGeo(TestCase):
|
||||
traffic_director_response = loads(fh.read())
|
||||
|
||||
@property
|
||||
def traffic_directors_reponse(self):
|
||||
def traffic_directors_response(self):
|
||||
return {
|
||||
'data': [{
|
||||
'active': 'Y',
|
||||
@@ -609,7 +609,7 @@ class TestDynProviderGeo(TestCase):
|
||||
mock.side_effect = [{'data': []}]
|
||||
self.assertEquals({}, provider.traffic_directors)
|
||||
|
||||
# a supported td and an ingored one
|
||||
# a supported td and an ignored one
|
||||
response = {
|
||||
'data': [{
|
||||
'active': 'Y',
|
||||
@@ -652,7 +652,7 @@ class TestDynProviderGeo(TestCase):
|
||||
set(tds.keys()))
|
||||
self.assertEquals(['A'], tds['unit.tests.'].keys())
|
||||
self.assertEquals(['A'], tds['geo.unit.tests.'].keys())
|
||||
provider.log.warn.assert_called_with("Failed to load TraficDirector "
|
||||
provider.log.warn.assert_called_with("Failed to load TrafficDirector "
|
||||
"'%s': %s", 'something else',
|
||||
'need more than 1 value to '
|
||||
'unpack')
|
||||
@@ -760,7 +760,7 @@ class TestDynProviderGeo(TestCase):
|
||||
# only traffic director
|
||||
mock.side_effect = [
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse,
|
||||
self.traffic_directors_response,
|
||||
# get traffic director
|
||||
self.traffic_director_response,
|
||||
# get zone
|
||||
@@ -811,7 +811,7 @@ class TestDynProviderGeo(TestCase):
|
||||
# both traffic director and regular, regular is ignored
|
||||
mock.side_effect = [
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse,
|
||||
self.traffic_directors_response,
|
||||
# get traffic director
|
||||
self.traffic_director_response,
|
||||
# get zone
|
||||
@@ -861,7 +861,7 @@ class TestDynProviderGeo(TestCase):
|
||||
# busted traffic director
|
||||
mock.side_effect = [
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse,
|
||||
self.traffic_directors_response,
|
||||
# get traffic director
|
||||
busted_traffic_director_response,
|
||||
# get zone
|
||||
@@ -934,14 +934,14 @@ class TestDynProviderGeo(TestCase):
|
||||
provider = DynProvider('test', 'cust', 'user', 'pass',
|
||||
traffic_directors_enabled=True)
|
||||
|
||||
# will be tested seperately
|
||||
# will be tested separately
|
||||
provider._mod_rulesets = MagicMock()
|
||||
|
||||
mock.side_effect = [
|
||||
# create traffic director
|
||||
self.traffic_director_response,
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse
|
||||
self.traffic_directors_response
|
||||
]
|
||||
provider._mod_geo_Create(None, Create(self.geo_record))
|
||||
# td now lives in cache
|
||||
|
||||
@@ -364,10 +364,10 @@ class TestGoogleCloudProvider(TestCase):
|
||||
|
||||
# test_zone gets fed the same records as zone does, except it's in
|
||||
# the format returned by google API, so after populate they should look
|
||||
# excactly the same.
|
||||
# exactly the same.
|
||||
self.assertEqual(test_zone.records, zone.records)
|
||||
|
||||
test_zone2 = Zone('nonexistant.zone.', [])
|
||||
test_zone2 = Zone('nonexistent.zone.', [])
|
||||
exists = provider.populate(test_zone2, False, False)
|
||||
self.assertFalse(exists)
|
||||
|
||||
@@ -405,8 +405,8 @@ class TestGoogleCloudProvider(TestCase):
|
||||
provider.gcloud_client.list_zones = Mock(
|
||||
return_value=DummyIterator([]))
|
||||
|
||||
self.assertIsNone(provider.gcloud_zones.get("nonexistant.xone"),
|
||||
msg="Check that nonexistant zones return None when"
|
||||
self.assertIsNone(provider.gcloud_zones.get("nonexistent.zone"),
|
||||
msg="Check that nonexistent zones return None when"
|
||||
"there's no create=True flag")
|
||||
|
||||
def test__get_rrsets(self):
|
||||
@@ -427,7 +427,22 @@ class TestGoogleCloudProvider(TestCase):
|
||||
provider.gcloud_client.list_zones = Mock(
|
||||
return_value=DummyIterator([]))
|
||||
|
||||
mock_zone = provider._create_gcloud_zone("nonexistant.zone.mock")
|
||||
mock_zone = provider._create_gcloud_zone("nonexistent.zone.mock")
|
||||
|
||||
mock_zone.create.assert_called()
|
||||
provider.gcloud_client.zone.assert_called()
|
||||
|
||||
def test__create_zone_ip6_arpa(self):
|
||||
def _create_dummy_zone(name, dns_name):
|
||||
return DummyGoogleCloudZone(name=name, dns_name=dns_name)
|
||||
|
||||
provider = self._get_provider()
|
||||
|
||||
provider.gcloud_client = Mock()
|
||||
provider.gcloud_client.zone = Mock(side_effect=_create_dummy_zone)
|
||||
|
||||
mock_zone = \
|
||||
provider._create_gcloud_zone('0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa')
|
||||
|
||||
self.assertRegexpMatches(mock_zone.name, '^[a-z][a-z0-9-]*[a-z0-9]$')
|
||||
self.assertEqual(len(mock_zone.name), 63)
|
||||
|
||||
@@ -199,14 +199,14 @@ class TestOvhProvider(TestCase):
|
||||
api_record.append({
|
||||
'fieldType': 'SPF',
|
||||
'ttl': 1000,
|
||||
'target': 'v=spf1 include:unit.texts.rerirect ~all',
|
||||
'target': 'v=spf1 include:unit.texts.redirect ~all',
|
||||
'subDomain': '',
|
||||
'id': 13
|
||||
})
|
||||
expected.add(Record.new(zone, '', {
|
||||
'ttl': 1000,
|
||||
'type': 'SPF',
|
||||
'value': 'v=spf1 include:unit.texts.rerirect ~all'
|
||||
'value': 'v=spf1 include:unit.texts.redirect ~all'
|
||||
}))
|
||||
|
||||
# SSHFP
|
||||
@@ -390,11 +390,11 @@ class TestOvhProvider(TestCase):
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain=u'', target=u'1.2.3.4', ttl=100),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
||||
subDomain=u'10 20 30 foo-1.unit.tests.',
|
||||
target='_srv._tcp', ttl=800),
|
||||
subDomain='_srv._tcp',
|
||||
target=u'10 20 30 foo-1.unit.tests.', ttl=800),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
||||
subDomain=u'40 50 60 foo-2.unit.tests.',
|
||||
target='_srv._tcp', ttl=800),
|
||||
subDomain='_srv._tcp',
|
||||
target=u'40 50 60 foo-2.unit.tests.', ttl=800),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'PTR',
|
||||
subDomain='4', target=u'unit.tests.', ttl=900),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
|
||||
@@ -402,8 +402,8 @@ class TestOvhProvider(TestCase):
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
|
||||
subDomain='www3', target=u'ns4.unit.tests.', ttl=700),
|
||||
call(u'/domain/zone/unit.tests/record',
|
||||
fieldType=u'SSHFP', target=u'', ttl=1100,
|
||||
subDomain=u'1 1 bf6b6825d2977c511a475bbefb88a'
|
||||
fieldType=u'SSHFP', subDomain=u'', ttl=1100,
|
||||
target=u'1 1 bf6b6825d2977c511a475bbefb88a'
|
||||
u'ad54'
|
||||
u'a92ac73',
|
||||
),
|
||||
@@ -416,7 +416,7 @@ class TestOvhProvider(TestCase):
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SPF',
|
||||
subDomain=u'', ttl=1000,
|
||||
target=u'v=spf1 include:unit.texts.'
|
||||
u'rerirect ~all',
|
||||
u'redirect ~all',
|
||||
),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain='sub', target=u'1.2.3.4', ttl=200),
|
||||
|
||||
@@ -100,7 +100,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
# No existing records -> creates for every record in expected
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=200, text=EMPTY_TEXT)
|
||||
# post 201, is reponse to the create with data
|
||||
# post 201, is response to the create with data
|
||||
mock.patch(ANY, status_code=201, text=assert_rrsets_callback)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
@@ -119,7 +119,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
mock.get(ANY, status_code=422, text='')
|
||||
# patch 422's, unknown zone
|
||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
||||
# post 201, is reponse to the create with data
|
||||
# post 201, is response to the create with data
|
||||
mock.post(ANY, status_code=201, text=assert_rrsets_callback)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
|
||||
@@ -331,9 +331,9 @@ class TestRoute53Provider(TestCase):
|
||||
stubber.assert_no_pending_responses()
|
||||
|
||||
# Populate a zone that doesn't exist
|
||||
noexist = Zone('does.not.exist.', [])
|
||||
provider.populate(noexist)
|
||||
self.assertEquals(set(), noexist.records)
|
||||
nonexistent = Zone('does.not.exist.', [])
|
||||
provider.populate(nonexistent)
|
||||
self.assertEquals(set(), nonexistent.records)
|
||||
|
||||
def test_sync(self):
|
||||
provider, stubber = self._get_stubbed_provider()
|
||||
|
||||
@@ -430,7 +430,7 @@ class TestRecord(TestCase):
|
||||
self.assertEqual(change.new, other)
|
||||
|
||||
# full sorting
|
||||
# equivilent
|
||||
# equivalent
|
||||
b_naptr_value = b.values[0]
|
||||
self.assertEquals(0, b_naptr_value.__cmp__(b_naptr_value))
|
||||
# by order
|
||||
@@ -710,7 +710,7 @@ class TestRecord(TestCase):
|
||||
Record.new(self.zone, 'unknown', {})
|
||||
self.assertTrue('missing type' in ctx.exception.message)
|
||||
|
||||
# Unkown type
|
||||
# Unknown type
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Record.new(self.zone, 'unknown', {
|
||||
'type': 'XXX',
|
||||
@@ -1360,7 +1360,7 @@ class TestRecordValidation(TestCase):
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'algorithm': 'nope',
|
||||
'fingerprint_type': 1,
|
||||
'fingerprint_type': 2,
|
||||
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
||||
}
|
||||
})
|
||||
@@ -1386,7 +1386,7 @@ class TestRecordValidation(TestCase):
|
||||
'type': 'SSHFP',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'algorithm': 1,
|
||||
'algorithm': 2,
|
||||
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
||||
}
|
||||
})
|
||||
@@ -1398,7 +1398,7 @@ class TestRecordValidation(TestCase):
|
||||
'type': 'SSHFP',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'algorithm': 1,
|
||||
'algorithm': 3,
|
||||
'fingerprint_type': 'yeeah',
|
||||
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user