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
|
#### 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
|
implementation. Sorting can be disabled in the YamlProvider with
|
||||||
`enforce_order: False`.
|
`enforce_order: False`.
|
||||||
* Semi-colon/escaping fixes and improvements.
|
* 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 |
|
| Provider | Record Support | GeoDNS Support | Notes |
|
||||||
|--|--|--|--|
|
|--|--|--|--|
|
||||||
| [AzureProvider](/octodns/provider/azuredns.py) | A, AAAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
|
| [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 |
|
| [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 |
|
| [DnsimpleProvider](/octodns/provider/dnsimple.py) | All | No | CAA tags restricted |
|
||||||
| [DynProvider](/octodns/provider/dyn.py) | All | Yes | |
|
| [DynProvider](/octodns/provider/dyn.py) | All | Yes | |
|
||||||
| [GoogleCloudProvider](/octodns/provider/googlecloud.py) | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | No | |
|
| [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.
|
* 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
|
* 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
|
* 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
|
## Custom Sources and Providers
|
||||||
|
|
||||||
|
@@ -53,11 +53,11 @@ The geo labels breakdown based on:
|
|||||||
|
|
||||||
1.
|
1.
|
||||||
- 'AF': 14, # Continental Africa
|
- 'AF': 14, # Continental Africa
|
||||||
- 'AN': 17, # Continental Antartica
|
- 'AN': 17, # Continental Antarctica
|
||||||
- 'AS': 15, # Contentinal Asia
|
- 'AS': 15, # Continental Asia
|
||||||
- 'EU': 13, # Contentinal Europe
|
- 'EU': 13, # Continental Europe
|
||||||
- 'NA': 11, # Continental North America
|
- 'NA': 11, # Continental North America
|
||||||
- 'OC': 16, # Contentinal Austrailia/Oceania
|
- 'OC': 16, # Continental Australia/Oceania
|
||||||
- 'SA': 12, # Continental South America
|
- 'SA': 12, # Continental South America
|
||||||
|
|
||||||
2. ISO Country Code https://en.wikipedia.org/wiki/ISO_3166-2
|
2. ISO Country Code https://en.wikipedia.org/wiki/ISO_3166-2
|
||||||
|
@@ -65,7 +65,7 @@ def main():
|
|||||||
resolver = AsyncResolver(configure=False,
|
resolver = AsyncResolver(configure=False,
|
||||||
num_workers=int(args.num_workers))
|
num_workers=int(args.num_workers))
|
||||||
if not ip_addr_re.match(server):
|
if not ip_addr_re.match(server):
|
||||||
server = str(query(server, 'A')[0])
|
server = unicode(query(server, 'A')[0])
|
||||||
log.info('server=%s', server)
|
log.info('server=%s', server)
|
||||||
resolver.nameservers = [server]
|
resolver.nameservers = [server]
|
||||||
resolver.lifetime = int(args.timeout)
|
resolver.lifetime = int(args.timeout)
|
||||||
@@ -81,12 +81,12 @@ def main():
|
|||||||
stdout.write(',')
|
stdout.write(',')
|
||||||
stdout.write(record._type)
|
stdout.write(record._type)
|
||||||
stdout.write(',')
|
stdout.write(',')
|
||||||
stdout.write(str(record.ttl))
|
stdout.write(unicode(record.ttl))
|
||||||
compare = {}
|
compare = {}
|
||||||
for future in futures:
|
for future in futures:
|
||||||
stdout.write(',')
|
stdout.write(',')
|
||||||
try:
|
try:
|
||||||
answers = [str(r) for r in future.result()]
|
answers = [unicode(r) for r in future.result()]
|
||||||
except (NoAnswer, NoNameservers):
|
except (NoAnswer, NoNameservers):
|
||||||
answers = ['*no answer*']
|
answers = ['*no answer*']
|
||||||
except NXDOMAIN:
|
except NXDOMAIN:
|
||||||
|
@@ -26,7 +26,7 @@ def main():
|
|||||||
help='Limit sync to the specified zone(s)')
|
help='Limit sync to the specified zone(s)')
|
||||||
|
|
||||||
# --sources isn't an option here b/c filtering sources out would be super
|
# --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
|
# everything, or even just part of things when there are multiple sources
|
||||||
|
|
||||||
parser.add_argument('--target', default=[], action='append',
|
parser.add_argument('--target', default=[], action='append',
|
||||||
|
@@ -51,7 +51,7 @@ class MakeThreadFuture(object):
|
|||||||
|
|
||||||
class MainThreadExecutor(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
|
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
|
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
|
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):
|
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
|
Notes on Azure records: An Azure record set has the form
|
||||||
RecordSet(name=<...>, type=<...>, arecords=[...], aaaa_records, ..)
|
RecordSet(name=<...>, type=<...>, arecords=[...], aaaa_records, ..)
|
||||||
@@ -222,7 +222,7 @@ class AzureProvider(BaseProvider):
|
|||||||
azuredns:
|
azuredns:
|
||||||
class: octodns.provider.azuredns.AzureProvider
|
class: octodns.provider.azuredns.AzureProvider
|
||||||
client_id: env/AZURE_APPLICATION_ID
|
client_id: env/AZURE_APPLICATION_ID
|
||||||
key: env/AZURE_AUTHENICATION_KEY
|
key: env/AZURE_AUTHENTICATION_KEY
|
||||||
directory_id: env/AZURE_DIRECTORY_ID
|
directory_id: env/AZURE_DIRECTORY_ID
|
||||||
sub_id: env/AZURE_SUBSCRIPTION_ID
|
sub_id: env/AZURE_SUBSCRIPTION_ID
|
||||||
resource_group: 'TestResource1'
|
resource_group: 'TestResource1'
|
||||||
|
@@ -17,7 +17,8 @@ class BaseProvider(BaseSource):
|
|||||||
delete_pcent_threshold=Plan.MAX_SAFE_DELETE_PCENT):
|
delete_pcent_threshold=Plan.MAX_SAFE_DELETE_PCENT):
|
||||||
super(BaseProvider, self).__init__(id)
|
super(BaseProvider, self).__init__(id)
|
||||||
self.log.debug('__init__: id=%s, apply_disabled=%s, '
|
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,
|
id,
|
||||||
apply_disabled,
|
apply_disabled,
|
||||||
update_pcent_threshold,
|
update_pcent_threshold,
|
||||||
@@ -29,14 +30,14 @@ class BaseProvider(BaseSource):
|
|||||||
def _include_change(self, change):
|
def _include_change(self, change):
|
||||||
'''
|
'''
|
||||||
An opportunity for providers to filter out false positives due to
|
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
|
return True
|
||||||
|
|
||||||
def _extra_changes(self, existing, changes):
|
def _extra_changes(self, existing, changes):
|
||||||
'''
|
'''
|
||||||
An opportunity for providers to add extra changes to the plan that are
|
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.
|
base NS records.
|
||||||
'''
|
'''
|
||||||
return []
|
return []
|
||||||
@@ -66,7 +67,7 @@ class BaseProvider(BaseSource):
|
|||||||
extra = self._extra_changes(existing, changes)
|
extra = self._extra_changes(existing, changes)
|
||||||
if extra:
|
if extra:
|
||||||
self.log.info('plan: extra changes\n %s', '\n '
|
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
|
changes += extra
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
|
@@ -14,14 +14,18 @@ from ..record import Record, Update
|
|||||||
from .base import BaseProvider
|
from .base import BaseProvider
|
||||||
|
|
||||||
|
|
||||||
class CloudflareAuthenticationError(Exception):
|
class CloudflareError(Exception):
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
try:
|
try:
|
||||||
message = data['errors'][0]['message']
|
message = data['errors'][0]['message']
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
message = 'Authentication error'
|
message = 'Cloudflare error'
|
||||||
super(CloudflareAuthenticationError, self).__init__(message)
|
super(CloudflareError, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class CloudflareAuthenticationError(CloudflareError):
|
||||||
|
def __init__(self, data):
|
||||||
|
CloudflareError.__init__(self, data)
|
||||||
|
|
||||||
|
|
||||||
class CloudflareProvider(BaseProvider):
|
class CloudflareProvider(BaseProvider):
|
||||||
@@ -34,18 +38,24 @@ class CloudflareProvider(BaseProvider):
|
|||||||
email: dns-manager@example.com
|
email: dns-manager@example.com
|
||||||
# The api key (required)
|
# The api key (required)
|
||||||
token: foo
|
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
|
SUPPORTS_GEO = False
|
||||||
# TODO: support SRV
|
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SRV',
|
||||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SPF',
|
'SPF', 'TXT'))
|
||||||
'TXT'))
|
|
||||||
|
|
||||||
MIN_TTL = 120
|
MIN_TTL = 120
|
||||||
TIMEOUT = 15
|
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 = 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)
|
super(CloudflareProvider, self).__init__(id, *args, **kwargs)
|
||||||
|
|
||||||
sess = Session()
|
sess = Session()
|
||||||
@@ -53,6 +63,7 @@ class CloudflareProvider(BaseProvider):
|
|||||||
'X-Auth-Email': email,
|
'X-Auth-Email': email,
|
||||||
'X-Auth-Key': token,
|
'X-Auth-Key': token,
|
||||||
})
|
})
|
||||||
|
self.cdn = cdn
|
||||||
self._sess = sess
|
self._sess = sess
|
||||||
|
|
||||||
self._zones = None
|
self._zones = None
|
||||||
@@ -65,8 +76,11 @@ class CloudflareProvider(BaseProvider):
|
|||||||
resp = self._sess.request(method, url, params=params, json=data,
|
resp = self._sess.request(method, url, params=params, json=data,
|
||||||
timeout=self.TIMEOUT)
|
timeout=self.TIMEOUT)
|
||||||
self.log.debug('_request: status=%d', resp.status_code)
|
self.log.debug('_request: status=%d', resp.status_code)
|
||||||
|
if resp.status_code == 400:
|
||||||
|
raise CloudflareError(resp.json())
|
||||||
if resp.status_code == 403:
|
if resp.status_code == 403:
|
||||||
raise CloudflareAuthenticationError(resp.json())
|
raise CloudflareAuthenticationError(resp.json())
|
||||||
|
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
@@ -88,6 +102,18 @@ class CloudflareProvider(BaseProvider):
|
|||||||
|
|
||||||
return self._zones
|
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):
|
def _data_for_multiple(self, _type, records):
|
||||||
return {
|
return {
|
||||||
'ttl': records[0]['ttl'],
|
'ttl': records[0]['ttl'],
|
||||||
@@ -147,6 +173,21 @@ class CloudflareProvider(BaseProvider):
|
|||||||
'values': ['{}.'.format(r['content']) for r in records],
|
'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):
|
def zone_records(self, zone):
|
||||||
if zone.name not in self._zone_records:
|
if zone.name not in self._zone_records:
|
||||||
zone_id = self.zones.get(zone.name, False)
|
zone_id = self.zones.get(zone.name, False)
|
||||||
@@ -169,6 +210,20 @@ class CloudflareProvider(BaseProvider):
|
|||||||
|
|
||||||
return self._zone_records[zone.name]
|
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):
|
def populate(self, zone, target=False, lenient=False):
|
||||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||||
target, lenient)
|
target, lenient)
|
||||||
@@ -187,15 +242,17 @@ class CloudflareProvider(BaseProvider):
|
|||||||
|
|
||||||
for name, types in values.items():
|
for name, types in values.items():
|
||||||
for _type, records in types.items():
|
for _type, records in types.items():
|
||||||
|
record = self._record_for(zone, name, _type, records,
|
||||||
|
lenient)
|
||||||
|
|
||||||
# Cloudflare supports ALIAS semantics with root CNAMEs
|
# only one rewrite is needed for names where the proxy is
|
||||||
if _type == 'CNAME' and name == '':
|
# enabled at multiple records with a different type but
|
||||||
_type = 'ALIAS'
|
# 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)
|
zone.add_record(record)
|
||||||
|
|
||||||
self.log.info('populate: found %s records, exists=%s',
|
self.log.info('populate: found %s records, exists=%s',
|
||||||
@@ -206,9 +263,16 @@ class CloudflareProvider(BaseProvider):
|
|||||||
if isinstance(change, Update):
|
if isinstance(change, Update):
|
||||||
existing = change.existing.data
|
existing = change.existing.data
|
||||||
new = change.new.data
|
new = change.new.data
|
||||||
new['ttl'] = max(120, new['ttl'])
|
new['ttl'] = max(self.MIN_TTL, new['ttl'])
|
||||||
if new == existing:
|
if new == existing:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
def _contents_for_multiple(self, record):
|
def _contents_for_multiple(self, record):
|
||||||
@@ -244,6 +308,21 @@ class CloudflareProvider(BaseProvider):
|
|||||||
'content': value.exchange
|
'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):
|
def _gen_contents(self, record):
|
||||||
name = record.fqdn[:-1]
|
name = record.fqdn[:-1]
|
||||||
_type = record._type
|
_type = record._type
|
||||||
@@ -299,10 +378,6 @@ class CloudflareProvider(BaseProvider):
|
|||||||
for c in self._gen_contents(change.new)
|
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
|
# Find the things we need to add
|
||||||
adds = []
|
adds = []
|
||||||
for k, content in new_contents.items():
|
for k, content in new_contents.items():
|
||||||
@@ -312,22 +387,25 @@ class CloudflareProvider(BaseProvider):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
adds.append(content)
|
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
|
# Find things we need to remove
|
||||||
name = change.new.fqdn[:-1]
|
hostname = zone.hostname_from_fqdn(change.new.fqdn[:-1])
|
||||||
_type = change.new._type
|
_type = change.new._type
|
||||||
# OK, work through each record from the zone
|
# OK, work through each record from the zone
|
||||||
for record in self.zone_records(change.new.zone):
|
for record in self.zone_records(zone):
|
||||||
if name == record['name'] and _type == record['type']:
|
name = zone.hostname_from_fqdn(record['name'])
|
||||||
# This is match for our name and type, we need to look at
|
# Use the _record_for so that we include all of standard
|
||||||
# contents now, build a dict of the relevant keys and vals
|
# conversion logic
|
||||||
content = {}
|
r = self._record_for(zone, name, record['type'], [record], True)
|
||||||
for k in keys:
|
if hostname == r.name and _type == r._type:
|
||||||
content[k] = record[k]
|
|
||||||
# :-(
|
# Round trip the single value through a record to contents flow
|
||||||
if _type in ('CNAME', 'MX', 'NS'):
|
# to get a consistent _gen_contents result that matches what
|
||||||
content['content'] += '.'
|
# 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
|
# If the hash of that dict isn't in new this record isn't
|
||||||
# needed
|
# needed
|
||||||
if self._hash_content(content) not in new_contents:
|
if self._hash_content(content) not in new_contents:
|
||||||
|
@@ -56,7 +56,7 @@ class DigitalOceanClient(object):
|
|||||||
self._request('POST', '/domains', data={'name': name,
|
self._request('POST', '/domains', data={'name': name,
|
||||||
'ip_address': '192.0.2.1'})
|
'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)
|
records = self.records(name)
|
||||||
for record in records:
|
for record in records:
|
||||||
if record['name'] == '' and record['type'] == 'A':
|
if record['name'] == '' and record['type'] == 'A':
|
||||||
|
@@ -160,7 +160,7 @@ class DnsimpleProvider(BaseProvider):
|
|||||||
record['content'].split(' ', 5)
|
record['content'].split(' ', 5)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# their api will let you create invalid records, this
|
# 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
|
# purposes. That will cause updates to happen to delete them if
|
||||||
# they shouldn't exist or update them if they're wrong
|
# they shouldn't exist or update them if they're wrong
|
||||||
continue
|
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')
|
cls.log.debug('get: fetched')
|
||||||
except DynectGetError:
|
except DynectGetError:
|
||||||
if not create:
|
if not create:
|
||||||
cls.log.debug("get: does't exist")
|
cls.log.debug("get: doesn't exist")
|
||||||
return None
|
return None
|
||||||
# this value shouldn't really matter, it's not tied to
|
# this value shouldn't really matter, it's not tied to
|
||||||
# whois or anything
|
# whois or anything
|
||||||
@@ -129,11 +129,11 @@ class DynProvider(BaseProvider):
|
|||||||
REGION_CODES = {
|
REGION_CODES = {
|
||||||
'NA': 11, # Continental North America
|
'NA': 11, # Continental North America
|
||||||
'SA': 12, # Continental South America
|
'SA': 12, # Continental South America
|
||||||
'EU': 13, # Contentinal Europe
|
'EU': 13, # Continental Europe
|
||||||
'AF': 14, # Continental Africa
|
'AF': 14, # Continental Africa
|
||||||
'AS': 15, # Contentinal Asia
|
'AS': 15, # Continental Asia
|
||||||
'OC': 16, # Contentinal Austrailia/Oceania
|
'OC': 16, # Continental Australia/Oceania
|
||||||
'AN': 17, # Continental Antartica
|
'AN': 17, # Continental Antarctica
|
||||||
}
|
}
|
||||||
|
|
||||||
_sess_create_lock = Lock()
|
_sess_create_lock = Lock()
|
||||||
@@ -166,7 +166,7 @@ class DynProvider(BaseProvider):
|
|||||||
if DynectSession.get_session() is None:
|
if DynectSession.get_session() is None:
|
||||||
# We need to create a new session for this thread and DynectSession
|
# 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
|
# 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
|
# has finished (long time b/c it makes http calls) the subsequent
|
||||||
# creates will blow away DynectSession._instances, potentially
|
# creates will blow away DynectSession._instances, potentially
|
||||||
# multiple times if there are multiple creates in flight. Only the
|
# multiple times if there are multiple creates in flight. Only the
|
||||||
@@ -291,7 +291,7 @@ class DynProvider(BaseProvider):
|
|||||||
try:
|
try:
|
||||||
fqdn, _type = td.label.split(':', 1)
|
fqdn, _type = td.label.split(':', 1)
|
||||||
except ValueError as e:
|
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)
|
td.label, e.message)
|
||||||
continue
|
continue
|
||||||
tds[fqdn][_type] = td
|
tds[fqdn][_type] = td
|
||||||
|
@@ -127,9 +127,10 @@ class GoogleCloudProvider(BaseProvider):
|
|||||||
:type return: new google.cloud.dns.ManagedZone
|
:type return: new google.cloud.dns.ManagedZone
|
||||||
"""
|
"""
|
||||||
# Zone name must begin with a letter, end with a letter or digit,
|
# Zone name must begin with a letter, end with a letter or digit,
|
||||||
# and only contain lowercase letters, digits or dashes
|
# and only contain lowercase letters, digits or dashes,
|
||||||
zone_name = '{}-{}'.format(
|
# and be 63 characters or less
|
||||||
dns_name[:-1].replace('.', '-'), uuid4().hex)
|
zone_name = 'zone-{}-{}'.format(
|
||||||
|
dns_name.replace('.', '-'), uuid4().hex)[:63]
|
||||||
|
|
||||||
gcloud_zone = self.gcloud_client.zone(
|
gcloud_zone = self.gcloud_client.zone(
|
||||||
name=zone_name,
|
name=zone_name,
|
||||||
|
@@ -75,9 +75,9 @@ class Ns1Provider(BaseProvider):
|
|||||||
else:
|
else:
|
||||||
values.extend(answer['answer'])
|
values.extend(answer['answer'])
|
||||||
codes.append([])
|
codes.append([])
|
||||||
values = [str(x) for x in values]
|
values = [unicode(x) for x in values]
|
||||||
geo = OrderedDict(
|
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['values'] = values
|
||||||
data['geo'] = geo
|
data['geo'] = geo
|
||||||
@@ -236,11 +236,10 @@ class Ns1Provider(BaseProvider):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
params['filters'] = []
|
params['filters'] = []
|
||||||
if len(params['answers']) > 1:
|
if has_country:
|
||||||
params['filters'].append(
|
params['filters'].append(
|
||||||
{"filter": "shuffle", "config": {}}
|
{"filter": "shuffle", "config": {}}
|
||||||
)
|
)
|
||||||
if has_country:
|
|
||||||
params['filters'].append(
|
params['filters'].append(
|
||||||
{"filter": "geotarget_country", "config": {}}
|
{"filter": "geotarget_country", "config": {}}
|
||||||
)
|
)
|
||||||
|
@@ -269,10 +269,11 @@ class OvhProvider(BaseProvider):
|
|||||||
def _params_for_SRV(record):
|
def _params_for_SRV(record):
|
||||||
for value in record.values:
|
for value in record.values:
|
||||||
yield {
|
yield {
|
||||||
'subDomain': '{} {} {} {}'.format(value.priority,
|
'target': '{} {} {} {}'.format(value.priority,
|
||||||
value.weight, value.port,
|
value.weight,
|
||||||
value.target),
|
value.port,
|
||||||
'target': record.name,
|
value.target),
|
||||||
|
'subDomain': record.name,
|
||||||
'ttl': record.ttl,
|
'ttl': record.ttl,
|
||||||
'fieldType': record._type
|
'fieldType': record._type
|
||||||
}
|
}
|
||||||
@@ -281,10 +282,10 @@ class OvhProvider(BaseProvider):
|
|||||||
def _params_for_SSHFP(record):
|
def _params_for_SSHFP(record):
|
||||||
for value in record.values:
|
for value in record.values:
|
||||||
yield {
|
yield {
|
||||||
'subDomain': '{} {} {}'.format(value.algorithm,
|
'target': '{} {} {}'.format(value.algorithm,
|
||||||
value.fingerprint_type,
|
value.fingerprint_type,
|
||||||
value.fingerprint),
|
value.fingerprint),
|
||||||
'target': record.name,
|
'subDomain': record.name,
|
||||||
'ttl': record.ttl,
|
'ttl': record.ttl,
|
||||||
'fieldType': record._type
|
'fieldType': record._type
|
||||||
}
|
}
|
||||||
|
@@ -61,17 +61,17 @@ class Plan(object):
|
|||||||
delete_pcent = self.change_counts['Delete'] / existing_record_count
|
delete_pcent = self.change_counts['Delete'] / existing_record_count
|
||||||
|
|
||||||
if update_pcent > self.update_pcent_threshold:
|
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(
|
'({}/{})'.format(
|
||||||
update_pcent,
|
update_pcent * 100,
|
||||||
self.MAX_SAFE_UPDATE_PCENT * 100,
|
self.update_pcent_threshold * 100,
|
||||||
self.change_counts['Update'],
|
self.change_counts['Update'],
|
||||||
existing_record_count))
|
existing_record_count))
|
||||||
if delete_pcent > self.delete_pcent_threshold:
|
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(
|
'({}/{})'.format(
|
||||||
delete_pcent,
|
delete_pcent * 100,
|
||||||
self.MAX_SAFE_DELETE_PCENT * 100,
|
self.delete_pcent_threshold * 100,
|
||||||
self.change_counts['Delete'],
|
self.change_counts['Delete'],
|
||||||
existing_record_count))
|
existing_record_count))
|
||||||
|
|
||||||
@@ -147,11 +147,11 @@ class PlanLogger(_PlanOutput):
|
|||||||
|
|
||||||
def _value_stringifier(record, sep):
|
def _value_stringifier(record, sep):
|
||||||
try:
|
try:
|
||||||
values = [str(v) for v in record.values]
|
values = [unicode(v) for v in record.values]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
values = [record.value]
|
values = [record.value]
|
||||||
for code, gv in sorted(getattr(record, 'geo', {}).items()):
|
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))
|
values.append('{}: {}'.format(code, vs))
|
||||||
return sep.join(values)
|
return sep.join(values)
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ class PlanMarkdown(_PlanOutput):
|
|||||||
fh.write(' | ')
|
fh.write(' | ')
|
||||||
# TTL
|
# TTL
|
||||||
if existing:
|
if existing:
|
||||||
fh.write(str(existing.ttl))
|
fh.write(unicode(existing.ttl))
|
||||||
fh.write(' | ')
|
fh.write(' | ')
|
||||||
fh.write(_value_stringifier(existing, '; '))
|
fh.write(_value_stringifier(existing, '; '))
|
||||||
fh.write(' | |\n')
|
fh.write(' | |\n')
|
||||||
@@ -201,7 +201,7 @@ class PlanMarkdown(_PlanOutput):
|
|||||||
fh.write('| | | | ')
|
fh.write('| | | | ')
|
||||||
|
|
||||||
if new:
|
if new:
|
||||||
fh.write(str(new.ttl))
|
fh.write(unicode(new.ttl))
|
||||||
fh.write(' | ')
|
fh.write(' | ')
|
||||||
fh.write(_value_stringifier(new, '; '))
|
fh.write(_value_stringifier(new, '; '))
|
||||||
fh.write(' | ')
|
fh.write(' | ')
|
||||||
@@ -210,7 +210,7 @@ class PlanMarkdown(_PlanOutput):
|
|||||||
fh.write(' |\n')
|
fh.write(' |\n')
|
||||||
|
|
||||||
fh.write('\nSummary: ')
|
fh.write('\nSummary: ')
|
||||||
fh.write(str(plan))
|
fh.write(unicode(plan))
|
||||||
fh.write('\n\n')
|
fh.write('\n\n')
|
||||||
else:
|
else:
|
||||||
fh.write('## No changes were planned\n')
|
fh.write('## No changes were planned\n')
|
||||||
@@ -261,7 +261,7 @@ class PlanHtml(_PlanOutput):
|
|||||||
# TTL
|
# TTL
|
||||||
if existing:
|
if existing:
|
||||||
fh.write(' <td>')
|
fh.write(' <td>')
|
||||||
fh.write(str(existing.ttl))
|
fh.write(unicode(existing.ttl))
|
||||||
fh.write('</td>\n <td>')
|
fh.write('</td>\n <td>')
|
||||||
fh.write(_value_stringifier(existing, '<br/>'))
|
fh.write(_value_stringifier(existing, '<br/>'))
|
||||||
fh.write('</td>\n <td></td>\n </tr>\n')
|
fh.write('</td>\n <td></td>\n </tr>\n')
|
||||||
@@ -270,7 +270,7 @@ class PlanHtml(_PlanOutput):
|
|||||||
|
|
||||||
if new:
|
if new:
|
||||||
fh.write(' <td>')
|
fh.write(' <td>')
|
||||||
fh.write(str(new.ttl))
|
fh.write(unicode(new.ttl))
|
||||||
fh.write('</td>\n <td>')
|
fh.write('</td>\n <td>')
|
||||||
fh.write(_value_stringifier(new, '<br/>'))
|
fh.write(_value_stringifier(new, '<br/>'))
|
||||||
fh.write('</td>\n <td>')
|
fh.write('</td>\n <td>')
|
||||||
@@ -279,7 +279,7 @@ class PlanHtml(_PlanOutput):
|
|||||||
fh.write('</td>\n </tr>\n')
|
fh.write('</td>\n </tr>\n')
|
||||||
|
|
||||||
fh.write(' <tr>\n <td colspan=6>Summary: ')
|
fh.write(' <tr>\n <td colspan=6>Summary: ')
|
||||||
fh.write(str(plan))
|
fh.write(unicode(plan))
|
||||||
fh.write('</td>\n </tr>\n</table>\n')
|
fh.write('</td>\n </tr>\n</table>\n')
|
||||||
else:
|
else:
|
||||||
fh.write('<b>No changes were planned</b>')
|
fh.write('<b>No changes were planned</b>')
|
||||||
|
@@ -178,7 +178,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
|||||||
raise Exception('PowerDNS unauthorized host={}'
|
raise Exception('PowerDNS unauthorized host={}'
|
||||||
.format(self.host))
|
.format(self.host))
|
||||||
elif e.response.status_code == 422:
|
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
|
# domain. We'll just ignore it here and leave the zone
|
||||||
# untouched.
|
# untouched.
|
||||||
pass
|
pass
|
||||||
@@ -297,8 +297,8 @@ class PowerDnsBaseProvider(BaseProvider):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# sorting mostly to make things deterministic for testing, but in
|
# sorting mostly to make things deterministic for testing, but in
|
||||||
# theory it let us find what we're after quickier (though sorting would
|
# theory it let us find what we're after quicker (though sorting would
|
||||||
# ve more exepensive.)
|
# be more expensive.)
|
||||||
for record in sorted(existing.records):
|
for record in sorted(existing.records):
|
||||||
if record == ns:
|
if record == ns:
|
||||||
# We've found the top-level NS record, return any changes
|
# We've found the top-level NS record, return any changes
|
||||||
@@ -344,7 +344,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
|||||||
e.response.text)
|
e.response.text)
|
||||||
raise
|
raise
|
||||||
self.log.info('_apply: creating zone=%s', desired.name)
|
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
|
# domain. We'll try to create it with the correct records instead
|
||||||
# of update. Hopefully all the mods are creates :-)
|
# of update. Hopefully all the mods are creates :-)
|
||||||
data = {
|
data = {
|
||||||
|
@@ -130,17 +130,9 @@ class RackspaceProvider(BaseProvider):
|
|||||||
def _delete(self, path, data=None):
|
def _delete(self, path, data=None):
|
||||||
return self._request('DELETE', path, data=data)
|
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
|
@classmethod
|
||||||
def _key_for_record(cls, rs_record):
|
def _key_for_record(cls, rs_record):
|
||||||
return cls._as_unicode(rs_record['type'], 'ascii'), \
|
return rs_record['type'], rs_record['name'], rs_record['data']
|
||||||
cls._as_unicode(rs_record['name'], 'utf-8'), \
|
|
||||||
cls._as_unicode(rs_record['data'], 'utf-8')
|
|
||||||
|
|
||||||
def _data_for_multiple(self, rrset):
|
def _data_for_multiple(self, rrset):
|
||||||
return {
|
return {
|
||||||
|
@@ -61,7 +61,7 @@ class _Route53Record(object):
|
|||||||
|
|
||||||
# NOTE: we're using __hash__ and __cmp__ methods that consider
|
# NOTE: we're using __hash__ and __cmp__ methods that consider
|
||||||
# _Route53Records equivalent if they have the same class, fqdn, and _type.
|
# _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):
|
def __hash__(self):
|
||||||
'sub-classes should never use this method'
|
'sub-classes should never use this method'
|
||||||
@@ -682,7 +682,7 @@ class Route53Provider(BaseProvider):
|
|||||||
.get('CountryCode', False) == '*':
|
.get('CountryCode', False) == '*':
|
||||||
# it's a default record
|
# it's a default record
|
||||||
continue
|
continue
|
||||||
# we expect a healtcheck now
|
# we expect a healthcheck now
|
||||||
try:
|
try:
|
||||||
health_check_id = rrset['HealthCheckId']
|
health_check_id = rrset['HealthCheckId']
|
||||||
caller_ref = \
|
caller_ref = \
|
||||||
@@ -733,7 +733,7 @@ class Route53Provider(BaseProvider):
|
|||||||
batch_rs_count)
|
batch_rs_count)
|
||||||
# send the batch
|
# send the batch
|
||||||
self._really_apply(batch, zone_id)
|
self._really_apply(batch, zone_id)
|
||||||
# start a new batch with the lefovers
|
# start a new batch with the leftovers
|
||||||
batch = mods
|
batch = mods
|
||||||
batch_rs_count = mods_rs_count
|
batch_rs_count = mods_rs_count
|
||||||
|
|
||||||
|
@@ -122,7 +122,7 @@ class Record(object):
|
|||||||
self.__class__.__name__, name)
|
self.__class__.__name__, name)
|
||||||
self.zone = zone
|
self.zone = zone
|
||||||
# force everything lower-case just to be safe
|
# 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.source = source
|
||||||
self.ttl = int(data['ttl'])
|
self.ttl = int(data['ttl'])
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ class Record(object):
|
|||||||
|
|
||||||
# NOTE: we're using __hash__ and __cmp__ methods that consider Records
|
# NOTE: we're using __hash__ and __cmp__ methods that consider Records
|
||||||
# equivalent if they have the same name & _type. Values are ignored. This
|
# 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):
|
def __hash__(self):
|
||||||
return '{}:{}'.format(self.name, self._type).__hash__()
|
return '{}:{}'.format(self.name, self._type).__hash__()
|
||||||
@@ -274,7 +274,8 @@ class _ValuesMixin(object):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def __repr__(self):
|
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__,
|
return '<{} {} {}, {}, {}>'.format(self.__class__.__name__,
|
||||||
self._type, self.ttl,
|
self._type, self.ttl,
|
||||||
self.fqdn, values)
|
self.fqdn, values)
|
||||||
@@ -678,8 +679,8 @@ class PtrRecord(_ValueMixin, Record):
|
|||||||
|
|
||||||
|
|
||||||
class SshfpValue(object):
|
class SshfpValue(object):
|
||||||
VALID_ALGORITHMS = (1, 2)
|
VALID_ALGORITHMS = (1, 2, 3)
|
||||||
VALID_FINGERPRINT_TYPES = (1,)
|
VALID_FINGERPRINT_TYPES = (1, 2)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _validate_value(cls, value):
|
def _validate_value(cls, value):
|
||||||
|
@@ -37,10 +37,10 @@ class Zone(object):
|
|||||||
if not name[-1] == '.':
|
if not name[-1] == '.':
|
||||||
raise Exception('Invalid zone name {}, missing ending dot'
|
raise Exception('Invalid zone name {}, missing ending dot'
|
||||||
.format(name))
|
.format(name))
|
||||||
# Force everyting to lowercase just to be safe
|
# Force everything to lowercase just to be safe
|
||||||
self.name = str(name).lower() if name else name
|
self.name = unicode(name).lower() if name else name
|
||||||
self.sub_zones = sub_zones
|
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
|
# duplicates and detect when CNAMEs co-exist with other records
|
||||||
self._records = defaultdict(set)
|
self._records = defaultdict(set)
|
||||||
# optional leading . to match empty hostname
|
# optional leading . to match empty hostname
|
||||||
|
@@ -11,4 +11,4 @@ git tag -s v$VERSION -m "Release $VERSION"
|
|||||||
git push origin v$VERSION
|
git push origin v$VERSION
|
||||||
echo "Tagged and pushed v$VERSION"
|
echo "Tagged and pushed v$VERSION"
|
||||||
python setup.py sdist upload
|
python setup.py sdist upload
|
||||||
echo "Updloaded $VERSION"
|
echo "Uploaded $VERSION"
|
||||||
|
@@ -156,14 +156,64 @@
|
|||||||
"meta": {
|
"meta": {
|
||||||
"auto_added": false
|
"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": {
|
"result_info": {
|
||||||
"page": 2,
|
"page": 2,
|
||||||
"per_page": 10,
|
"per_page": 11,
|
||||||
"total_pages": 2,
|
"total_pages": 2,
|
||||||
"count": 9,
|
"count": 9,
|
||||||
"total_count": 19
|
"total_count": 21
|
||||||
},
|
},
|
||||||
"success": true,
|
"success": true,
|
||||||
"errors": [],
|
"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.', [])
|
zone = Zone('unit.tests.', [])
|
||||||
with self.assertRaises(NotImplementedError) as ctx:
|
with self.assertRaises(NotImplementedError) as ctx:
|
||||||
HasSupportsGeo('hassupportesgeo').populate(zone)
|
HasSupportsGeo('hassupportsgeo').populate(zone)
|
||||||
self.assertEquals('Abstract base class, SUPPORTS property missing',
|
self.assertEquals('Abstract base class, SUPPORTS property missing',
|
||||||
ctx.exception.message)
|
ctx.exception.message)
|
||||||
|
|
||||||
class HasSupports(HasSupportsGeo):
|
class HasSupports(HasSupportsGeo):
|
||||||
SUPPORTS = set(('A',))
|
SUPPORTS = set(('A',))
|
||||||
with self.assertRaises(NotImplementedError) as ctx:
|
with self.assertRaises(NotImplementedError) as ctx:
|
||||||
HasSupports('hassupportes').populate(zone)
|
HasSupports('hassupports').populate(zone)
|
||||||
self.assertEquals('Abstract base class, populate method missing',
|
self.assertEquals('Abstract base class, populate method missing',
|
||||||
ctx.exception.message)
|
ctx.exception.message)
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class TestBaseProvider(TestCase):
|
|||||||
'value': '1.2.3.4'
|
'value': '1.2.3.4'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.assertTrue(HasSupports('hassupportesgeo')
|
self.assertTrue(HasSupports('hassupportsgeo')
|
||||||
.supports(list(zone.records)[0]))
|
.supports(list(zone.records)[0]))
|
||||||
|
|
||||||
plan = HasPopulate('haspopulate').plan(zone)
|
plan = HasPopulate('haspopulate').plan(zone)
|
||||||
@@ -178,7 +178,7 @@ class TestBaseProvider(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
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,
|
'ttl': 60,
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'value': '2.3.4.5'
|
'value': '2.3.4.5'
|
||||||
@@ -210,7 +210,7 @@ class TestBaseProvider(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
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,
|
'ttl': 60,
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'value': '2.3.4.5'
|
'value': '2.3.4.5'
|
||||||
@@ -236,7 +236,7 @@ class TestBaseProvider(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
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,
|
'ttl': 60,
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'value': '2.3.4.5'
|
'value': '2.3.4.5'
|
||||||
@@ -258,7 +258,7 @@ class TestBaseProvider(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
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,
|
'ttl': 60,
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'value': '2.3.4.5'
|
'value': '2.3.4.5'
|
||||||
@@ -284,7 +284,7 @@ class TestBaseProvider(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
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,
|
'ttl': 60,
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'value': '2.3.4.5'
|
'value': '2.3.4.5'
|
||||||
@@ -307,7 +307,7 @@ class TestBaseProvider(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
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,
|
'ttl': 60,
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'value': '2.3.4.5'
|
'value': '2.3.4.5'
|
||||||
@@ -335,7 +335,7 @@ class TestBaseProvider(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
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,
|
'ttl': 60,
|
||||||
'type': 'A',
|
'type': 'A',
|
||||||
'value': '2.3.4.5'
|
'value': '2.3.4.5'
|
||||||
|
@@ -42,6 +42,20 @@ class TestCloudflareProvider(TestCase):
|
|||||||
def test_populate(self):
|
def test_populate(self):
|
||||||
provider = CloudflareProvider('test', 'email', 'token')
|
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
|
# Bad auth
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
mock.get(ANY, status_code=403,
|
mock.get(ANY, status_code=403,
|
||||||
@@ -52,6 +66,8 @@ class TestCloudflareProvider(TestCase):
|
|||||||
with self.assertRaises(Exception) as ctx:
|
with self.assertRaises(Exception) as ctx:
|
||||||
zone = Zone('unit.tests.', [])
|
zone = Zone('unit.tests.', [])
|
||||||
provider.populate(zone)
|
provider.populate(zone)
|
||||||
|
self.assertEquals('CloudflareAuthenticationError',
|
||||||
|
type(ctx.exception).__name__)
|
||||||
self.assertEquals('Unknown X-Auth-Key or X-Auth-Email',
|
self.assertEquals('Unknown X-Auth-Key or X-Auth-Email',
|
||||||
ctx.exception.message)
|
ctx.exception.message)
|
||||||
|
|
||||||
@@ -62,7 +78,9 @@ class TestCloudflareProvider(TestCase):
|
|||||||
with self.assertRaises(Exception) as ctx:
|
with self.assertRaises(Exception) as ctx:
|
||||||
zone = Zone('unit.tests.', [])
|
zone = Zone('unit.tests.', [])
|
||||||
provider.populate(zone)
|
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
|
# General error
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
@@ -119,15 +137,16 @@ class TestCloudflareProvider(TestCase):
|
|||||||
|
|
||||||
zone = Zone('unit.tests.', [])
|
zone = Zone('unit.tests.', [])
|
||||||
provider.populate(zone)
|
provider.populate(zone)
|
||||||
self.assertEquals(11, len(zone.records))
|
self.assertEquals(12, len(zone.records))
|
||||||
|
|
||||||
changes = self.expected.changes(zone, provider)
|
changes = self.expected.changes(zone, provider)
|
||||||
|
|
||||||
self.assertEquals(0, len(changes))
|
self.assertEquals(0, len(changes))
|
||||||
|
|
||||||
# re-populating the same zone/records comes out of cache, no calls
|
# re-populating the same zone/records comes out of cache, no calls
|
||||||
again = Zone('unit.tests.', [])
|
again = Zone('unit.tests.', [])
|
||||||
provider.populate(again)
|
provider.populate(again)
|
||||||
self.assertEquals(11, len(again.records))
|
self.assertEquals(12, len(again.records))
|
||||||
|
|
||||||
def test_apply(self):
|
def test_apply(self):
|
||||||
provider = CloudflareProvider('test', 'email', 'token')
|
provider = CloudflareProvider('test', 'email', 'token')
|
||||||
@@ -141,12 +160,12 @@ class TestCloudflareProvider(TestCase):
|
|||||||
'id': 42,
|
'id': 42,
|
||||||
}
|
}
|
||||||
}, # zone create
|
}, # zone create
|
||||||
] + [None] * 18 # individual record creates
|
] + [None] * 20 # individual record creates
|
||||||
|
|
||||||
# non-existant zone, create everything
|
# non-existant zone, create everything
|
||||||
plan = provider.plan(self.expected)
|
plan = provider.plan(self.expected)
|
||||||
self.assertEquals(11, len(plan.changes))
|
self.assertEquals(12, len(plan.changes))
|
||||||
self.assertEquals(11, provider.apply(plan))
|
self.assertEquals(12, provider.apply(plan))
|
||||||
self.assertFalse(plan.exists)
|
self.assertFalse(plan.exists)
|
||||||
|
|
||||||
provider._request.assert_has_calls([
|
provider._request.assert_has_calls([
|
||||||
@@ -172,7 +191,7 @@ class TestCloudflareProvider(TestCase):
|
|||||||
}),
|
}),
|
||||||
], True)
|
], True)
|
||||||
# expected number of total calls
|
# expected number of total calls
|
||||||
self.assertEquals(20, provider._request.call_count)
|
self.assertEquals(22, provider._request.call_count)
|
||||||
|
|
||||||
provider._request.reset_mock()
|
provider._request.reset_mock()
|
||||||
|
|
||||||
@@ -486,3 +505,187 @@ class TestCloudflareProvider(TestCase):
|
|||||||
'ttl': 300,
|
'ttl': 300,
|
||||||
'type': 'CNAME'
|
'type': 'CNAME'
|
||||||
}, list(contents)[0])
|
}, 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())
|
traffic_director_response = loads(fh.read())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def traffic_directors_reponse(self):
|
def traffic_directors_response(self):
|
||||||
return {
|
return {
|
||||||
'data': [{
|
'data': [{
|
||||||
'active': 'Y',
|
'active': 'Y',
|
||||||
@@ -609,7 +609,7 @@ class TestDynProviderGeo(TestCase):
|
|||||||
mock.side_effect = [{'data': []}]
|
mock.side_effect = [{'data': []}]
|
||||||
self.assertEquals({}, provider.traffic_directors)
|
self.assertEquals({}, provider.traffic_directors)
|
||||||
|
|
||||||
# a supported td and an ingored one
|
# a supported td and an ignored one
|
||||||
response = {
|
response = {
|
||||||
'data': [{
|
'data': [{
|
||||||
'active': 'Y',
|
'active': 'Y',
|
||||||
@@ -652,7 +652,7 @@ class TestDynProviderGeo(TestCase):
|
|||||||
set(tds.keys()))
|
set(tds.keys()))
|
||||||
self.assertEquals(['A'], tds['unit.tests.'].keys())
|
self.assertEquals(['A'], tds['unit.tests.'].keys())
|
||||||
self.assertEquals(['A'], tds['geo.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',
|
"'%s': %s", 'something else',
|
||||||
'need more than 1 value to '
|
'need more than 1 value to '
|
||||||
'unpack')
|
'unpack')
|
||||||
@@ -760,7 +760,7 @@ class TestDynProviderGeo(TestCase):
|
|||||||
# only traffic director
|
# only traffic director
|
||||||
mock.side_effect = [
|
mock.side_effect = [
|
||||||
# get traffic directors
|
# get traffic directors
|
||||||
self.traffic_directors_reponse,
|
self.traffic_directors_response,
|
||||||
# get traffic director
|
# get traffic director
|
||||||
self.traffic_director_response,
|
self.traffic_director_response,
|
||||||
# get zone
|
# get zone
|
||||||
@@ -811,7 +811,7 @@ class TestDynProviderGeo(TestCase):
|
|||||||
# both traffic director and regular, regular is ignored
|
# both traffic director and regular, regular is ignored
|
||||||
mock.side_effect = [
|
mock.side_effect = [
|
||||||
# get traffic directors
|
# get traffic directors
|
||||||
self.traffic_directors_reponse,
|
self.traffic_directors_response,
|
||||||
# get traffic director
|
# get traffic director
|
||||||
self.traffic_director_response,
|
self.traffic_director_response,
|
||||||
# get zone
|
# get zone
|
||||||
@@ -861,7 +861,7 @@ class TestDynProviderGeo(TestCase):
|
|||||||
# busted traffic director
|
# busted traffic director
|
||||||
mock.side_effect = [
|
mock.side_effect = [
|
||||||
# get traffic directors
|
# get traffic directors
|
||||||
self.traffic_directors_reponse,
|
self.traffic_directors_response,
|
||||||
# get traffic director
|
# get traffic director
|
||||||
busted_traffic_director_response,
|
busted_traffic_director_response,
|
||||||
# get zone
|
# get zone
|
||||||
@@ -934,14 +934,14 @@ class TestDynProviderGeo(TestCase):
|
|||||||
provider = DynProvider('test', 'cust', 'user', 'pass',
|
provider = DynProvider('test', 'cust', 'user', 'pass',
|
||||||
traffic_directors_enabled=True)
|
traffic_directors_enabled=True)
|
||||||
|
|
||||||
# will be tested seperately
|
# will be tested separately
|
||||||
provider._mod_rulesets = MagicMock()
|
provider._mod_rulesets = MagicMock()
|
||||||
|
|
||||||
mock.side_effect = [
|
mock.side_effect = [
|
||||||
# create traffic director
|
# create traffic director
|
||||||
self.traffic_director_response,
|
self.traffic_director_response,
|
||||||
# get traffic directors
|
# get traffic directors
|
||||||
self.traffic_directors_reponse
|
self.traffic_directors_response
|
||||||
]
|
]
|
||||||
provider._mod_geo_Create(None, Create(self.geo_record))
|
provider._mod_geo_Create(None, Create(self.geo_record))
|
||||||
# td now lives in cache
|
# 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
|
# 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
|
# 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)
|
self.assertEqual(test_zone.records, zone.records)
|
||||||
|
|
||||||
test_zone2 = Zone('nonexistant.zone.', [])
|
test_zone2 = Zone('nonexistent.zone.', [])
|
||||||
exists = provider.populate(test_zone2, False, False)
|
exists = provider.populate(test_zone2, False, False)
|
||||||
self.assertFalse(exists)
|
self.assertFalse(exists)
|
||||||
|
|
||||||
@@ -405,8 +405,8 @@ class TestGoogleCloudProvider(TestCase):
|
|||||||
provider.gcloud_client.list_zones = Mock(
|
provider.gcloud_client.list_zones = Mock(
|
||||||
return_value=DummyIterator([]))
|
return_value=DummyIterator([]))
|
||||||
|
|
||||||
self.assertIsNone(provider.gcloud_zones.get("nonexistant.xone"),
|
self.assertIsNone(provider.gcloud_zones.get("nonexistent.zone"),
|
||||||
msg="Check that nonexistant zones return None when"
|
msg="Check that nonexistent zones return None when"
|
||||||
"there's no create=True flag")
|
"there's no create=True flag")
|
||||||
|
|
||||||
def test__get_rrsets(self):
|
def test__get_rrsets(self):
|
||||||
@@ -427,7 +427,22 @@ class TestGoogleCloudProvider(TestCase):
|
|||||||
provider.gcloud_client.list_zones = Mock(
|
provider.gcloud_client.list_zones = Mock(
|
||||||
return_value=DummyIterator([]))
|
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()
|
mock_zone.create.assert_called()
|
||||||
provider.gcloud_client.zone.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({
|
api_record.append({
|
||||||
'fieldType': 'SPF',
|
'fieldType': 'SPF',
|
||||||
'ttl': 1000,
|
'ttl': 1000,
|
||||||
'target': 'v=spf1 include:unit.texts.rerirect ~all',
|
'target': 'v=spf1 include:unit.texts.redirect ~all',
|
||||||
'subDomain': '',
|
'subDomain': '',
|
||||||
'id': 13
|
'id': 13
|
||||||
})
|
})
|
||||||
expected.add(Record.new(zone, '', {
|
expected.add(Record.new(zone, '', {
|
||||||
'ttl': 1000,
|
'ttl': 1000,
|
||||||
'type': 'SPF',
|
'type': 'SPF',
|
||||||
'value': 'v=spf1 include:unit.texts.rerirect ~all'
|
'value': 'v=spf1 include:unit.texts.redirect ~all'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
# SSHFP
|
# SSHFP
|
||||||
@@ -390,11 +390,11 @@ class TestOvhProvider(TestCase):
|
|||||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||||
subDomain=u'', target=u'1.2.3.4', ttl=100),
|
subDomain=u'', target=u'1.2.3.4', ttl=100),
|
||||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
||||||
subDomain=u'10 20 30 foo-1.unit.tests.',
|
subDomain='_srv._tcp',
|
||||||
target='_srv._tcp', ttl=800),
|
target=u'10 20 30 foo-1.unit.tests.', ttl=800),
|
||||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
||||||
subDomain=u'40 50 60 foo-2.unit.tests.',
|
subDomain='_srv._tcp',
|
||||||
target='_srv._tcp', ttl=800),
|
target=u'40 50 60 foo-2.unit.tests.', ttl=800),
|
||||||
call(u'/domain/zone/unit.tests/record', fieldType=u'PTR',
|
call(u'/domain/zone/unit.tests/record', fieldType=u'PTR',
|
||||||
subDomain='4', target=u'unit.tests.', ttl=900),
|
subDomain='4', target=u'unit.tests.', ttl=900),
|
||||||
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
|
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',
|
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
|
||||||
subDomain='www3', target=u'ns4.unit.tests.', ttl=700),
|
subDomain='www3', target=u'ns4.unit.tests.', ttl=700),
|
||||||
call(u'/domain/zone/unit.tests/record',
|
call(u'/domain/zone/unit.tests/record',
|
||||||
fieldType=u'SSHFP', target=u'', ttl=1100,
|
fieldType=u'SSHFP', subDomain=u'', ttl=1100,
|
||||||
subDomain=u'1 1 bf6b6825d2977c511a475bbefb88a'
|
target=u'1 1 bf6b6825d2977c511a475bbefb88a'
|
||||||
u'ad54'
|
u'ad54'
|
||||||
u'a92ac73',
|
u'a92ac73',
|
||||||
),
|
),
|
||||||
@@ -416,7 +416,7 @@ class TestOvhProvider(TestCase):
|
|||||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SPF',
|
call(u'/domain/zone/unit.tests/record', fieldType=u'SPF',
|
||||||
subDomain=u'', ttl=1000,
|
subDomain=u'', ttl=1000,
|
||||||
target=u'v=spf1 include:unit.texts.'
|
target=u'v=spf1 include:unit.texts.'
|
||||||
u'rerirect ~all',
|
u'redirect ~all',
|
||||||
),
|
),
|
||||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||||
subDomain='sub', target=u'1.2.3.4', ttl=200),
|
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
|
# No existing records -> creates for every record in expected
|
||||||
with requests_mock() as mock:
|
with requests_mock() as mock:
|
||||||
mock.get(ANY, status_code=200, text=EMPTY_TEXT)
|
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)
|
mock.patch(ANY, status_code=201, text=assert_rrsets_callback)
|
||||||
|
|
||||||
plan = provider.plan(expected)
|
plan = provider.plan(expected)
|
||||||
@@ -119,7 +119,7 @@ class TestPowerDnsProvider(TestCase):
|
|||||||
mock.get(ANY, status_code=422, text='')
|
mock.get(ANY, status_code=422, text='')
|
||||||
# patch 422's, unknown zone
|
# patch 422's, unknown zone
|
||||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
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)
|
mock.post(ANY, status_code=201, text=assert_rrsets_callback)
|
||||||
|
|
||||||
plan = provider.plan(expected)
|
plan = provider.plan(expected)
|
||||||
|
@@ -331,9 +331,9 @@ class TestRoute53Provider(TestCase):
|
|||||||
stubber.assert_no_pending_responses()
|
stubber.assert_no_pending_responses()
|
||||||
|
|
||||||
# Populate a zone that doesn't exist
|
# Populate a zone that doesn't exist
|
||||||
noexist = Zone('does.not.exist.', [])
|
nonexistent = Zone('does.not.exist.', [])
|
||||||
provider.populate(noexist)
|
provider.populate(nonexistent)
|
||||||
self.assertEquals(set(), noexist.records)
|
self.assertEquals(set(), nonexistent.records)
|
||||||
|
|
||||||
def test_sync(self):
|
def test_sync(self):
|
||||||
provider, stubber = self._get_stubbed_provider()
|
provider, stubber = self._get_stubbed_provider()
|
||||||
|
@@ -430,7 +430,7 @@ class TestRecord(TestCase):
|
|||||||
self.assertEqual(change.new, other)
|
self.assertEqual(change.new, other)
|
||||||
|
|
||||||
# full sorting
|
# full sorting
|
||||||
# equivilent
|
# equivalent
|
||||||
b_naptr_value = b.values[0]
|
b_naptr_value = b.values[0]
|
||||||
self.assertEquals(0, b_naptr_value.__cmp__(b_naptr_value))
|
self.assertEquals(0, b_naptr_value.__cmp__(b_naptr_value))
|
||||||
# by order
|
# by order
|
||||||
@@ -710,7 +710,7 @@ class TestRecord(TestCase):
|
|||||||
Record.new(self.zone, 'unknown', {})
|
Record.new(self.zone, 'unknown', {})
|
||||||
self.assertTrue('missing type' in ctx.exception.message)
|
self.assertTrue('missing type' in ctx.exception.message)
|
||||||
|
|
||||||
# Unkown type
|
# Unknown type
|
||||||
with self.assertRaises(Exception) as ctx:
|
with self.assertRaises(Exception) as ctx:
|
||||||
Record.new(self.zone, 'unknown', {
|
Record.new(self.zone, 'unknown', {
|
||||||
'type': 'XXX',
|
'type': 'XXX',
|
||||||
@@ -1360,7 +1360,7 @@ class TestRecordValidation(TestCase):
|
|||||||
'ttl': 600,
|
'ttl': 600,
|
||||||
'value': {
|
'value': {
|
||||||
'algorithm': 'nope',
|
'algorithm': 'nope',
|
||||||
'fingerprint_type': 1,
|
'fingerprint_type': 2,
|
||||||
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1386,7 +1386,7 @@ class TestRecordValidation(TestCase):
|
|||||||
'type': 'SSHFP',
|
'type': 'SSHFP',
|
||||||
'ttl': 600,
|
'ttl': 600,
|
||||||
'value': {
|
'value': {
|
||||||
'algorithm': 1,
|
'algorithm': 2,
|
||||||
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1398,7 +1398,7 @@ class TestRecordValidation(TestCase):
|
|||||||
'type': 'SSHFP',
|
'type': 'SSHFP',
|
||||||
'ttl': 600,
|
'ttl': 600,
|
||||||
'value': {
|
'value': {
|
||||||
'algorithm': 1,
|
'algorithm': 3,
|
||||||
'fingerprint_type': 'yeeah',
|
'fingerprint_type': 'yeeah',
|
||||||
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user