mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge remote-tracking branch 'origin/master' into python3-start
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
#
|
||||
# Do not add editor or OS specific ignores here. Have a look at adding
|
||||
# `excludesfile` to your `~/.gitconfig` to globally ignore such things.
|
||||
#
|
||||
*.pyc
|
||||
.coverage
|
||||
.env
|
||||
|
||||
+13
-1
@@ -1,3 +1,15 @@
|
||||
## v0.9.8 - 2019-09-30 - One with no changes b/c PyPi description problems
|
||||
|
||||
* No material changes
|
||||
|
||||
## v0.9.7 - 2019-09-30 - It's about time
|
||||
|
||||
* AkamaiProvider, ConstellixProvider, MythicBeastsProvider, SelectelProvider,
|
||||
& TransipPovider providers added
|
||||
* Route53Provider seperator fix
|
||||
* YamlProvider export error around stringification
|
||||
* PyPi markdown rendering fix
|
||||
|
||||
## v0.9.6 - 2019-07-16 - The little one that fixes stuff from the big one
|
||||
|
||||
* Reduced dynamic record value weight range to 0-15 so that Dyn and Route53
|
||||
@@ -112,7 +124,7 @@ Adds an OVH provider.
|
||||
|
||||
## v0.8.6 - 2017-09-06 - CAA record type,
|
||||
|
||||
Misc fixes and improvments.
|
||||
Misc fixes and improvements.
|
||||
|
||||
* Azure TXT record fix
|
||||
* PowerDNS api support for https
|
||||
|
||||
@@ -90,8 +90,8 @@ Now that we have something to tell OctoDNS about our providers & zones we need t
|
||||
ttl: 60
|
||||
type: A
|
||||
values:
|
||||
- 1.2.3.4
|
||||
- 1.2.3.5
|
||||
- 1.2.3.4
|
||||
- 1.2.3.5
|
||||
```
|
||||
|
||||
Further information can be found in [Records Documentation](/docs/records.md).
|
||||
@@ -172,7 +172,9 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| Provider | Requirements | Record Support | Dynamic/Geo Support | Notes |
|
||||
|--|--|--|--|--|
|
||||
| [AzureProvider](/octodns/provider/azuredns.py) | azure-mgmt-dns | A, AAAA, CAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
|
||||
| [Akamai](/octodns/provider/fastdns.py) | edgegrid-python | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT | No | |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | | A, AAAA, ALIAS, CAA, CNAME, MX, NS, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [ConstellixProvider](/octodns/provider/constellix.py) | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [DigitalOceanProvider](/octodns/provider/digitalocean.py) | | A, AAAA, CAA, CNAME, MX, NS, TXT, SRV | No | CAA tags restricted |
|
||||
| [DnsMadeEasyProvider](/octodns/provider/dnsmadeeasy.py) | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [DnsimpleProvider](/octodns/provider/dnsimple.py) | | All | No | CAA tags restricted |
|
||||
@@ -185,6 +187,8 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| [PowerDnsProvider](/octodns/provider/powerdns.py) | | All | No | |
|
||||
| [Rackspace](/octodns/provider/rackspace.py) | | A, AAAA, ALIAS, CNAME, MX, NS, PTR, SPF, TXT | No | |
|
||||
| [Route53](/octodns/provider/route53.py) | boto3 | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | Both | CNAME health checks don't support a Host header |
|
||||
| [Selectel](/octodns/provider/selectel.py) | | A, AAAA, CNAME, MX, NS, SPF, SRV, TXT | No | |
|
||||
| [Transip](/octodns/provider/transip.py) | transip | A, AAAA, CNAME, MX, SRV, SPF, TXT, SSHFP, CAA | No | |
|
||||
| [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
||||
| [ZoneFileSource](/octodns/source/axfr.py) | | A, AAAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
||||
| [TinyDnsFileSource](/octodns/source/tinydns.py) | | A, CNAME, MX, NS, PTR | No | read-only |
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
__VERSION__ = '0.9.6'
|
||||
__VERSION__ = '0.9.8'
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
from requests import Session
|
||||
from base64 import b64encode
|
||||
from ipaddress import ip_address
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import time
|
||||
|
||||
from ..record import Record
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
class ConstellixClientException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConstellixClientBadRequest(ConstellixClientException):
|
||||
|
||||
def __init__(self, resp):
|
||||
errors = resp.json()['errors']
|
||||
super(ConstellixClientBadRequest, self).__init__(
|
||||
'\n - {}'.format('\n - '.join(errors)))
|
||||
|
||||
|
||||
class ConstellixClientUnauthorized(ConstellixClientException):
|
||||
|
||||
def __init__(self):
|
||||
super(ConstellixClientUnauthorized, self).__init__('Unauthorized')
|
||||
|
||||
|
||||
class ConstellixClientNotFound(ConstellixClientException):
|
||||
|
||||
def __init__(self):
|
||||
super(ConstellixClientNotFound, self).__init__('Not Found')
|
||||
|
||||
|
||||
class ConstellixClient(object):
|
||||
BASE = 'https://api.dns.constellix.com/v1/domains'
|
||||
|
||||
def __init__(self, api_key, secret_key, ratelimit_delay=0.0):
|
||||
self.api_key = api_key
|
||||
self.secret_key = secret_key
|
||||
self.ratelimit_delay = ratelimit_delay
|
||||
self._sess = Session()
|
||||
self._sess.headers.update({'x-cnsdns-apiKey': self.api_key})
|
||||
self._domains = None
|
||||
|
||||
def _current_time(self):
|
||||
return str(int(time.time() * 1000))
|
||||
|
||||
def _hmac_hash(self, now):
|
||||
return hmac.new(self.secret_key.encode('utf-8'), now.encode('utf-8'),
|
||||
digestmod=hashlib.sha1).digest()
|
||||
|
||||
def _request(self, method, path, params=None, data=None):
|
||||
now = self._current_time()
|
||||
hmac_hash = self._hmac_hash(now)
|
||||
|
||||
headers = {
|
||||
'x-cnsdns-hmac': b64encode(hmac_hash),
|
||||
'x-cnsdns-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 ConstellixClientBadRequest(resp)
|
||||
if resp.status_code == 401:
|
||||
raise ConstellixClientUnauthorized()
|
||||
if resp.status_code == 404:
|
||||
raise ConstellixClientNotFound()
|
||||
resp.raise_for_status()
|
||||
time.sleep(self.ratelimit_delay)
|
||||
return resp
|
||||
|
||||
@property
|
||||
def domains(self):
|
||||
if self._domains is None:
|
||||
zones = []
|
||||
|
||||
resp = self._request('GET', '/').json()
|
||||
zones += resp
|
||||
|
||||
self._domains = {'{}.'.format(z['name']): z['id'] for z in zones}
|
||||
|
||||
return self._domains
|
||||
|
||||
def domain(self, name):
|
||||
path = '/{}'.format(self.domains.get(name))
|
||||
return self._request('GET', path).json()
|
||||
|
||||
def domain_create(self, name):
|
||||
self._request('POST', '/', data={'names': [name]})
|
||||
|
||||
def _absolutize_value(self, value, zone_name):
|
||||
if value == '':
|
||||
value = zone_name
|
||||
elif not value.endswith('.'):
|
||||
value = '{}.{}'.format(value, zone_name)
|
||||
|
||||
return value
|
||||
|
||||
def records(self, zone_name):
|
||||
zone_id = self.domains.get(zone_name, False)
|
||||
path = '/{}/records'.format(zone_id)
|
||||
|
||||
resp = self._request('GET', path).json()
|
||||
for record in resp:
|
||||
# change ANAME records to ALIAS
|
||||
if record['type'] == 'ANAME':
|
||||
record['type'] = 'ALIAS'
|
||||
|
||||
# change relative values to absolute
|
||||
value = record['value']
|
||||
if record['type'] in ['ALIAS', 'CNAME', 'MX', 'NS', 'SRV']:
|
||||
if isinstance(value, unicode):
|
||||
record['value'] = self._absolutize_value(value,
|
||||
zone_name)
|
||||
if isinstance(value, list):
|
||||
for v in value:
|
||||
v['value'] = self._absolutize_value(v['value'],
|
||||
zone_name)
|
||||
|
||||
# compress IPv6 addresses
|
||||
if record['type'] == 'AAAA':
|
||||
for i, v in enumerate(value):
|
||||
value[i] = str(ip_address(v))
|
||||
|
||||
return resp
|
||||
|
||||
def record_create(self, zone_name, record_type, params):
|
||||
# change ALIAS records to ANAME
|
||||
if record_type == 'ALIAS':
|
||||
record_type = 'ANAME'
|
||||
|
||||
zone_id = self.domains.get(zone_name, False)
|
||||
path = '/{}/records/{}'.format(zone_id, record_type)
|
||||
|
||||
self._request('POST', path, data=params)
|
||||
|
||||
def record_delete(self, zone_name, record_type, record_id):
|
||||
zone_id = self.domains.get(zone_name, False)
|
||||
path = '/{}/records/{}/{}'.format(zone_id, record_type, record_id)
|
||||
self._request('DELETE', path)
|
||||
|
||||
|
||||
class ConstellixProvider(BaseProvider):
|
||||
'''
|
||||
Constellix DNS provider
|
||||
|
||||
constellix:
|
||||
class: octodns.provider.constellix.ConstellixProvider
|
||||
# Your Contellix api key (required)
|
||||
api_key: env/CONSTELLIX_API_KEY
|
||||
# Your Constellix secret key (required)
|
||||
secret_key: env/CONSTELLIX_SECRET_KEY
|
||||
# Amount of time to wait between requests to avoid
|
||||
# ratelimit (optional)
|
||||
ratelimit_delay: 0.0
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX',
|
||||
'NS', 'PTR', 'SPF', 'SRV', 'TXT'))
|
||||
|
||||
def __init__(self, id, api_key, secret_key, ratelimit_delay=0.0,
|
||||
*args, **kwargs):
|
||||
self.log = logging.getLogger('ConstellixProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, api_key=***, secret_key=***', id)
|
||||
super(ConstellixProvider, self).__init__(id, *args, **kwargs)
|
||||
self._client = ConstellixClient(api_key, secret_key, ratelimit_delay)
|
||||
self._zone_records = {}
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
record = records[0]
|
||||
return {
|
||||
'ttl': record['ttl'],
|
||||
'type': _type,
|
||||
'values': record['value']
|
||||
}
|
||||
|
||||
_data_for_A = _data_for_multiple
|
||||
_data_for_AAAA = _data_for_multiple
|
||||
|
||||
def _data_for_CAA(self, _type, records):
|
||||
values = []
|
||||
record = records[0]
|
||||
for value in record['value']:
|
||||
values.append({
|
||||
'flags': value['flag'],
|
||||
'tag': value['tag'],
|
||||
'value': value['data']
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_NS(self, _type, records):
|
||||
record = records[0]
|
||||
return {
|
||||
'ttl': record['ttl'],
|
||||
'type': _type,
|
||||
'values': [value['value'] for value in record['value']]
|
||||
}
|
||||
|
||||
def _data_for_ALIAS(self, _type, records):
|
||||
record = records[0]
|
||||
return {
|
||||
'ttl': record['ttl'],
|
||||
'type': _type,
|
||||
'value': record['value'][0]['value']
|
||||
}
|
||||
|
||||
_data_for_PTR = _data_for_ALIAS
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
values = [value['value'].replace(';', '\\;')
|
||||
for value in records[0]['value']]
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
_data_for_SPF = _data_for_TXT
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
record = records[0]
|
||||
for value in record['value']:
|
||||
values.append({
|
||||
'preference': value['level'],
|
||||
'exchange': value['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
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
record = records[0]
|
||||
for value in record['value']:
|
||||
values.append({
|
||||
'port': value['port'],
|
||||
'priority': value['priority'],
|
||||
'target': value['value'],
|
||||
'weight': value['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 ConstellixClientNotFound:
|
||||
return []
|
||||
|
||||
return self._zone_records[zone.name]
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in self.zone_records(zone):
|
||||
_type = record['type']
|
||||
if _type not in self.SUPPORTS:
|
||||
self.log.warning('populate: skipping unsupported %s record',
|
||||
_type)
|
||||
continue
|
||||
values[record['name']][record['type']].append(record)
|
||||
|
||||
before = len(zone.records)
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
record = Record.new(zone, name, data_for(_type, records),
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record, lenient=lenient)
|
||||
|
||||
exists = zone.name in self._zone_records
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _params_for_multiple(self, record):
|
||||
yield {
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'roundRobin': [{
|
||||
'value': value
|
||||
} for value in record.values]
|
||||
}
|
||||
|
||||
_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 {
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'host': record.value,
|
||||
}
|
||||
|
||||
_params_for_CNAME = _params_for_single
|
||||
|
||||
def _params_for_ALIAS(self, record):
|
||||
yield {
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'roundRobin': [{
|
||||
'value': record.value,
|
||||
'disableFlag': False
|
||||
}]
|
||||
}
|
||||
|
||||
_params_for_PTR = _params_for_ALIAS
|
||||
|
||||
def _params_for_MX(self, record):
|
||||
values = []
|
||||
for value in record.values:
|
||||
values.append({
|
||||
'value': value.exchange,
|
||||
'level': value.preference
|
||||
})
|
||||
yield {
|
||||
'value': value.exchange,
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'roundRobin': values
|
||||
}
|
||||
|
||||
def _params_for_SRV(self, record):
|
||||
values = []
|
||||
for value in record.values:
|
||||
values.append({
|
||||
'value': value.target,
|
||||
'priority': value.priority,
|
||||
'weight': value.weight,
|
||||
'port': value.port
|
||||
})
|
||||
for value in record.values:
|
||||
yield {
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'roundRobin': values
|
||||
}
|
||||
|
||||
def _params_for_TXT(self, record):
|
||||
# Constellix does not want values escaped
|
||||
values = []
|
||||
for value in record.chunked_values:
|
||||
values.append({
|
||||
'value': value.replace('\\;', ';')
|
||||
})
|
||||
yield {
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'roundRobin': values
|
||||
}
|
||||
|
||||
_params_for_SPF = _params_for_TXT
|
||||
|
||||
def _params_for_CAA(self, record):
|
||||
values = []
|
||||
for value in record.values:
|
||||
values.append({
|
||||
'tag': value.tag,
|
||||
'data': value.value,
|
||||
'flag': value.flags,
|
||||
})
|
||||
yield {
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'roundRobin': values
|
||||
}
|
||||
|
||||
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, new._type, 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['type'],
|
||||
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))
|
||||
|
||||
try:
|
||||
self._client.domain(desired.name)
|
||||
except ConstellixClientNotFound:
|
||||
self.log.debug('_apply: no matching zone, creating domain')
|
||||
self._client.domain_create(desired.name[:-1])
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,525 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from requests import Session
|
||||
from akamai.edgegrid import EdgeGridAuth
|
||||
from urlparse import urljoin
|
||||
from collections import defaultdict
|
||||
|
||||
from logging import getLogger
|
||||
from ..record import Record
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
class AkamaiClientNotFound(Exception):
|
||||
|
||||
def __init__(self, resp):
|
||||
message = "404: Resource not found"
|
||||
super(AkamaiClientNotFound, self).__init__(message)
|
||||
|
||||
|
||||
class AkamaiClient(object):
|
||||
'''
|
||||
Client for making calls to Akamai Fast DNS API using Python Requests
|
||||
|
||||
Fast DNS Zone Management API V2, found here:
|
||||
developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html
|
||||
|
||||
Info on Python Requests library:
|
||||
https://2.python-requests.org/en/master/
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, client_secret, host, access_token, client_token):
|
||||
|
||||
self.base = "https://" + host + "/config-dns/v2/"
|
||||
|
||||
sess = Session()
|
||||
sess.auth = EdgeGridAuth(
|
||||
client_token=client_token,
|
||||
client_secret=client_secret,
|
||||
access_token=access_token
|
||||
)
|
||||
self._sess = sess
|
||||
|
||||
def _request(self, method, path, params=None, data=None, v1=False):
|
||||
|
||||
url = urljoin(self.base, path)
|
||||
resp = self._sess.request(method, url, params=params, json=data)
|
||||
|
||||
if resp.status_code == 404:
|
||||
raise AkamaiClientNotFound(resp)
|
||||
resp.raise_for_status()
|
||||
|
||||
return resp
|
||||
|
||||
def record_create(self, zone, name, record_type, content):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('POST', path, data=content)
|
||||
|
||||
return result
|
||||
|
||||
def record_delete(self, zone, name, record_type):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('DELETE', path)
|
||||
|
||||
return result
|
||||
|
||||
def record_replace(self, zone, name, record_type, content):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('PUT', path, data=content)
|
||||
|
||||
return result
|
||||
|
||||
def zone_get(self, zone):
|
||||
path = 'zones/{}'.format(zone)
|
||||
result = self._request('GET', path)
|
||||
|
||||
return result
|
||||
|
||||
def zone_create(self, contractId, params, gid=None):
|
||||
path = 'zones?contractId={}'.format(contractId)
|
||||
|
||||
if gid is not None:
|
||||
path += '&gid={}'.format(gid)
|
||||
|
||||
result = self._request('POST', path, data=params)
|
||||
|
||||
return result
|
||||
|
||||
def zone_recordset_get(self, zone, page=None, pageSize=None, search=None,
|
||||
showAll="true", sortBy="name", types=None):
|
||||
|
||||
params = {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'search': search,
|
||||
'showAll': showAll,
|
||||
'sortBy': sortBy,
|
||||
'types': types
|
||||
}
|
||||
|
||||
path = 'zones/{}/recordsets'.format(zone)
|
||||
result = self._request('GET', path, params=params)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AkamaiProvider(BaseProvider):
|
||||
|
||||
'''
|
||||
Akamai Fast DNS Provider
|
||||
|
||||
fastdns.py:
|
||||
|
||||
Example config file with variables:
|
||||
"
|
||||
---
|
||||
providers:
|
||||
config:
|
||||
class: octodns.provider.yaml.YamlProvider
|
||||
directory: ./config (example path to directory of zone files)
|
||||
fastdns:
|
||||
class: octodns.provider.fastdns.AkamaiProvider
|
||||
client_secret: env/AKAMAI_CLIENT_SECRET
|
||||
host: env/AKAMAI_HOST
|
||||
access_token: env/AKAMAI_ACCESS_TOKEN
|
||||
client_token: env/AKAMAI_CLIENT_TOKEN
|
||||
contract_id: env/AKAMAI_CONTRACT_ID (optional)
|
||||
|
||||
zones:
|
||||
example.com.:
|
||||
sources:
|
||||
- config
|
||||
targets:
|
||||
- fastdns
|
||||
"
|
||||
|
||||
The first four variables above can be hidden in environment variables
|
||||
and octoDNS will automatically search for them in the shell. It is
|
||||
possible to also hard-code into the config file: eg, contract_id.
|
||||
|
||||
The first four values can be found by generating credentials:
|
||||
https://control.akamai.com/
|
||||
Configure > Organization > Manage APIs > New API Client for me
|
||||
Select appropriate group, and fill relevant fields.
|
||||
For API Service Name, select DNS-Zone Record Management
|
||||
and then set appropriate Access level (Read-Write to make changes).
|
||||
Then select the "New Credential" button to generate values for above
|
||||
|
||||
The contract_id paramater is optional, and only required for creating
|
||||
a new zone. If the zone being managed already exists in Akamai for the
|
||||
user in question, then this paramater is not needed.
|
||||
|
||||
'''
|
||||
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SPF',
|
||||
'SRV', 'SSHFP', 'TXT'))
|
||||
|
||||
def __init__(self, id, client_secret, host, access_token, client_token,
|
||||
contract_id=None, gid=None, *args, **kwargs):
|
||||
|
||||
self.log = getLogger('AkamaiProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, ')
|
||||
super(AkamaiProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
self._dns_client = AkamaiClient(client_secret, host, access_token,
|
||||
client_token)
|
||||
|
||||
self._zone_records = {}
|
||||
self._contractId = contract_id
|
||||
self._gid = gid
|
||||
|
||||
def zone_records(self, zone):
|
||||
""" returns records for a zone, looks for it if not present, or
|
||||
returns empty [] if can't find a match
|
||||
"""
|
||||
if zone.name not in self._zone_records:
|
||||
try:
|
||||
name = zone.name[:-1]
|
||||
response = self._dns_client.zone_recordset_get(name)
|
||||
self._zone_records[zone.name] = response.json()["recordsets"]
|
||||
|
||||
except (AkamaiClientNotFound, KeyError):
|
||||
return []
|
||||
|
||||
return self._zone_records[zone.name]
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s', zone.name)
|
||||
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in self.zone_records(zone):
|
||||
|
||||
_type = record.get('type')
|
||||
# Akamai sends down prefix.zonename., while octodns expects prefix
|
||||
_name = record.get('name').split("." + zone.name[:-1], 1)[0]
|
||||
if _name == zone.name[:-1]:
|
||||
_name = '' # root / @
|
||||
|
||||
if _type not in self.SUPPORTS:
|
||||
continue
|
||||
values[_name][_type].append(record)
|
||||
|
||||
before = len(zone.records)
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
record = Record.new(zone, name, data_for(_type, records[0]),
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record, lenient=lenient)
|
||||
|
||||
exists = zone.name in self._zone_records
|
||||
found = len(zone.records) - before
|
||||
self.log.info('populate: found %s records, exists=%s', found, exists)
|
||||
|
||||
return exists
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
changes = plan.changes
|
||||
self.log.debug('apply: zone=%s, chnges=%d', desired.name, len(changes))
|
||||
|
||||
zone_name = desired.name[:-1]
|
||||
try:
|
||||
self._dns_client.zone_get(zone_name)
|
||||
|
||||
except AkamaiClientNotFound:
|
||||
self.log.info("zone not found, creating zone")
|
||||
params = self._build_zone_config(zone_name)
|
||||
self._dns_client.zone_create(self._contractId, params, self._gid)
|
||||
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
getattr(self, '_apply_{}'.format(class_name))(change)
|
||||
|
||||
# Clear out the cache if any
|
||||
self._zone_records.pop(desired.name, None)
|
||||
|
||||
def _apply_Create(self, change):
|
||||
|
||||
new = change.new
|
||||
record_type = new._type
|
||||
|
||||
params_for = getattr(self, '_params_for_{}'.format(record_type))
|
||||
values = self._get_values(new.data)
|
||||
rdata = params_for(values)
|
||||
|
||||
zone = new.zone.name[:-1]
|
||||
name = self._set_full_name(new.name, zone)
|
||||
|
||||
content = {
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"ttl": new.ttl,
|
||||
"rdata": rdata
|
||||
}
|
||||
|
||||
self._dns_client.record_create(zone, name, record_type, content)
|
||||
|
||||
return
|
||||
|
||||
def _apply_Delete(self, change):
|
||||
|
||||
zone = change.existing.zone.name[:-1]
|
||||
name = self._set_full_name(change.existing.name, zone)
|
||||
record_type = change.existing._type
|
||||
|
||||
self._dns_client.record_delete(zone, name, record_type)
|
||||
|
||||
return
|
||||
|
||||
def _apply_Update(self, change):
|
||||
|
||||
new = change.new
|
||||
record_type = new._type
|
||||
|
||||
params_for = getattr(self, '_params_for_{}'.format(record_type))
|
||||
values = self._get_values(new.data)
|
||||
rdata = params_for(values)
|
||||
|
||||
zone = new.zone.name[:-1]
|
||||
name = self._set_full_name(new.name, zone)
|
||||
|
||||
content = {
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"ttl": new.ttl,
|
||||
"rdata": rdata
|
||||
}
|
||||
|
||||
self._dns_client.record_replace(zone, name, record_type, content)
|
||||
|
||||
return
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': [r for r in records['rdata']]
|
||||
}
|
||||
|
||||
_data_for_A = _data_for_multiple
|
||||
_data_for_AAAA = _data_for_multiple
|
||||
_data_for_NS = _data_for_multiple
|
||||
_data_for_SPF = _data_for_multiple
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
value = records['rdata'][0]
|
||||
if (value[-1] != '.'):
|
||||
value = '{}.'.format(value)
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'value': value
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
preference, exchange = r.split(" ", 1)
|
||||
values.append({
|
||||
'preference': preference,
|
||||
'exchange': exchange
|
||||
})
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_NAPTR(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
order, preference, flags, service, regexp, repl = r.split(' ', 5)
|
||||
|
||||
values.append({
|
||||
'flags': flags[1:-1],
|
||||
'order': order,
|
||||
'preference': preference,
|
||||
'regexp': regexp[1:-1],
|
||||
'replacement': repl,
|
||||
'service': service[1:-1]
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_PTR(self, _type, records):
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'value': records['rdata'][0]
|
||||
}
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
priority, weight, port, target = r.split(' ', 3)
|
||||
values.append({
|
||||
'port': port,
|
||||
'priority': priority,
|
||||
'target': target,
|
||||
'weight': weight
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_SSHFP(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
algorithm, fp_type, fingerprint = r.split(' ', 2)
|
||||
values.append({
|
||||
'algorithm': algorithm,
|
||||
'fingerprint': fingerprint.lower(),
|
||||
'fingerprint_type': fp_type
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
r = r[1:-1]
|
||||
values.append(r.replace(';', '\\;'))
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _params_for_multiple(self, values):
|
||||
return [r for r in values]
|
||||
|
||||
def _params_for_single(self, values):
|
||||
return values
|
||||
|
||||
_params_for_A = _params_for_multiple
|
||||
_params_for_AAAA = _params_for_multiple
|
||||
_params_for_NS = _params_for_multiple
|
||||
|
||||
_params_for_CNAME = _params_for_single
|
||||
_params_for_PTR = _params_for_single
|
||||
|
||||
def _params_for_MX(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
preference = r['preference']
|
||||
exchange = r['exchange']
|
||||
|
||||
record = '{} {}'.format(preference, exchange)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_NAPTR(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
ordr = r['order']
|
||||
prf = r['preference']
|
||||
flg = "\"" + r['flags'] + "\""
|
||||
srvc = "\"" + r['service'] + "\""
|
||||
rgx = "\"" + r['regexp'] + "\""
|
||||
rpl = r['replacement']
|
||||
|
||||
record = '{} {} {} {} {} {}'.format(ordr, prf, flg, srvc, rgx, rpl)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SPF(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
txt = "\"" + r.replace('\\;', ';') + "\""
|
||||
rdata.append(txt)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SRV(self, values):
|
||||
rdata = []
|
||||
for r in values:
|
||||
priority = r['priority']
|
||||
weight = r['weight']
|
||||
port = r['port']
|
||||
target = r['target']
|
||||
|
||||
record = '{} {} {} {}'.format(priority, weight, port, target)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SSHFP(self, values):
|
||||
rdata = []
|
||||
for r in values:
|
||||
algorithm = r['algorithm']
|
||||
fp_type = r['fingerprint_type']
|
||||
fp = r['fingerprint']
|
||||
|
||||
record = '{} {} {}'.format(algorithm, fp_type, fp)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_TXT(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
txt = "\"" + r.replace('\\;', ';') + "\""
|
||||
rdata.append(txt)
|
||||
|
||||
return rdata
|
||||
|
||||
def _build_zone_config(self, zone, _type="primary", comment=None,
|
||||
masters=[]):
|
||||
|
||||
if self._contractId is None:
|
||||
raise NameError("contractId not specified to create zone")
|
||||
|
||||
return {
|
||||
"zone": zone,
|
||||
"type": _type,
|
||||
"comment": comment,
|
||||
"masters": masters
|
||||
}
|
||||
|
||||
def _get_values(self, data):
|
||||
|
||||
try:
|
||||
vals = data['values']
|
||||
except KeyError:
|
||||
vals = [data['value']]
|
||||
|
||||
return vals
|
||||
|
||||
def _set_full_name(self, name, zone):
|
||||
name = name + '.' + zone
|
||||
|
||||
# octodns's name for root is ''
|
||||
if (name[0] == '.'):
|
||||
name = name[1:]
|
||||
|
||||
return name
|
||||
+11
-11
@@ -536,13 +536,13 @@ def _mod_keyer(mod):
|
||||
# before all changes, followed by a "CREATE", internally in the AWS API.
|
||||
# Because of this, we order changes as follows:
|
||||
# - Delete any records that we wish to delete that are GEOS
|
||||
# (because they are never targetted by anything)
|
||||
# (because they are never targeted by anything)
|
||||
# - Delete any records that we wish to delete that are SECONDARY
|
||||
# (because they are no longer targetted by GEOS)
|
||||
# (because they are no longer targeted by GEOS)
|
||||
# - Delete any records that we wish to delete that are PRIMARY
|
||||
# (because they are no longer targetted by SECONDARY)
|
||||
# (because they are no longer targeted by SECONDARY)
|
||||
# - Delete any records that we wish to delete that are VALUES
|
||||
# (because they are no longer targetted by PRIMARY)
|
||||
# (because they are no longer targeted by PRIMARY)
|
||||
# - CREATE/UPSERT any records that are VALUES
|
||||
# (because they don't depend on other records)
|
||||
# - CREATE/UPSERT any records that are PRIMARY
|
||||
@@ -731,7 +731,7 @@ class Route53Provider(BaseProvider):
|
||||
def _data_for_CAA(self, rrset):
|
||||
values = []
|
||||
for rr in rrset['ResourceRecords']:
|
||||
flags, tag, value = rr['Value'].split(' ')
|
||||
flags, tag, value = rr['Value'].split()
|
||||
values.append({
|
||||
'flags': flags,
|
||||
'tag': tag,
|
||||
@@ -769,7 +769,7 @@ class Route53Provider(BaseProvider):
|
||||
def _data_for_MX(self, rrset):
|
||||
values = []
|
||||
for rr in rrset['ResourceRecords']:
|
||||
preference, exchange = rr['Value'].split(' ')
|
||||
preference, exchange = rr['Value'].split()
|
||||
values.append({
|
||||
'preference': preference,
|
||||
'exchange': exchange,
|
||||
@@ -784,7 +784,7 @@ class Route53Provider(BaseProvider):
|
||||
values = []
|
||||
for rr in rrset['ResourceRecords']:
|
||||
order, preference, flags, service, regexp, replacement = \
|
||||
rr['Value'].split(' ')
|
||||
rr['Value'].split()
|
||||
flags = flags[1:-1]
|
||||
service = service[1:-1]
|
||||
regexp = regexp[1:-1]
|
||||
@@ -812,7 +812,7 @@ class Route53Provider(BaseProvider):
|
||||
def _data_for_SRV(self, rrset):
|
||||
values = []
|
||||
for rr in rrset['ResourceRecords']:
|
||||
priority, weight, port, target = rr['Value'].split(' ')
|
||||
priority, weight, port, target = rr['Value'].split()
|
||||
values.append({
|
||||
'priority': priority,
|
||||
'weight': weight,
|
||||
@@ -1036,7 +1036,7 @@ class Route53Provider(BaseProvider):
|
||||
.get('healthcheck', {}) \
|
||||
.get('measure_latency', True)
|
||||
|
||||
def _health_check_equivilent(self, host, path, protocol, port,
|
||||
def _health_check_equivalent(self, host, path, protocol, port,
|
||||
measure_latency, health_check, value=None):
|
||||
config = health_check['HealthCheckConfig']
|
||||
|
||||
@@ -1088,7 +1088,7 @@ class Route53Provider(BaseProvider):
|
||||
if not health_check['CallerReference'].startswith(expected_ref):
|
||||
# not match, ignore
|
||||
continue
|
||||
if self._health_check_equivilent(healthcheck_host,
|
||||
if self._health_check_equivalent(healthcheck_host,
|
||||
healthcheck_path,
|
||||
healthcheck_protocol,
|
||||
healthcheck_port,
|
||||
@@ -1245,7 +1245,7 @@ class Route53Provider(BaseProvider):
|
||||
health_check = self.health_checks[health_check_id]
|
||||
caller_ref = health_check['CallerReference']
|
||||
if caller_ref.startswith(self.HEALTH_CHECK_VERSION):
|
||||
if self._health_check_equivilent(healthcheck_host,
|
||||
if self._health_check_equivalent(healthcheck_host,
|
||||
healthcheck_path,
|
||||
healthcheck_protocol,
|
||||
healthcheck_port,
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from requests import Session
|
||||
|
||||
from ..record import Record, Update
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
class SelectelAuthenticationRequired(Exception):
|
||||
def __init__(self, msg):
|
||||
message = 'Authorization failed. Invalid or empty token.'
|
||||
super(SelectelAuthenticationRequired, self).__init__(message)
|
||||
|
||||
|
||||
class SelectelProvider(BaseProvider):
|
||||
SUPPORTS_GEO = False
|
||||
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SPF', 'SRV'))
|
||||
|
||||
MIN_TTL = 60
|
||||
|
||||
PAGINATION_LIMIT = 50
|
||||
|
||||
API_URL = 'https://api.selectel.ru/domains/v1'
|
||||
|
||||
def __init__(self, id, token, *args, **kwargs):
|
||||
self.log = getLogger('SelectelProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s', id)
|
||||
super(SelectelProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
self._sess = Session()
|
||||
self._sess.headers.update({
|
||||
'X-Token': token,
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
self._zone_records = {}
|
||||
self._domain_list = self.domain_list()
|
||||
self._zones = None
|
||||
|
||||
def _request(self, method, path, params=None, data=None):
|
||||
self.log.debug('_request: method=%s, path=%s', method, path)
|
||||
|
||||
url = '{}{}'.format(self.API_URL, path)
|
||||
resp = self._sess.request(method, url, params=params, json=data)
|
||||
|
||||
self.log.debug('_request: status=%s', resp.status_code)
|
||||
if resp.status_code == 401:
|
||||
raise SelectelAuthenticationRequired(resp.text)
|
||||
elif resp.status_code == 404:
|
||||
return {}
|
||||
resp.raise_for_status()
|
||||
if method == 'DELETE':
|
||||
return {}
|
||||
return resp.json()
|
||||
|
||||
def _get_total_count(self, path):
|
||||
url = '{}{}'.format(self.API_URL, path)
|
||||
resp = self._sess.request('HEAD', url)
|
||||
return int(resp.headers['X-Total-Count'])
|
||||
|
||||
def _request_with_pagination(self, path, total_count):
|
||||
result = []
|
||||
for offset in range(0, total_count, self.PAGINATION_LIMIT):
|
||||
result += self._request('GET', path,
|
||||
params={'limit': self.PAGINATION_LIMIT,
|
||||
'offset': offset})
|
||||
return result
|
||||
|
||||
def _include_change(self, change):
|
||||
if isinstance(change, Update):
|
||||
existing = change.existing.data
|
||||
new = change.new.data
|
||||
new['ttl'] = max(self.MIN_TTL, new['ttl'])
|
||||
if new == existing:
|
||||
self.log.debug('_include_changes: new=%s, found existing=%s',
|
||||
new, existing)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
changes = plan.changes
|
||||
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
|
||||
len(changes))
|
||||
|
||||
zone_name = desired.name[:-1]
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
getattr(self, '_apply_{}'.format(class_name).lower())(zone_name,
|
||||
change)
|
||||
|
||||
def _apply_create(self, zone_name, change):
|
||||
new = change.new
|
||||
params_for = getattr(self, '_params_for_{}'.format(new._type))
|
||||
for params in params_for(new):
|
||||
self.create_record(zone_name, params)
|
||||
|
||||
def _apply_update(self, zone_name, change):
|
||||
self._apply_delete(zone_name, change)
|
||||
self._apply_create(zone_name, change)
|
||||
|
||||
def _apply_delete(self, zone_name, change):
|
||||
existing = change.existing
|
||||
self.delete_record(zone_name, existing._type, existing.name)
|
||||
|
||||
def _params_for_multiple(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'content': value,
|
||||
'name': record.fqdn,
|
||||
'ttl': max(self.MIN_TTL, record.ttl),
|
||||
'type': record._type,
|
||||
}
|
||||
|
||||
def _params_for_single(self, record):
|
||||
yield {
|
||||
'content': record.value,
|
||||
'name': record.fqdn,
|
||||
'ttl': max(self.MIN_TTL, record.ttl),
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
def _params_for_MX(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'content': value.exchange,
|
||||
'name': record.fqdn,
|
||||
'ttl': max(self.MIN_TTL, record.ttl),
|
||||
'type': record._type,
|
||||
'priority': value.preference
|
||||
}
|
||||
|
||||
def _params_for_SRV(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'name': record.fqdn,
|
||||
'target': value.target,
|
||||
'ttl': max(self.MIN_TTL, record.ttl),
|
||||
'type': record._type,
|
||||
'port': value.port,
|
||||
'weight': value.weight,
|
||||
'priority': value.priority
|
||||
}
|
||||
|
||||
_params_for_A = _params_for_multiple
|
||||
_params_for_AAAA = _params_for_multiple
|
||||
_params_for_NS = _params_for_multiple
|
||||
_params_for_TXT = _params_for_multiple
|
||||
_params_for_SPF = _params_for_multiple
|
||||
|
||||
_params_for_CNAME = _params_for_single
|
||||
|
||||
def _data_for_A(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': [r['content'] for r in records],
|
||||
}
|
||||
|
||||
_data_for_AAAA = _data_for_A
|
||||
|
||||
def _data_for_NS(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': ['{}.'.format(r['content']) for r in records],
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
values.append({
|
||||
'preference': record['priority'],
|
||||
'exchange': '{}.'.format(record['content']),
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
only = records[0]
|
||||
return {
|
||||
'ttl': only['ttl'],
|
||||
'type': _type,
|
||||
'value': '{}.'.format(only['content'])
|
||||
}
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': [r['content'] for r in records],
|
||||
}
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
values.append({
|
||||
'priority': record['priority'],
|
||||
'weight': record['weight'],
|
||||
'port': record['port'],
|
||||
'target': '{}.'.format(record['target']),
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records[0]['ttl'],
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s',
|
||||
zone.name, target, lenient)
|
||||
before = len(zone.records)
|
||||
records = self.zone_records(zone)
|
||||
if records:
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in records:
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
_type = record['type']
|
||||
if _type in self.SUPPORTS:
|
||||
values[name][record['type']].append(record)
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
data = data_for(_type, records)
|
||||
record = Record.new(zone, name, data, source=self,
|
||||
lenient=lenient)
|
||||
zone.add_record(record)
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
|
||||
def domain_list(self):
|
||||
path = '/'
|
||||
domains = {}
|
||||
domains_list = []
|
||||
|
||||
total_count = self._get_total_count(path)
|
||||
domains_list = self._request_with_pagination(path, total_count)
|
||||
|
||||
for domain in domains_list:
|
||||
domains[domain['name']] = domain
|
||||
return domains
|
||||
|
||||
def zone_records(self, zone):
|
||||
path = '/{}/records/'.format(zone.name[:-1])
|
||||
zone_records = []
|
||||
|
||||
total_count = self._get_total_count(path)
|
||||
zone_records = self._request_with_pagination(path, total_count)
|
||||
|
||||
self._zone_records[zone.name] = zone_records
|
||||
return self._zone_records[zone.name]
|
||||
|
||||
def create_domain(self, name, zone=""):
|
||||
path = '/'
|
||||
|
||||
data = {
|
||||
'name': name,
|
||||
'bind_zone': zone,
|
||||
}
|
||||
|
||||
resp = self._request('POST', path, data=data)
|
||||
self._domain_list[name] = resp
|
||||
return resp
|
||||
|
||||
def create_record(self, zone_name, data):
|
||||
self.log.debug('Create record. Zone: %s, data %s', zone_name, data)
|
||||
if zone_name in self._domain_list.keys():
|
||||
domain_id = self._domain_list[zone_name]['id']
|
||||
else:
|
||||
domain_id = self.create_domain(zone_name)['id']
|
||||
|
||||
path = '/{}/records/'.format(domain_id)
|
||||
return self._request('POST', path, data=data)
|
||||
|
||||
def delete_record(self, domain, _type, zone):
|
||||
self.log.debug('Delete record. Domain: %s, Type: %s', domain, _type)
|
||||
|
||||
domain_id = self._domain_list[domain]['id']
|
||||
records = self._zone_records.get('{}.'.format(domain), False)
|
||||
if not records:
|
||||
path = '/{}/records/'.format(domain_id)
|
||||
records = self._request('GET', path)
|
||||
|
||||
for record in records:
|
||||
full_domain = domain
|
||||
if zone:
|
||||
full_domain = '{}{}'.format(zone, domain)
|
||||
if record['type'] == _type and record['name'] == full_domain:
|
||||
path = '/{}/records/{}'.format(domain_id, record['id'])
|
||||
return self._request('DELETE', path)
|
||||
|
||||
self.log.debug('Delete record failed (Record not found)')
|
||||
@@ -0,0 +1,353 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from suds import WebFault
|
||||
|
||||
from collections import defaultdict
|
||||
from .base import BaseProvider
|
||||
from logging import getLogger
|
||||
from ..record import Record
|
||||
from transip.service.domain import DomainService
|
||||
from transip.service.objects import DnsEntry
|
||||
|
||||
|
||||
class TransipException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TransipConfigException(TransipException):
|
||||
pass
|
||||
|
||||
|
||||
class TransipNewZoneException(TransipException):
|
||||
pass
|
||||
|
||||
|
||||
class TransipProvider(BaseProvider):
|
||||
'''
|
||||
Transip DNS provider
|
||||
|
||||
transip:
|
||||
class: octodns.provider.transip.TransipProvider
|
||||
# Your Transip account name (required)
|
||||
account: yourname
|
||||
# Path to a private key file (required if key is not used)
|
||||
key_file: /path/to/file
|
||||
# The api key as string (required if key_file is not used)
|
||||
key: |
|
||||
\'''
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...
|
||||
-----END PRIVATE KEY-----
|
||||
\'''
|
||||
# if both `key_file` and `key` are presented `key_file` is used
|
||||
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(
|
||||
('A', 'AAAA', 'CNAME', 'MX', 'SRV', 'SPF', 'TXT', 'SSHFP', 'CAA'))
|
||||
# unsupported by OctoDNS: 'TLSA'
|
||||
MIN_TTL = 120
|
||||
TIMEOUT = 15
|
||||
ROOT_RECORD = '@'
|
||||
|
||||
def __init__(self, id, account, key=None, key_file=None, *args, **kwargs):
|
||||
self.log = getLogger('TransipProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, account=%s, token=***', id,
|
||||
account)
|
||||
super(TransipProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
if key_file is not None:
|
||||
self._client = DomainService(account, private_key_file=key_file)
|
||||
elif key is not None:
|
||||
self._client = DomainService(account, private_key=key)
|
||||
else:
|
||||
raise TransipConfigException(
|
||||
'Missing `key` of `key_file` parameter in config'
|
||||
)
|
||||
|
||||
self.account = account
|
||||
self.key = key
|
||||
|
||||
self._currentZone = {}
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
|
||||
exists = False
|
||||
self._currentZone = zone
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
|
||||
before = len(zone.records)
|
||||
try:
|
||||
zoneInfo = self._client.get_info(zone.name[:-1])
|
||||
except WebFault as e:
|
||||
if e.fault.faultcode == '102' and target is False:
|
||||
# Zone not found in account, and not a target so just
|
||||
# leave an empty zone.
|
||||
return exists
|
||||
elif e.fault.faultcode == '102' and target is True:
|
||||
self.log.warning('populate: Transip can\'t create new zones')
|
||||
raise TransipNewZoneException(
|
||||
('populate: ({}) Transip used ' +
|
||||
'as target for non-existing zone: {}').format(
|
||||
e.fault.faultcode, zone.name))
|
||||
else:
|
||||
self.log.error('populate: (%s) %s ', e.fault.faultcode,
|
||||
e.fault.faultstring)
|
||||
raise e
|
||||
|
||||
self.log.debug('populate: found %s records for zone %s',
|
||||
len(zoneInfo.dnsEntries), zone.name)
|
||||
exists = True
|
||||
if zoneInfo.dnsEntries:
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in zoneInfo.dnsEntries:
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
if name == self.ROOT_RECORD:
|
||||
name = ''
|
||||
|
||||
if record['type'] in self.SUPPORTS:
|
||||
values[name][record['type']].append(record)
|
||||
|
||||
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, lenient=lenient)
|
||||
self.log.info('populate: found %s records, exists = %s',
|
||||
len(zone.records) - before, exists)
|
||||
|
||||
self._currentZone = {}
|
||||
return exists
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
changes = plan.changes
|
||||
self.log.debug('apply: zone=%s, changes=%d', desired.name,
|
||||
len(changes))
|
||||
|
||||
self._currentZone = plan.desired
|
||||
try:
|
||||
self._client.get_info(plan.desired.name[:-1])
|
||||
except WebFault as e:
|
||||
self.log.warning('_apply: %s ', e.message)
|
||||
raise e
|
||||
|
||||
_dns_entries = []
|
||||
for record in plan.desired.records:
|
||||
if record._type in self.SUPPORTS:
|
||||
entries_for = getattr(self,
|
||||
'_entries_for_{}'.format(record._type))
|
||||
|
||||
# Root records have '@' as name
|
||||
name = record.name
|
||||
if name == '':
|
||||
name = self.ROOT_RECORD
|
||||
|
||||
_dns_entries.extend(entries_for(name, record))
|
||||
|
||||
try:
|
||||
self._client.set_dns_entries(plan.desired.name[:-1], _dns_entries)
|
||||
except WebFault as e:
|
||||
self.log.warning(('_apply: Set DNS returned ' +
|
||||
'one or more errors: {}').format(
|
||||
e.fault.faultstring))
|
||||
raise TransipException(200, e.fault.faultstring)
|
||||
|
||||
self._currentZone = {}
|
||||
|
||||
def _entries_for_multiple(self, name, record):
|
||||
_entries = []
|
||||
|
||||
for value in record.values:
|
||||
_entries.append(DnsEntry(name, record.ttl, record._type, value))
|
||||
|
||||
return _entries
|
||||
|
||||
def _entries_for_single(self, name, record):
|
||||
|
||||
return [DnsEntry(name, record.ttl, record._type, record.value)]
|
||||
|
||||
_entries_for_A = _entries_for_multiple
|
||||
_entries_for_AAAA = _entries_for_multiple
|
||||
_entries_for_NS = _entries_for_multiple
|
||||
_entries_for_SPF = _entries_for_multiple
|
||||
_entries_for_CNAME = _entries_for_single
|
||||
|
||||
def _entries_for_MX(self, name, record):
|
||||
_entries = []
|
||||
|
||||
for value in record.values:
|
||||
content = "{} {}".format(value.preference, value.exchange)
|
||||
_entries.append(DnsEntry(name, record.ttl, record._type, content))
|
||||
|
||||
return _entries
|
||||
|
||||
def _entries_for_SRV(self, name, record):
|
||||
_entries = []
|
||||
|
||||
for value in record.values:
|
||||
content = "{} {} {} {}".format(value.priority, value.weight,
|
||||
value.port, value.target)
|
||||
_entries.append(DnsEntry(name, record.ttl, record._type, content))
|
||||
|
||||
return _entries
|
||||
|
||||
def _entries_for_SSHFP(self, name, record):
|
||||
_entries = []
|
||||
|
||||
for value in record.values:
|
||||
content = "{} {} {}".format(value.algorithm,
|
||||
value.fingerprint_type,
|
||||
value.fingerprint)
|
||||
_entries.append(DnsEntry(name, record.ttl, record._type, content))
|
||||
|
||||
return _entries
|
||||
|
||||
def _entries_for_CAA(self, name, record):
|
||||
_entries = []
|
||||
|
||||
for value in record.values:
|
||||
content = "{} {} {}".format(value.flags, value.tag,
|
||||
value.value)
|
||||
_entries.append(DnsEntry(name, record.ttl, record._type, content))
|
||||
|
||||
return _entries
|
||||
|
||||
def _entries_for_TXT(self, name, record):
|
||||
_entries = []
|
||||
|
||||
for value in record.values:
|
||||
value = value.replace('\\;', ';')
|
||||
_entries.append(DnsEntry(name, record.ttl, record._type, value))
|
||||
|
||||
return _entries
|
||||
|
||||
def _parse_to_fqdn(self, value):
|
||||
|
||||
# Enforce switch from suds.sax.text.Text to string
|
||||
value = str(value)
|
||||
|
||||
# TransIP allows '@' as value to alias the root record.
|
||||
# this provider won't set an '@' value, but can be an existing record
|
||||
if value == self.ROOT_RECORD:
|
||||
value = self._currentZone.name
|
||||
|
||||
if value[-1] != '.':
|
||||
self.log.debug('parseToFQDN: changed %s to %s', value,
|
||||
'{}.{}'.format(value, self._currentZone.name))
|
||||
value = '{}.{}'.format(value, self._currentZone.name)
|
||||
|
||||
return value
|
||||
|
||||
def _get_lowest_ttl(self, records):
|
||||
_ttl = 100000
|
||||
for record in records:
|
||||
_ttl = min(_ttl, record['expire'])
|
||||
return _ttl
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
|
||||
_values = []
|
||||
for record in records:
|
||||
# Enforce switch from suds.sax.text.Text to string
|
||||
_values.append(str(record['content']))
|
||||
|
||||
return {
|
||||
'ttl': self._get_lowest_ttl(records),
|
||||
'type': _type,
|
||||
'values': _values
|
||||
}
|
||||
|
||||
_data_for_A = _data_for_multiple
|
||||
_data_for_AAAA = _data_for_multiple
|
||||
_data_for_NS = _data_for_multiple
|
||||
_data_for_SPF = _data_for_multiple
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['expire'],
|
||||
'type': _type,
|
||||
'value': self._parse_to_fqdn(records[0]['content'])
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
_values = []
|
||||
for record in records:
|
||||
preference, exchange = record['content'].split(" ", 1)
|
||||
_values.append({
|
||||
'preference': preference,
|
||||
'exchange': self._parse_to_fqdn(exchange)
|
||||
})
|
||||
return {
|
||||
'ttl': self._get_lowest_ttl(records),
|
||||
'type': _type,
|
||||
'values': _values
|
||||
}
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
_values = []
|
||||
for record in records:
|
||||
priority, weight, port, target = record['content'].split(' ', 3)
|
||||
_values.append({
|
||||
'port': port,
|
||||
'priority': priority,
|
||||
'target': self._parse_to_fqdn(target),
|
||||
'weight': weight
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': self._get_lowest_ttl(records),
|
||||
'values': _values
|
||||
}
|
||||
|
||||
def _data_for_SSHFP(self, _type, records):
|
||||
_values = []
|
||||
for record in records:
|
||||
algorithm, fp_type, fingerprint = record['content'].split(' ', 2)
|
||||
_values.append({
|
||||
'algorithm': algorithm,
|
||||
'fingerprint': fingerprint.lower(),
|
||||
'fingerprint_type': fp_type
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': self._get_lowest_ttl(records),
|
||||
'values': _values
|
||||
}
|
||||
|
||||
def _data_for_CAA(self, _type, records):
|
||||
_values = []
|
||||
for record in records:
|
||||
flags, tag, value = record['content'].split(' ', 2)
|
||||
_values.append({
|
||||
'flags': flags,
|
||||
'tag': tag,
|
||||
'value': value
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': self._get_lowest_ttl(records),
|
||||
'values': _values
|
||||
}
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
_values = []
|
||||
for record in records:
|
||||
_values.append(record['content'].replace(';', '\\;'))
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': self._get_lowest_ttl(records),
|
||||
'values': _values
|
||||
}
|
||||
@@ -5,5 +5,6 @@ pycodestyle==2.4.0
|
||||
pycountry>=18.12.8
|
||||
pycountry_convert>=0.7.2
|
||||
pyflakes==1.6.0
|
||||
readme_renderer[md]==24.0
|
||||
requests_mock
|
||||
twine==1.13.0
|
||||
|
||||
+3
-1
@@ -7,6 +7,7 @@ dnspython==1.15.0
|
||||
docutils==0.14
|
||||
dyn==1.8.1
|
||||
futures==3.2.0; python_version < '3.0'
|
||||
edgegrid-python==1.1.1
|
||||
google-cloud-core==0.28.1
|
||||
google-cloud-dns==0.29.0
|
||||
incf.countryutils==1.0
|
||||
@@ -17,7 +18,8 @@ natsort==5.5.0
|
||||
nsone==0.9.100
|
||||
ovh==0.4.8
|
||||
python-dateutil==2.6.1
|
||||
requests==2.20.0
|
||||
requests==2.22.0
|
||||
s3transfer==0.1.13
|
||||
six==1.12.0
|
||||
setuptools==38.5.2
|
||||
transip==2.0.0
|
||||
|
||||
@@ -22,5 +22,6 @@ git tag -s "v$VERSION" -m "Release $VERSION"
|
||||
git push origin "v$VERSION"
|
||||
echo "Tagged and pushed v$VERSION"
|
||||
python setup.py sdist
|
||||
twine check dist/*$VERSION.tar.gz
|
||||
twine upload dist/*$VERSION.tar.gz
|
||||
echo "Uploaded $VERSION"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from StringIO import StringIO
|
||||
from os.path import dirname, join
|
||||
import octodns
|
||||
|
||||
@@ -21,6 +22,39 @@ console_scripts = {
|
||||
for name in cmds
|
||||
}
|
||||
|
||||
|
||||
def long_description():
|
||||
buf = StringIO()
|
||||
yaml_block = False
|
||||
supported_providers = False
|
||||
with open('README.md') as fh:
|
||||
for line in fh:
|
||||
if line == '```yaml\n':
|
||||
yaml_block = True
|
||||
continue
|
||||
elif yaml_block and line == '---\n':
|
||||
# skip the line
|
||||
continue
|
||||
elif yaml_block and line == '```\n':
|
||||
yaml_block = False
|
||||
continue
|
||||
elif supported_providers:
|
||||
if line.startswith('## '):
|
||||
supported_providers = False
|
||||
# write this line out, no continue
|
||||
else:
|
||||
# We're ignoring this one
|
||||
continue
|
||||
elif line == '## Supported providers\n':
|
||||
supported_providers = True
|
||||
continue
|
||||
buf.write(line)
|
||||
buf = buf.getvalue()
|
||||
with open('/tmp/mod', 'w') as fh:
|
||||
fh.write(buf)
|
||||
return buf
|
||||
|
||||
|
||||
setup(
|
||||
author='Ross McFarland',
|
||||
author_email='[email protected]',
|
||||
@@ -40,7 +74,7 @@ setup(
|
||||
'requests>=2.20.0'
|
||||
],
|
||||
license='MIT',
|
||||
long_description=open('README.md').read(),
|
||||
long_description=long_description(),
|
||||
long_description_content_type='text/markdown',
|
||||
name='octodns',
|
||||
packages=find_packages(),
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
[{
|
||||
"id": 123123,
|
||||
"name": "unit.tests",
|
||||
"soa": {
|
||||
"primaryNameserver": "ns11.constellix.com.",
|
||||
"email": "dns.constellix.com.",
|
||||
"ttl": 86400,
|
||||
"serial": 2015010102,
|
||||
"refresh": 43200,
|
||||
"retry": 3600,
|
||||
"expire": 1209600,
|
||||
"negCache": 180
|
||||
},
|
||||
"createdTs": "2019-08-07T03:36:02Z",
|
||||
"modifiedTs": "2019-08-07T03:36:02Z",
|
||||
"typeId": 1,
|
||||
"domainTags": [],
|
||||
"folder": null,
|
||||
"hasGtdRegions": false,
|
||||
"hasGeoIP": false,
|
||||
"nameserverGroup": 1,
|
||||
"nameservers": ["ns11.constellix.com.", "ns21.constellix.com.", "ns31.constellix.com.", "ns41.constellix.net.", "ns51.constellix.net.", "ns61.constellix.net."],
|
||||
"note": "",
|
||||
"version": 0,
|
||||
"status": "ACTIVE",
|
||||
"tags": [],
|
||||
"contactIds": []
|
||||
}]
|
||||
+598
@@ -0,0 +1,598 @@
|
||||
[{
|
||||
"id": 1808529,
|
||||
"type": "CAA",
|
||||
"recordType": "caa",
|
||||
"name": "",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 3600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149569216,
|
||||
"value": [{
|
||||
"flag": 0,
|
||||
"tag": "issue",
|
||||
"data": "ca.unit.tests",
|
||||
"caaProviderId": 1,
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"flag": 0,
|
||||
"tag": "issue",
|
||||
"data": "ca.unit.tests",
|
||||
"caaProviderId": 1,
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808516,
|
||||
"type": "A",
|
||||
"recordType": "a",
|
||||
"name": "",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 300,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149623640,
|
||||
"value": ["1.2.3.4", "1.2.3.5"],
|
||||
"roundRobin": [{
|
||||
"value": "1.2.3.4",
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "1.2.3.5",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"geolocation": null,
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"roundRobinFailover": [],
|
||||
"pools": [],
|
||||
"poolsDetail": []
|
||||
}, {
|
||||
"id": 1808527,
|
||||
"type": "SRV",
|
||||
"recordType": "srv",
|
||||
"name": "_srv._tcp",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149714387,
|
||||
"value": [{
|
||||
"value": "foo-1.unit.tests.",
|
||||
"priority": 10,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "foo-2.unit.tests.",
|
||||
"priority": 12,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "foo-1.unit.tests.",
|
||||
"priority": 10,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "foo-2.unit.tests.",
|
||||
"priority": 12,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808515,
|
||||
"type": "AAAA",
|
||||
"recordType": "aaaa",
|
||||
"name": "aaaa",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149739464,
|
||||
"value": ["2601:644:500:e210:62f8:1dff:feb8:947a"],
|
||||
"roundRobin": [{
|
||||
"value": "2601:644:500:e210:62f8:1dff:feb8:947a",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"geolocation": null,
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"pools": [],
|
||||
"poolsDetail": [],
|
||||
"roundRobinFailover": []
|
||||
}, {
|
||||
"id": 1808530,
|
||||
"type": "ANAME",
|
||||
"recordType": "aname",
|
||||
"name": "",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 1800,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565150251379,
|
||||
"value": [{
|
||||
"value": "aname.unit.tests.",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "aname.unit.tests.",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"geolocation": null,
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"pools": [],
|
||||
"poolsDetail": []
|
||||
}, {
|
||||
"id": 1808521,
|
||||
"type": "CNAME",
|
||||
"recordType": "cname",
|
||||
"name": "cname",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 300,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565152113825,
|
||||
"value": "",
|
||||
"roundRobin": [{
|
||||
"value": "",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": [{
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 1,
|
||||
"markedActive": false
|
||||
}, {
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 2,
|
||||
"markedActive": false
|
||||
}]
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": [{
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 1,
|
||||
"markedActive": false
|
||||
}, {
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 2,
|
||||
"markedActive": false
|
||||
}]
|
||||
},
|
||||
"pools": [],
|
||||
"poolsDetail": [],
|
||||
"geolocation": null,
|
||||
"host": ""
|
||||
}, {
|
||||
"id": 1808522,
|
||||
"type": "CNAME",
|
||||
"recordType": "cname",
|
||||
"name": "included",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 3600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565152119137,
|
||||
"value": "",
|
||||
"roundRobin": [{
|
||||
"value": "",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": [{
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 1,
|
||||
"markedActive": false
|
||||
}, {
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 2,
|
||||
"markedActive": false
|
||||
}]
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": [{
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 1,
|
||||
"markedActive": false
|
||||
}, {
|
||||
"id": null,
|
||||
"value": "",
|
||||
"disableFlag": false,
|
||||
"failedFlag": false,
|
||||
"status": "N/A",
|
||||
"sortOrder": 2,
|
||||
"markedActive": false
|
||||
}]
|
||||
},
|
||||
"pools": [],
|
||||
"poolsDetail": [],
|
||||
"geolocation": null,
|
||||
"host": ""
|
||||
}, {
|
||||
"id": 1808523,
|
||||
"type": "MX",
|
||||
"recordType": "mx",
|
||||
"name": "mx",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 300,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149879856,
|
||||
"value": [{
|
||||
"value": "smtp-3.unit.tests.",
|
||||
"level": 30,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "smtp-2.unit.tests.",
|
||||
"level": 20,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "smtp-4.unit.tests.",
|
||||
"level": 10,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "smtp-1.unit.tests.",
|
||||
"level": 40,
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "smtp-3.unit.tests.",
|
||||
"level": 30,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "smtp-2.unit.tests.",
|
||||
"level": 20,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "smtp-4.unit.tests.",
|
||||
"level": 10,
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "smtp-1.unit.tests.",
|
||||
"level": 40,
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808525,
|
||||
"type": "PTR",
|
||||
"recordType": "ptr",
|
||||
"name": "ptr",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 300,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565150115139,
|
||||
"value": [{
|
||||
"value": "foo.bar.com.",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "foo.bar.com.",
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808526,
|
||||
"type": "SPF",
|
||||
"recordType": "spf",
|
||||
"name": "spf",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149916132,
|
||||
"value": [{
|
||||
"value": "\"v=spf1 ip4:192.168.0.1/16-all\"",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "\"v=spf1 ip4:192.168.0.1/16-all\"",
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808528,
|
||||
"type": "TXT",
|
||||
"recordType": "txt",
|
||||
"name": "txt",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149966915,
|
||||
"value": [{
|
||||
"value": "\"Bah bah black sheep\"",
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "\"have you any wool.\"",
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "\"Bah bah black sheep\"",
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "\"have you any wool.\"",
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"",
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808524,
|
||||
"type": "NS",
|
||||
"recordType": "ns",
|
||||
"name": "under",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 3600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565150062850,
|
||||
"value": [{
|
||||
"value": "ns1.unit.tests.",
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "ns2",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "ns1.unit.tests.",
|
||||
"disableFlag": false
|
||||
}, {
|
||||
"value": "ns2",
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808531,
|
||||
"type": "HTTPRedirection",
|
||||
"recordType": "httpredirection",
|
||||
"name": "unsupported",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 300,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565150348154,
|
||||
"value": "https://redirect.unit.tests",
|
||||
"roundRobin": [{
|
||||
"value": "https://redirect.unit.tests"
|
||||
}],
|
||||
"title": "Unsupported Record",
|
||||
"keywords": "unsupported",
|
||||
"description": "unsupported record",
|
||||
"hardlinkFlag": false,
|
||||
"redirectTypeId": 1,
|
||||
"url": "https://redirect.unit.tests"
|
||||
}, {
|
||||
"id": 1808519,
|
||||
"type": "A",
|
||||
"recordType": "a",
|
||||
"name": "www",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 300,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565150079027,
|
||||
"value": ["2.2.3.6"],
|
||||
"roundRobin": [{
|
||||
"value": "2.2.3.6",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"geolocation": null,
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"roundRobinFailover": [],
|
||||
"pools": [],
|
||||
"poolsDetail": []
|
||||
}, {
|
||||
"id": 1808603,
|
||||
"type": "ANAME",
|
||||
"recordType": "aname",
|
||||
"name": "sub",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 1800,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565153387855,
|
||||
"value": [{
|
||||
"value": "aname.unit.tests.",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": "aname.unit.tests.",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"geolocation": null,
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"pools": [],
|
||||
"poolsDetail": []
|
||||
}, {
|
||||
"id": 1808520,
|
||||
"type": "A",
|
||||
"recordType": "a",
|
||||
"name": "www.sub",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 300,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565150090588,
|
||||
"value": ["2.2.3.6"],
|
||||
"roundRobin": [{
|
||||
"value": "2.2.3.6",
|
||||
"disableFlag": false
|
||||
}],
|
||||
"geolocation": null,
|
||||
"recordFailover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"failover": {
|
||||
"disabled": false,
|
||||
"failoverType": 1,
|
||||
"failoverTypeStr": "Normal (always lowest level)",
|
||||
"values": []
|
||||
},
|
||||
"roundRobinFailover": [],
|
||||
"pools": [],
|
||||
"poolsDetail": []
|
||||
}]
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"recordsets": [
|
||||
{
|
||||
"rdata": [
|
||||
"",
|
||||
"12 20 foo-2.unit.tests."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"",
|
||||
"1 1"
|
||||
],
|
||||
"type": "SSHFP",
|
||||
"name": "unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"",
|
||||
"100 \"U\" \"SIP+D2U\" \"!^.*$!sip:[email protected]!\" ."
|
||||
],
|
||||
"type": "NAPTR",
|
||||
"name": "naptr.unit.tests",
|
||||
"ttl": 600
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"totalElements": 3,
|
||||
"showAll": true
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"recordsets": [
|
||||
{
|
||||
"rdata": [
|
||||
"10 20 30 foo-1.other.tests.",
|
||||
"12 20 30 foo-2.other.tests."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.old.other.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 20 30 foo-1.other.tests.",
|
||||
"12 20 30 foo-2.other.tests."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.old.other.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2601:644:500:e210:62f8:1dff:feb8:9471"
|
||||
],
|
||||
"type": "AAAA",
|
||||
"name": "aaaa.old.other.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.akam.net.",
|
||||
"ns2.akam.net.",
|
||||
"ns3.akam.net.",
|
||||
"ns4.akam.net."
|
||||
],
|
||||
"type": "NS",
|
||||
"name": "old.other.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"1.2.3.4",
|
||||
"1.2.3.5"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "old.other.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300"
|
||||
],
|
||||
"type": "SOA",
|
||||
"name": "other.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49",
|
||||
"1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73"
|
||||
],
|
||||
"type": "SSHFP",
|
||||
"name": "old.other.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"other.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "old.cname.other.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"other.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "excluded.old.other.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"other.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "included.old.other.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 smtp-4.other.tests.",
|
||||
"20 smtp-2.other.tests.",
|
||||
"30 smtp-3.other.tests.",
|
||||
"40 smtp-1.other.tests."
|
||||
],
|
||||
"type": "MX",
|
||||
"name": "mx.old.other.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:[email protected]!\" .",
|
||||
"100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:[email protected]!\" ."
|
||||
],
|
||||
"type": "NAPTR",
|
||||
"name": "naptr.old.other.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"foo.bar.com."
|
||||
],
|
||||
"type": "PTR",
|
||||
"name": "ptr.old.other.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"\"v=spf1 ip4:192.168.0.1/16-all\""
|
||||
],
|
||||
"type": "SPF",
|
||||
"name": "spf.old.other.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.other.tests.",
|
||||
"ns2.other.tests."
|
||||
],
|
||||
"type": "NS",
|
||||
"name": "under.old.other.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"\"Bah bah black sheep\"",
|
||||
"\"have you any wool.\"",
|
||||
"\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\""
|
||||
],
|
||||
"type": "TXT",
|
||||
"name": "txt.old.other.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2.2.3.7"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "www.other.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2.2.3.6"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "www.sub.old.other.tests",
|
||||
"ttl": 300
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"totalElements": 16,
|
||||
"showAll": true
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"recordsets": [
|
||||
{
|
||||
"rdata": [
|
||||
"10 20 30 foo-1.unit.tests.",
|
||||
"12 20 30 foo-2.unit.tests."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.old.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 20 30 foo-1.unit.tests.",
|
||||
"12 20 30 foo-2.unit.tests."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.old.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2601:644:500:e210:62f8:1dff:feb8:9471"
|
||||
],
|
||||
"type": "AAAA",
|
||||
"name": "aaaa.old.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.akam.net.",
|
||||
"ns2.akam.net.",
|
||||
"ns3.akam.net.",
|
||||
"ns4.akam.net."
|
||||
],
|
||||
"type": "NS",
|
||||
"name": "old.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"1.2.3.4",
|
||||
"1.2.3.5"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "old.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300"
|
||||
],
|
||||
"type": "SOA",
|
||||
"name": "unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49",
|
||||
"1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73"
|
||||
],
|
||||
"type": "SSHFP",
|
||||
"name": "old.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"unit.tests"
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "old.cname.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"unit.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "excluded.old.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"unit.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "included.old.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 smtp-4.unit.tests.",
|
||||
"20 smtp-2.unit.tests.",
|
||||
"30 smtp-3.unit.tests.",
|
||||
"40 smtp-1.unit.tests."
|
||||
],
|
||||
"type": "MX",
|
||||
"name": "mx.old.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:[email protected]!\" .",
|
||||
"100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:[email protected]!\" ."
|
||||
],
|
||||
"type": "NAPTR",
|
||||
"name": "naptr.old.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"foo.bar.com."
|
||||
],
|
||||
"type": "PTR",
|
||||
"name": "ptr.old.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"\"v=spf1 ip4:192.168.0.1/16-all\""
|
||||
],
|
||||
"type": "SPF",
|
||||
"name": "spf.old.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.unit.tests.",
|
||||
"ns2.unit.tests."
|
||||
],
|
||||
"type": "NS",
|
||||
"name": "under.old.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"\"Bah bah black sheep\"",
|
||||
"\"have you any wool.\"",
|
||||
"\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\""
|
||||
],
|
||||
"type": "TXT",
|
||||
"name": "txt.old.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2.2.3.7"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "www.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2.2.3.6"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "www.sub.old.unit.tests",
|
||||
"ttl": 300
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"totalElements": 16,
|
||||
"showAll": true
|
||||
}
|
||||
}
|
||||
Vendored
+157
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"recordsets": [
|
||||
{
|
||||
"rdata": [
|
||||
"10 20 30 foo-1.unit.tests.",
|
||||
"12 20 30 foo-2.unit.tests."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2601:644:500:e210:62f8:1dff:feb8:947a"
|
||||
],
|
||||
"type": "AAAA",
|
||||
"name": "aaaa.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.akam.net.",
|
||||
"ns2.akam.net.",
|
||||
"ns3.akam.net.",
|
||||
"ns4.akam.net."
|
||||
],
|
||||
"type": "NS",
|
||||
"name": "unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"1.2.3.4",
|
||||
"1.2.3.5"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.akam.net hostmaster.akamai.com 1489074932 86400 7200 604800 300"
|
||||
],
|
||||
"type": "SOA",
|
||||
"name": "unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49",
|
||||
"1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73"
|
||||
],
|
||||
"type": "SSHFP",
|
||||
"name": "unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"unit.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "cname.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"unit.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "excluded.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"unit.tests."
|
||||
],
|
||||
"type": "CNAME",
|
||||
"name": "included.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 smtp-4.unit.tests.",
|
||||
"20 smtp-2.unit.tests.",
|
||||
"30 smtp-3.unit.tests.",
|
||||
"40 smtp-1.unit.tests."
|
||||
],
|
||||
"type": "MX",
|
||||
"name": "mx.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:[email protected]!\" .",
|
||||
"100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:[email protected]!\" ."
|
||||
],
|
||||
"type": "NAPTR",
|
||||
"name": "naptr.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"foo.bar.com."
|
||||
],
|
||||
"type": "PTR",
|
||||
"name": "ptr.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"\"v=spf1 ip4:192.168.0.1/16-all\""
|
||||
],
|
||||
"type": "SPF",
|
||||
"name": "spf.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"ns1.unit.tests.",
|
||||
"ns2.unit.tests."
|
||||
],
|
||||
"type": "NS",
|
||||
"name": "under.unit.tests",
|
||||
"ttl": 3600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"\"Bah bah black sheep\"",
|
||||
"\"have you any wool.\"",
|
||||
"\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\""
|
||||
],
|
||||
"type": "TXT",
|
||||
"name": "txt.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2.2.3.6"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "www.unit.tests",
|
||||
"ttl": 300
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2.2.3.6"
|
||||
],
|
||||
"type": "A",
|
||||
"name": "www.sub.unit.tests",
|
||||
"ttl": 300
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"totalElements": 16,
|
||||
"showAll": true
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ class TestCloudflareProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=200, json=self.empty)
|
||||
|
||||
@@ -111,7 +111,7 @@ class TestCloudflareProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
|
||||
# re-populating the same non-existant zone uses cache and makes no
|
||||
# re-populating the same non-existent zone uses cache and makes no
|
||||
# calls
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
@@ -174,7 +174,7 @@ class TestCloudflareProvider(TestCase):
|
||||
}, # zone create
|
||||
] + [None] * 20 # individual record creates
|
||||
|
||||
# non-existant zone, create everything
|
||||
# non-existent zone, create everything
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(12, len(plan.changes))
|
||||
self.assertEquals(12, provider.apply(plan))
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
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.constellix import ConstellixClientNotFound, \
|
||||
ConstellixProvider
|
||||
from octodns.provider.yaml import YamlProvider
|
||||
from octodns.zone import Zone
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class TestConstellixProvider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
|
||||
# Our test suite differs a bit, add our NS and remove the simple one
|
||||
expected.add_record(Record.new(expected, 'under', {
|
||||
'ttl': 3600,
|
||||
'type': 'NS',
|
||||
'values': [
|
||||
'ns1.unit.tests.',
|
||||
'ns2.unit.tests.',
|
||||
]
|
||||
}))
|
||||
|
||||
# Add some ALIAS records
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'ttl': 1800,
|
||||
'type': 'ALIAS',
|
||||
'value': 'aname.unit.tests.'
|
||||
}))
|
||||
|
||||
expected.add_record(Record.new(expected, 'sub', {
|
||||
'ttl': 1800,
|
||||
'type': 'ALIAS',
|
||||
'value': 'aname.unit.tests.'
|
||||
}))
|
||||
|
||||
for record in list(expected.records):
|
||||
if record.name == 'sub' and record._type == 'NS':
|
||||
expected._remove_record(record)
|
||||
break
|
||||
|
||||
def test_populate(self):
|
||||
provider = ConstellixProvider('test', 'api', 'secret')
|
||||
|
||||
# Bad auth
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=401,
|
||||
text='{"errors": ["Unable to authenticate token"]}')
|
||||
|
||||
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='{"errors": ["\\"unittests\\" is not '
|
||||
'a valid domain name"]}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('\n - "unittests" is not a valid domain name',
|
||||
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-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404,
|
||||
text='<html><head></head><body></body></html>')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
|
||||
# No diffs == no changes
|
||||
with requests_mock() as mock:
|
||||
base = 'https://api.dns.constellix.com/v1/domains'
|
||||
with open('tests/fixtures/constellix-domains.json') as fh:
|
||||
mock.get('{}{}'.format(base, '/'), text=fh.read())
|
||||
with open('tests/fixtures/constellix-records.json') as fh:
|
||||
mock.get('{}{}'.format(base, '/123123/records'),
|
||||
text=fh.read())
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(15, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# 2nd populate makes no network calls/all from cache
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(15, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
|
||||
def test_apply(self):
|
||||
provider = ConstellixProvider('test', 'api', 'secret')
|
||||
|
||||
resp = Mock()
|
||||
resp.json = Mock()
|
||||
provider._client._request = Mock(return_value=resp)
|
||||
|
||||
with open('tests/fixtures/constellix-domains.json') as fh:
|
||||
domains = json.load(fh)
|
||||
|
||||
# non-existent domain, create everything
|
||||
resp.json.side_effect = [
|
||||
ConstellixClientNotFound, # no zone in populate
|
||||
ConstellixClientNotFound, # 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={'names': ['unit.tests']}),
|
||||
# get all domains to build the cache
|
||||
call('GET', '/'),
|
||||
call('POST', '/123123/records/SRV', data={
|
||||
'roundRobin': [{
|
||||
'priority': 10,
|
||||
'weight': 20,
|
||||
'value': 'foo-1.unit.tests.',
|
||||
'port': 30
|
||||
}, {
|
||||
'priority': 12,
|
||||
'weight': 20,
|
||||
'value': 'foo-2.unit.tests.',
|
||||
'port': 30
|
||||
}],
|
||||
'name': '_srv._tcp',
|
||||
'ttl': 600,
|
||||
}),
|
||||
])
|
||||
|
||||
self.assertEquals(20, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
provider._client.records = Mock(return_value=[
|
||||
{
|
||||
'id': 11189897,
|
||||
'type': 'A',
|
||||
'name': 'www',
|
||||
'ttl': 300,
|
||||
'value': [
|
||||
'1.2.3.4',
|
||||
'2.2.3.4',
|
||||
]
|
||||
}, {
|
||||
'id': 11189898,
|
||||
'type': 'A',
|
||||
'name': 'ttl',
|
||||
'ttl': 600,
|
||||
'value': [
|
||||
'3.2.3.4'
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
# 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/A', data={
|
||||
'roundRobin': [{
|
||||
'value': '3.2.3.4'
|
||||
}],
|
||||
'name': 'ttl',
|
||||
'ttl': 300
|
||||
}),
|
||||
call('DELETE', '/123123/records/A/11189897'),
|
||||
call('DELETE', '/123123/records/A/11189898')
|
||||
], any_order=True)
|
||||
@@ -62,7 +62,7 @@ class TestDigitalOceanProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404,
|
||||
text='{"id":"not_found","message":"The resource you '
|
||||
@@ -154,7 +154,7 @@ class TestDigitalOceanProvider(TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
# non-existant domain, create everything
|
||||
# non-existent domain, create everything
|
||||
resp.json.side_effect = [
|
||||
DigitalOceanClientNotFound, # no zone in populate
|
||||
DigitalOceanClientNotFound, # no domain during apply
|
||||
|
||||
@@ -59,7 +59,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404,
|
||||
text='{"message": "Domain `foo.bar` not found"}')
|
||||
@@ -123,7 +123,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
resp.json = Mock()
|
||||
provider._client._request = Mock(return_value=resp)
|
||||
|
||||
# non-existant domain, create everything
|
||||
# non-existent domain, create everything
|
||||
resp.json.side_effect = [
|
||||
DnsimpleClientNotFound, # no zone in populate
|
||||
DnsimpleClientNotFound, # no domain during apply
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestDnsMadeEasyProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404,
|
||||
text='<html><head></head><body></body></html>')
|
||||
@@ -131,7 +131,7 @@ class TestDnsMadeEasyProvider(TestCase):
|
||||
with open('tests/fixtures/dnsmadeeasy-domains.json') as fh:
|
||||
domains = json.load(fh)
|
||||
|
||||
# non-existant domain, create everything
|
||||
# non-existent domain, create everything
|
||||
resp.json.side_effect = [
|
||||
DnsMadeEasyClientNotFound, # no zone in populate
|
||||
DnsMadeEasyClientNotFound, # no domain during apply
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
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.fastdns import AkamaiProvider
|
||||
from octodns.provider.yaml import YamlProvider
|
||||
from octodns.zone import Zone
|
||||
|
||||
|
||||
class TestFastdnsProvider(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 = AkamaiProvider("test", "secret", "akam.com", "atok", "ctok")
|
||||
|
||||
# Bad Auth
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=401, text='{"message": "Unauthorized"}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertEquals(401, ctx.exception.response.status_code)
|
||||
|
||||
# general error
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=502, text='Things caught fire')
|
||||
|
||||
with self.assertRaises(HTTPError) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404,
|
||||
text='{"message": "Domain `foo.bar` not found"}')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
|
||||
# No diffs == no changes
|
||||
with requests_mock() as mock:
|
||||
|
||||
with open('tests/fixtures/fastdns-records.json') as fh:
|
||||
mock.get(ANY, text=fh.read())
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(16, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# 2nd populate makes no network calls/all from cache
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(16, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
|
||||
def test_apply(self):
|
||||
provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok",
|
||||
"cid", "gid")
|
||||
|
||||
# tests create update delete through previous state config json
|
||||
with requests_mock() as mock:
|
||||
|
||||
with open('tests/fixtures/fastdns-records-prev.json') as fh:
|
||||
mock.get(ANY, text=fh.read())
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
mock.post(ANY, status_code=201)
|
||||
mock.put(ANY, status_code=200)
|
||||
mock.delete(ANY, status_code=204)
|
||||
|
||||
changes = provider.apply(plan)
|
||||
self.assertEquals(29, changes)
|
||||
|
||||
# Test against a zone that doesn't exist yet
|
||||
with requests_mock() as mock:
|
||||
with open('tests/fixtures/fastdns-records-prev-other.json') as fh:
|
||||
mock.get(ANY, status_code=404)
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
mock.post(ANY, status_code=201)
|
||||
mock.put(ANY, status_code=200)
|
||||
mock.delete(ANY, status_code=204)
|
||||
|
||||
changes = provider.apply(plan)
|
||||
self.assertEquals(14, changes)
|
||||
|
||||
# Test against a zone that doesn't exist yet, but gid not provided
|
||||
with requests_mock() as mock:
|
||||
with open('tests/fixtures/fastdns-records-prev-other.json') as fh:
|
||||
mock.get(ANY, status_code=404)
|
||||
provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok",
|
||||
"cid")
|
||||
plan = provider.plan(self.expected)
|
||||
mock.post(ANY, status_code=201)
|
||||
mock.put(ANY, status_code=200)
|
||||
mock.delete(ANY, status_code=204)
|
||||
|
||||
changes = provider.apply(plan)
|
||||
self.assertEquals(14, changes)
|
||||
|
||||
# Test against a zone that doesn't exist, but cid not provided
|
||||
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404)
|
||||
|
||||
provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok")
|
||||
plan = provider.plan(self.expected)
|
||||
mock.post(ANY, status_code=201)
|
||||
mock.put(ANY, status_code=200)
|
||||
mock.delete(ANY, status_code=204)
|
||||
|
||||
try:
|
||||
changes = provider.apply(plan)
|
||||
except NameError as e:
|
||||
expected = "contractId not specified to create zone"
|
||||
self.assertEquals(e.message, expected)
|
||||
@@ -191,7 +191,7 @@ class TestNs1Provider(TestCase):
|
||||
self.assertEquals(load_mock.side_effect, ctx.exception)
|
||||
self.assertEquals(('unit.tests',), load_mock.call_args[0])
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
# Non-existent zone doesn't populate anything
|
||||
load_mock.reset_mock()
|
||||
load_mock.side_effect = \
|
||||
ResourceException('server error: zone not found')
|
||||
@@ -323,7 +323,7 @@ class TestNs1Provider(TestCase):
|
||||
provider.apply(plan)
|
||||
self.assertEquals(create_mock.side_effect, ctx.exception)
|
||||
|
||||
# non-existant zone, create
|
||||
# non-existent zone, create
|
||||
load_mock.reset_mock()
|
||||
create_mock.reset_mock()
|
||||
load_mock.side_effect = \
|
||||
|
||||
@@ -42,7 +42,7 @@ with open('./tests/fixtures/powerdns-full-data.json') as fh:
|
||||
class TestPowerDnsProvider(TestCase):
|
||||
|
||||
def test_provider(self):
|
||||
provider = PowerDnsProvider('test', 'non.existant', 'api-key',
|
||||
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
|
||||
nameserver_values=['8.8.8.8.',
|
||||
'9.9.9.9.'])
|
||||
|
||||
@@ -64,7 +64,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=422,
|
||||
json={'error': "Could not find domain 'unit.tests.'"})
|
||||
@@ -164,7 +164,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
provider.apply(plan)
|
||||
|
||||
def test_small_change(self):
|
||||
provider = PowerDnsProvider('test', 'non.existant', 'api-key')
|
||||
provider = PowerDnsProvider('test', 'non.existent', 'api-key')
|
||||
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
@@ -204,7 +204,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
|
||||
def test_existing_nameservers(self):
|
||||
ns_values = ['8.8.8.8.', '9.9.9.9.']
|
||||
provider = PowerDnsProvider('test', 'non.existant', 'api-key',
|
||||
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
|
||||
nameserver_values=ns_values)
|
||||
|
||||
expected = Zone('unit.tests.', [])
|
||||
|
||||
@@ -504,7 +504,7 @@ class TestRoute53Provider(TestCase):
|
||||
'ResourceRecords': [{
|
||||
'Value': '10 smtp-1.unit.tests.',
|
||||
}, {
|
||||
'Value': '20 smtp-2.unit.tests.',
|
||||
'Value': '20 smtp-2.unit.tests.',
|
||||
}],
|
||||
'TTL': 64,
|
||||
}, {
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
import requests_mock
|
||||
|
||||
from octodns.provider.selectel import SelectelProvider
|
||||
from octodns.record import Record, Update
|
||||
from octodns.zone import Zone
|
||||
|
||||
|
||||
class TestSelectelProvider(TestCase):
|
||||
API_URL = 'https://api.selectel.ru/domains/v1'
|
||||
|
||||
api_record = []
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
expected = set()
|
||||
|
||||
domain = [{"name": "unit.tests", "id": 100000}]
|
||||
|
||||
# A, subdomain=''
|
||||
api_record.append({
|
||||
'type': 'A',
|
||||
'ttl': 100,
|
||||
'content': '1.2.3.4',
|
||||
'name': 'unit.tests',
|
||||
'id': 1
|
||||
})
|
||||
expected.add(Record.new(zone, '', {
|
||||
'ttl': 100,
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
}))
|
||||
|
||||
# A, subdomain='sub'
|
||||
api_record.append({
|
||||
'type': 'A',
|
||||
'ttl': 200,
|
||||
'content': '1.2.3.4',
|
||||
'name': 'sub.unit.tests',
|
||||
'id': 2
|
||||
})
|
||||
expected.add(Record.new(zone, 'sub', {
|
||||
'ttl': 200,
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
}))
|
||||
|
||||
# CNAME
|
||||
api_record.append({
|
||||
'type': 'CNAME',
|
||||
'ttl': 300,
|
||||
'content': 'unit.tests',
|
||||
'name': 'www2.unit.tests',
|
||||
'id': 3
|
||||
})
|
||||
expected.add(Record.new(zone, 'www2', {
|
||||
'ttl': 300,
|
||||
'type': 'CNAME',
|
||||
'value': 'unit.tests.',
|
||||
}))
|
||||
|
||||
# MX
|
||||
api_record.append({
|
||||
'type': 'MX',
|
||||
'ttl': 400,
|
||||
'content': 'mx1.unit.tests',
|
||||
'priority': 10,
|
||||
'name': 'unit.tests',
|
||||
'id': 4
|
||||
})
|
||||
expected.add(Record.new(zone, '', {
|
||||
'ttl': 400,
|
||||
'type': 'MX',
|
||||
'values': [{
|
||||
'preference': 10,
|
||||
'exchange': 'mx1.unit.tests.',
|
||||
}]
|
||||
}))
|
||||
|
||||
# NS
|
||||
api_record.append({
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'content': 'ns1.unit.tests',
|
||||
'name': 'unit.tests.',
|
||||
'id': 6
|
||||
})
|
||||
api_record.append({
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'content': 'ns2.unit.tests',
|
||||
'name': 'unit.tests',
|
||||
'id': 7
|
||||
})
|
||||
expected.add(Record.new(zone, '', {
|
||||
'ttl': 600,
|
||||
'type': 'NS',
|
||||
'values': ['ns1.unit.tests.', 'ns2.unit.tests.'],
|
||||
}))
|
||||
|
||||
# NS with sub
|
||||
api_record.append({
|
||||
'type': 'NS',
|
||||
'ttl': 700,
|
||||
'content': 'ns3.unit.tests',
|
||||
'name': 'www3.unit.tests',
|
||||
'id': 8
|
||||
})
|
||||
api_record.append({
|
||||
'type': 'NS',
|
||||
'ttl': 700,
|
||||
'content': 'ns4.unit.tests',
|
||||
'name': 'www3.unit.tests',
|
||||
'id': 9
|
||||
})
|
||||
expected.add(Record.new(zone, 'www3', {
|
||||
'ttl': 700,
|
||||
'type': 'NS',
|
||||
'values': ['ns3.unit.tests.', 'ns4.unit.tests.'],
|
||||
}))
|
||||
|
||||
# SRV
|
||||
api_record.append({
|
||||
'type': 'SRV',
|
||||
'ttl': 800,
|
||||
'target': 'foo-1.unit.tests',
|
||||
'weight': 20,
|
||||
'priority': 10,
|
||||
'port': 30,
|
||||
'id': 10,
|
||||
'name': '_srv._tcp.unit.tests'
|
||||
})
|
||||
api_record.append({
|
||||
'type': 'SRV',
|
||||
'ttl': 800,
|
||||
'target': 'foo-2.unit.tests',
|
||||
'name': '_srv._tcp.unit.tests',
|
||||
'weight': 50,
|
||||
'priority': 40,
|
||||
'port': 60,
|
||||
'id': 11
|
||||
})
|
||||
expected.add(Record.new(zone, '_srv._tcp', {
|
||||
'ttl': 800,
|
||||
'type': 'SRV',
|
||||
'values': [{
|
||||
'priority': 10,
|
||||
'weight': 20,
|
||||
'port': 30,
|
||||
'target': 'foo-1.unit.tests.',
|
||||
}, {
|
||||
'priority': 40,
|
||||
'weight': 50,
|
||||
'port': 60,
|
||||
'target': 'foo-2.unit.tests.',
|
||||
}]
|
||||
}))
|
||||
|
||||
# AAAA
|
||||
aaaa_record = {
|
||||
'type': 'AAAA',
|
||||
'ttl': 200,
|
||||
'content': '1:1ec:1::1',
|
||||
'name': 'unit.tests',
|
||||
'id': 15
|
||||
}
|
||||
api_record.append(aaaa_record)
|
||||
expected.add(Record.new(zone, '', {
|
||||
'ttl': 200,
|
||||
'type': 'AAAA',
|
||||
'value': '1:1ec:1::1',
|
||||
}))
|
||||
|
||||
# TXT
|
||||
api_record.append({
|
||||
'type': 'TXT',
|
||||
'ttl': 300,
|
||||
'content': 'little text',
|
||||
'name': 'text.unit.tests',
|
||||
'id': 16
|
||||
})
|
||||
expected.add(Record.new(zone, 'text', {
|
||||
'ttl': 200,
|
||||
'type': 'TXT',
|
||||
'value': 'little text',
|
||||
}))
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_populate(self, fake_http):
|
||||
zone = Zone('unit.tests.', [])
|
||||
fake_http.get('{}/unit.tests/records/'.format(self.API_URL),
|
||||
json=self.api_record)
|
||||
fake_http.get('{}/'.format(self.API_URL), json=self.domain)
|
||||
fake_http.head('{}/unit.tests/records/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.api_record))})
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
|
||||
provider = SelectelProvider(123, 'secret_token')
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertEquals(self.expected, zone.records)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_populate_invalid_record(self, fake_http):
|
||||
more_record = self.api_record
|
||||
more_record.append({"name": "unit.tests",
|
||||
"id": 100001,
|
||||
"content": "support.unit.tests.",
|
||||
"ttl": 300, "ns": "ns1.unit.tests",
|
||||
"type": "SOA",
|
||||
"email": "[email protected]"})
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
fake_http.get('{}/unit.tests/records/'.format(self.API_URL),
|
||||
json=more_record)
|
||||
fake_http.get('{}/'.format(self.API_URL), json=self.domain)
|
||||
fake_http.head('{}/unit.tests/records/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.api_record))})
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
|
||||
zone.add_record(Record.new(self.zone, 'unsup', {
|
||||
'ttl': 200,
|
||||
'type': 'NAPTR',
|
||||
'value': {
|
||||
'order': 40,
|
||||
'preference': 70,
|
||||
'flags': 'U',
|
||||
'service': 'SIP+D2U',
|
||||
'regexp': '!^.*$!sip:[email protected]!',
|
||||
'replacement': '.',
|
||||
}
|
||||
}))
|
||||
|
||||
provider = SelectelProvider(123, 'secret_token')
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertNotEqual(self.expected, zone.records)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_apply(self, fake_http):
|
||||
|
||||
fake_http.get('{}/unit.tests/records/'.format(self.API_URL),
|
||||
json=list())
|
||||
fake_http.get('{}/'.format(self.API_URL), json=self.domain)
|
||||
fake_http.head('{}/unit.tests/records/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': '0'})
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
fake_http.post('{}/100000/records/'.format(self.API_URL), json=list())
|
||||
|
||||
provider = SelectelProvider(123, 'test_token')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
for record in self.expected:
|
||||
zone.add_record(record)
|
||||
|
||||
plan = provider.plan(zone)
|
||||
self.assertEquals(8, len(plan.changes))
|
||||
self.assertEquals(8, provider.apply(plan))
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_domain_list(self, fake_http):
|
||||
fake_http.get('{}/'.format(self.API_URL), json=self.domain)
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
|
||||
expected = {'unit.tests': self.domain[0]}
|
||||
provider = SelectelProvider(123, 'test_token')
|
||||
|
||||
result = provider.domain_list()
|
||||
self.assertEquals(result, expected)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_authentication_fail(self, fake_http):
|
||||
fake_http.get('{}/'.format(self.API_URL), status_code=401)
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
SelectelProvider(123, 'fail_token')
|
||||
self.assertEquals(ctx.exception.message,
|
||||
'Authorization failed. Invalid or empty token.')
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_not_exist_domain(self, fake_http):
|
||||
fake_http.get('{}/'.format(self.API_URL), status_code=404, json='')
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
|
||||
fake_http.post('{}/'.format(self.API_URL),
|
||||
json={"name": "unit.tests",
|
||||
"create_date": 1507154178,
|
||||
"id": 100000})
|
||||
fake_http.get('{}/unit.tests/records/'.format(self.API_URL),
|
||||
json=list())
|
||||
fake_http.head('{}/unit.tests/records/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.api_record))})
|
||||
fake_http.post('{}/100000/records/'.format(self.API_URL),
|
||||
json=list())
|
||||
|
||||
provider = SelectelProvider(123, 'test_token')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
for record in self.expected:
|
||||
zone.add_record(record)
|
||||
|
||||
plan = provider.plan(zone)
|
||||
self.assertEquals(8, len(plan.changes))
|
||||
self.assertEquals(8, provider.apply(plan))
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_delete_no_exist_record(self, fake_http):
|
||||
fake_http.get('{}/'.format(self.API_URL), json=self.domain)
|
||||
fake_http.get('{}/100000/records/'.format(self.API_URL), json=list())
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
fake_http.head('{}/unit.tests/records/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': '0'})
|
||||
|
||||
provider = SelectelProvider(123, 'test_token')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
provider.delete_record('unit.tests', 'NS', zone)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_change_record(self, fake_http):
|
||||
exist_record = [self.aaaa_record,
|
||||
{"content": "6.6.5.7",
|
||||
"ttl": 100,
|
||||
"type": "A",
|
||||
"id": 100001,
|
||||
"name": "delete.unit.tests"},
|
||||
{"content": "9.8.2.1",
|
||||
"ttl": 100,
|
||||
"type": "A",
|
||||
"id": 100002,
|
||||
"name": "unit.tests"}] # exist
|
||||
fake_http.get('{}/unit.tests/records/'.format(self.API_URL),
|
||||
json=exist_record)
|
||||
fake_http.get('{}/'.format(self.API_URL), json=self.domain)
|
||||
fake_http.get('{}/100000/records/'.format(self.API_URL),
|
||||
json=exist_record)
|
||||
fake_http.head('{}/unit.tests/records/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(exist_record))})
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
fake_http.head('{}/100000/records/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(exist_record))})
|
||||
fake_http.post('{}/100000/records/'.format(self.API_URL),
|
||||
json=list())
|
||||
fake_http.delete('{}/100000/records/100001'.format(self.API_URL),
|
||||
text="")
|
||||
fake_http.delete('{}/100000/records/100002'.format(self.API_URL),
|
||||
text="")
|
||||
|
||||
provider = SelectelProvider(123, 'test_token')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
for record in self.expected:
|
||||
zone.add_record(record)
|
||||
|
||||
plan = provider.plan(zone)
|
||||
self.assertEquals(8, len(plan.changes))
|
||||
self.assertEquals(8, provider.apply(plan))
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_include_change_returns_false(self, fake_http):
|
||||
fake_http.get('{}/'.format(self.API_URL), json=self.domain)
|
||||
fake_http.head('{}/'.format(self.API_URL),
|
||||
headers={'X-Total-Count': str(len(self.domain))})
|
||||
provider = SelectelProvider(123, 'test_token')
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
exist_record = Record.new(zone, '', {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'values': ['1.1.1.1', '2.2.2.2']
|
||||
})
|
||||
new = Record.new(zone, '', {
|
||||
'ttl': 10,
|
||||
'type': 'A',
|
||||
'values': ['1.1.1.1', '2.2.2.2']
|
||||
})
|
||||
change = Update(exist_record, new)
|
||||
|
||||
include_change = provider._include_change(change)
|
||||
|
||||
self.assertFalse(include_change)
|
||||
@@ -0,0 +1,275 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
# from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
|
||||
from suds import WebFault
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.provider.transip import TransipProvider
|
||||
from octodns.provider.yaml import YamlProvider
|
||||
from octodns.zone import Zone
|
||||
from transip.service.domain import DomainService
|
||||
from transip.service.objects import DnsEntry
|
||||
|
||||
|
||||
class MockFault(object):
|
||||
faultstring = ""
|
||||
faultcode = ""
|
||||
|
||||
def __init__(self, code, string, *args, **kwargs):
|
||||
self.faultstring = string
|
||||
self.faultcode = code
|
||||
|
||||
|
||||
class MockResponse(object):
|
||||
dnsEntries = []
|
||||
|
||||
|
||||
class MockDomainService(DomainService):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MockDomainService, self).__init__('MockDomainService', *args,
|
||||
**kwargs)
|
||||
self.mockupEntries = []
|
||||
|
||||
def mockup(self, records):
|
||||
|
||||
provider = TransipProvider('', '', '')
|
||||
|
||||
_dns_entries = []
|
||||
for record in records:
|
||||
if record._type in provider.SUPPORTS:
|
||||
entries_for = getattr(provider,
|
||||
'_entries_for_{}'.format(record._type))
|
||||
|
||||
# Root records have '@' as name
|
||||
name = record.name
|
||||
if name == '':
|
||||
name = provider.ROOT_RECORD
|
||||
|
||||
_dns_entries.extend(entries_for(name, record))
|
||||
|
||||
# NS is not supported as a DNS Entry,
|
||||
# so it should cover the if statement
|
||||
_dns_entries.append(
|
||||
DnsEntry('@', '3600', 'NS', 'ns01.transip.nl.'))
|
||||
|
||||
self.mockupEntries = _dns_entries
|
||||
|
||||
# Skips authentication layer and returns the entries loaded by "Mockup"
|
||||
def get_info(self, domain_name):
|
||||
|
||||
# Special 'domain' to trigger error
|
||||
if str(domain_name) == str('notfound.unit.tests'):
|
||||
self.raiseZoneNotFound()
|
||||
|
||||
result = MockResponse()
|
||||
result.dnsEntries = self.mockupEntries
|
||||
return result
|
||||
|
||||
def set_dns_entries(self, domain_name, dns_entries):
|
||||
|
||||
# Special 'domain' to trigger error
|
||||
if str(domain_name) == str('failsetdns.unit.tests'):
|
||||
self.raiseSaveError()
|
||||
|
||||
return True
|
||||
|
||||
def raiseZoneNotFound(self):
|
||||
fault = MockFault(str('102'), '102 is zone not found')
|
||||
document = {}
|
||||
raise WebFault(fault, document)
|
||||
|
||||
def raiseInvalidAuth(self):
|
||||
fault = MockFault(str('200'), '200 is invalid auth')
|
||||
document = {}
|
||||
raise WebFault(fault, document)
|
||||
|
||||
def raiseSaveError(self):
|
||||
fault = MockFault(str('200'), '202 random error')
|
||||
document = {}
|
||||
raise WebFault(fault, document)
|
||||
|
||||
|
||||
class TestTransipProvider(TestCase):
|
||||
bogus_key = str("""-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA0U5HGCkLrz423IyUf3u4cKN2WrNz1x5KNr6PvH2M/zxas+zB
|
||||
elbxkdT3AQ+wmfcIvOuTmFRTHv35q2um1aBrPxVw+2s+lWo28VwIRttwIB1vIeWu
|
||||
lSBnkEZQRLyPI2tH0i5QoMX4CVPf9rvij3Uslimi84jdzDfPFIh6jZ6C8nLipOTG
|
||||
0IMhge1ofVfB0oSy5H+7PYS2858QLAf5ruYbzbAxZRivS402wGmQ0d0Lc1KxraAj
|
||||
kiMM5yj/CkH/Vm2w9I6+tLFeASE4ub5HCP5G/ig4dbYtqZMQMpqyAbGxd5SOVtyn
|
||||
UHagAJUxf8DT3I8PyjEHjxdOPUsxNyRtepO/7QIDAQABAoIBAQC7fiZ7gxE/ezjD
|
||||
2n6PsHFpHVTBLS2gzzZl0dCKZeFvJk6ODJDImaeuHhrh7X8ifMNsEI9XjnojMhl8
|
||||
MGPzy88mZHugDNK0H8B19x5G8v1/Fz7dG5WHas660/HFkS+b59cfdXOugYiOOn9O
|
||||
08HBBpLZNRUOmVUuQfQTjapSwGLG8PocgpyRD4zx0LnldnJcqYCxwCdev+AAsPnq
|
||||
ibNtOd/MYD37w9MEGcaxLE8wGgkv8yd97aTjkgE+tp4zsM4QE4Rag133tsLLNznT
|
||||
4Qr/of15M3NW/DXq/fgctyRcJjZpU66eCXLCz2iRTnLyyxxDC2nwlxKbubV+lcS0
|
||||
S4hbfd/BAoGBAO8jXxEaiybR0aIhhSR5esEc3ymo8R8vBN3ZMJ+vr5jEPXr/ZuFj
|
||||
/R4cZ2XV3VoQJG0pvIOYVPZ5DpJM7W+zSXtJ/7bLXy4Bnmh/rc+YYgC+AXQoLSil
|
||||
iD2OuB2xAzRAK71DVSO0kv8gEEXCersPT2i6+vC2GIlJvLcYbOdRKWGxAoGBAOAQ
|
||||
aJbRLtKujH+kMdoMI7tRlL8XwI+SZf0FcieEu//nFyerTePUhVgEtcE+7eQ7hyhG
|
||||
fIXUFx/wALySoqFzdJDLc8U8pTLhbUaoLOTjkwnCTKQVprhnISqQqqh/0U5u47IE
|
||||
RWzWKN6OHb0CezNTq80Dr6HoxmPCnJHBHn5LinT9AoGAQSpvZpbIIqz8pmTiBl2A
|
||||
QQ2gFpcuFeRXPClKYcmbXVLkuhbNL1BzEniFCLAt4LQTaRf9ghLJ3FyCxwVlkpHV
|
||||
zV4N6/8hkcTpKOraL38D/dXJSaEFJVVuee/hZl3tVJjEEpA9rDwx7ooLRSdJEJ6M
|
||||
ciq55UyKBSdt4KssSiDI2RECgYBL3mJ7xuLy5bWfNsrGiVvD/rC+L928/5ZXIXPw
|
||||
26oI0Yfun7ulDH4GOroMcDF/GYT/Zzac3h7iapLlR0WYI47xxGI0A//wBZLJ3QIu
|
||||
krxkDo2C9e3Y/NqnHgsbOQR3aWbiDT4wxydZjIeXS3LKA2fl6Hyc90PN3cTEOb8I
|
||||
hq2gRQKBgEt0SxhhtyB93SjgTzmUZZ7PiEf0YJatfM6cevmjWHexrZH+x31PB72s
|
||||
fH2BQyTKKzoCLB1k/6HRaMnZdrWyWSZ7JKz3AHJ8+58d0Hr8LTrzDM1L6BbjeDct
|
||||
N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
|
||||
-----END RSA PRIVATE KEY-----""")
|
||||
|
||||
def make_expected(self):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
return expected
|
||||
|
||||
def test_init(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
TransipProvider('test', 'unittest')
|
||||
|
||||
self.assertEquals(
|
||||
str('Missing `key` of `key_file` parameter in config'),
|
||||
str(ctx.exception))
|
||||
|
||||
TransipProvider('test', 'unittest', key=self.bogus_key)
|
||||
|
||||
# Existence and content of the key is tested in the SDK on client call
|
||||
TransipProvider('test', 'unittest', key_file='/fake/path')
|
||||
|
||||
def test_populate(self):
|
||||
_expected = self.make_expected()
|
||||
|
||||
# Unhappy Plan - Not authenticated
|
||||
# Live test against API, will fail in an unauthorized error
|
||||
with self.assertRaises(WebFault) as ctx:
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone, True)
|
||||
|
||||
self.assertEquals(str('WebFault'),
|
||||
str(ctx.exception.__class__.__name__))
|
||||
|
||||
self.assertEquals(str('200'), ctx.exception.fault.faultcode)
|
||||
|
||||
# Unhappy Plan - Zone does not exists
|
||||
# Will trigger an exception if provider is used as a target for a
|
||||
# non-existing zone
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
zone = Zone('notfound.unit.tests.', [])
|
||||
provider.populate(zone, True)
|
||||
|
||||
self.assertEquals(str('TransipNewZoneException'),
|
||||
str(ctx.exception.__class__.__name__))
|
||||
|
||||
self.assertEquals(
|
||||
'populate: (102) Transip used as target' +
|
||||
' for non-existing zone: notfound.unit.tests.',
|
||||
ctx.exception.message)
|
||||
|
||||
# Happy Plan - Zone does not exists
|
||||
# Won't trigger an exception if provider is NOT used as a target for a
|
||||
# non-existing zone.
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
zone = Zone('notfound.unit.tests.', [])
|
||||
provider.populate(zone, False)
|
||||
|
||||
# Happy Plan - Populate with mockup records
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
provider._client.mockup(_expected.records)
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone, False)
|
||||
|
||||
# Transip allows relative values for types like cname, mx.
|
||||
# Test is these are correctly appended with the domain
|
||||
provider._currentZone = zone
|
||||
self.assertEquals("www.unit.tests.", provider._parse_to_fqdn("www"))
|
||||
self.assertEquals("www.unit.tests.",
|
||||
provider._parse_to_fqdn("www.unit.tests."))
|
||||
self.assertEquals("www.sub.sub.sub.unit.tests.",
|
||||
provider._parse_to_fqdn("www.sub.sub.sub"))
|
||||
self.assertEquals("unit.tests.",
|
||||
provider._parse_to_fqdn("@"))
|
||||
|
||||
# Happy Plan - Even if the zone has no records the zone should exist
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
zone = Zone('unit.tests.', [])
|
||||
exists = provider.populate(zone, True)
|
||||
self.assertTrue(exists, 'populate should return true')
|
||||
|
||||
return
|
||||
|
||||
def test_plan(self):
|
||||
_expected = self.make_expected()
|
||||
|
||||
# Test Happy plan, only create
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
plan = provider.plan(_expected)
|
||||
|
||||
self.assertEqual(12, plan.change_counts['Create'])
|
||||
self.assertEqual(0, plan.change_counts['Update'])
|
||||
self.assertEqual(0, plan.change_counts['Delete'])
|
||||
|
||||
return
|
||||
|
||||
def test_apply(self):
|
||||
_expected = self.make_expected()
|
||||
|
||||
# Test happy flow. Create all supoorted records
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
plan = provider.plan(_expected)
|
||||
self.assertEqual(12, len(plan.changes))
|
||||
changes = provider.apply(plan)
|
||||
self.assertEqual(changes, len(plan.changes))
|
||||
|
||||
# Test unhappy flow. Trigger 'not found error' in apply stage
|
||||
# This should normally not happen as populate will capture it first
|
||||
# but just in case.
|
||||
changes = [] # reset changes
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
plan = provider.plan(_expected)
|
||||
plan.desired.name = 'notfound.unit.tests.'
|
||||
changes = provider.apply(plan)
|
||||
|
||||
# Changes should not be set due to an Exception
|
||||
self.assertEqual([], changes)
|
||||
|
||||
self.assertEquals(str('WebFault'),
|
||||
str(ctx.exception.__class__.__name__))
|
||||
|
||||
self.assertEquals(str('102'), ctx.exception.fault.faultcode)
|
||||
|
||||
# Test unhappy flow. Trigger a unrecoverable error while saving
|
||||
_expected = self.make_expected() # reset expected
|
||||
changes = [] # reset changes
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
plan = provider.plan(_expected)
|
||||
plan.desired.name = 'failsetdns.unit.tests.'
|
||||
changes = provider.apply(plan)
|
||||
|
||||
# Changes should not be set due to an Exception
|
||||
self.assertEqual([], changes)
|
||||
|
||||
self.assertEquals(str('TransipException'),
|
||||
str(ctx.exception.__class__.__name__))
|
||||
@@ -3166,7 +3166,7 @@ class TestDynamicRecords(TestCase):
|
||||
self.assertEquals(['rule 1 invalid pool "[]"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# rule references non-existant pool
|
||||
# rule references non-existent pool
|
||||
a_data = {
|
||||
'dynamic': {
|
||||
'pools': {
|
||||
@@ -3185,7 +3185,7 @@ class TestDynamicRecords(TestCase):
|
||||
},
|
||||
'rules': [{
|
||||
'geos': ['NA-US-CA'],
|
||||
'pool': 'non-existant',
|
||||
'pool': 'non-existent',
|
||||
}, {
|
||||
'pool': 'one',
|
||||
}],
|
||||
@@ -3199,7 +3199,7 @@ class TestDynamicRecords(TestCase):
|
||||
}
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, 'bad', a_data)
|
||||
self.assertEquals(["rule 1 undefined pool \"non-existant\""],
|
||||
self.assertEquals(["rule 1 undefined pool \"non-existent\""],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# rule with invalid geos
|
||||
|
||||
Reference in New Issue
Block a user