mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Ultra interface with support for basic records
This commit is contained in:
@@ -196,6 +196,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
|||||||
| [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 |
|
| [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 | |
|
| [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 | |
|
| [Transip](/octodns/provider/transip.py) | transip | A, AAAA, CNAME, MX, SRV, SPF, TXT, SSHFP, CAA | No | |
|
||||||
|
| [UltraDns](/octodns/provider/ultra.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | |
|
||||||
| [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
| [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 |
|
| [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 |
|
| [TinyDnsFileSource](/octodns/source/tinydns.py) | | A, CNAME, MX, NS, PTR | No | read-only |
|
||||||
|
|||||||
443
octodns/provider/ultra.py
Normal file
443
octodns/provider/ultra.py
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from ipaddress import ip_address
|
||||||
|
from logging import getLogger
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
from ..record import Record
|
||||||
|
from .base import BaseProvider
|
||||||
|
|
||||||
|
|
||||||
|
class UltraClientException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UltraError(UltraClientException):
|
||||||
|
'''
|
||||||
|
This exception is thrown for error messages returned from Ultra DNS
|
||||||
|
'''
|
||||||
|
def __init__(self, data):
|
||||||
|
try:
|
||||||
|
message = data.json()[0]['errorMessage']
|
||||||
|
except (IndexError, KeyError, TypeError, AttributeError):
|
||||||
|
message = 'Ultra error'
|
||||||
|
super(UltraError, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class UltraNoZonesExistException(UltraError):
|
||||||
|
def __init__(self, data):
|
||||||
|
UltraError.__init__(self, data)
|
||||||
|
|
||||||
|
|
||||||
|
class UltraClientUnauthorized(UltraClientException):
|
||||||
|
def __init__(self):
|
||||||
|
super(UltraClientUnauthorized, self).__init__('Unauthorized')
|
||||||
|
|
||||||
|
|
||||||
|
class UltraProvider(BaseProvider):
|
||||||
|
'''
|
||||||
|
Neustar UltraDNS provider
|
||||||
|
|
||||||
|
Documentation for Ultra REST API requires a login:
|
||||||
|
https://portal.ultradns.com/static/docs/REST-API_User_Guide.pdf
|
||||||
|
Implemented to the July 18, 2017 version of the document
|
||||||
|
|
||||||
|
ultra:
|
||||||
|
class: octodns.provider.ultra.UltraProvider
|
||||||
|
# Ultra Account Name (required)
|
||||||
|
account: acct
|
||||||
|
# Ultra username (required)
|
||||||
|
username: user
|
||||||
|
# Ultra password (required)
|
||||||
|
password: pass
|
||||||
|
# Whether to use the ultradns test endpoint
|
||||||
|
# (optional, default is false)
|
||||||
|
test_endpoint: false
|
||||||
|
'''
|
||||||
|
|
||||||
|
RECORDS_TO_TYPE = {
|
||||||
|
'A (1)': 'A',
|
||||||
|
'AAAA (28)': 'AAAA',
|
||||||
|
'CAA (257)': 'CAA',
|
||||||
|
'CNAME (5)': 'CNAME',
|
||||||
|
'MX (15)': 'MX',
|
||||||
|
'NS (2)': 'NS',
|
||||||
|
'PTR (12)': 'PTR',
|
||||||
|
'SPF (99)': 'SPF',
|
||||||
|
'SRV (33)': 'SRV',
|
||||||
|
'TXT (16)': 'TXT',
|
||||||
|
}
|
||||||
|
TYPE_TO_RECORDS = {v: k for k, v in RECORDS_TO_TYPE.items()}
|
||||||
|
SUPPORTS = set(TYPE_TO_RECORDS.keys())
|
||||||
|
|
||||||
|
SUPPORTS_GEO = False
|
||||||
|
SUPPORTS_DYNAMIC = False
|
||||||
|
TIMEOUT = 5
|
||||||
|
|
||||||
|
def _request(self, method, path, params=None,
|
||||||
|
data=None, json=None, json_response=True):
|
||||||
|
self.log.debug('_request: method=%s, path=%s', method, path)
|
||||||
|
|
||||||
|
url = '{}{}'.format(self.base_uri, path)
|
||||||
|
resp = self._sess.request(method,
|
||||||
|
url,
|
||||||
|
params=params,
|
||||||
|
data=data,
|
||||||
|
json=json,
|
||||||
|
timeout=self.TIMEOUT)
|
||||||
|
self.log.debug('_request: status=%d', resp.status_code)
|
||||||
|
|
||||||
|
if resp.status_code == 401:
|
||||||
|
raise UltraClientUnauthorized()
|
||||||
|
|
||||||
|
if json_response:
|
||||||
|
payload = resp.json()
|
||||||
|
|
||||||
|
# Expected return value when no zones exist in an account
|
||||||
|
if resp.status_code == 404 and len(payload) == 1 and \
|
||||||
|
payload[0]['errorCode'] == 70002:
|
||||||
|
raise UltraNoZonesExistException(resp)
|
||||||
|
else:
|
||||||
|
payload = resp.text
|
||||||
|
resp.raise_for_status()
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _get(self, path, **kwargs):
|
||||||
|
return self._request('GET', path, **kwargs)
|
||||||
|
|
||||||
|
def _post(self, path, **kwargs):
|
||||||
|
return self._request('POST', path, **kwargs)
|
||||||
|
|
||||||
|
def _delete(self, path, **kwargs):
|
||||||
|
return self._request('DELETE', path, **kwargs)
|
||||||
|
|
||||||
|
def _put(self, path, **kwargs):
|
||||||
|
return self._request('PUT', path, **kwargs)
|
||||||
|
|
||||||
|
def _login(self, username, password):
|
||||||
|
'''
|
||||||
|
Get an authorization token by logging in using the provided credentials
|
||||||
|
'''
|
||||||
|
path = '/v2/authorization/token'
|
||||||
|
data = {
|
||||||
|
'grant_type': 'password',
|
||||||
|
'username': username,
|
||||||
|
'password': password
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = self._post(path, data=data)
|
||||||
|
self._sess.headers.update({
|
||||||
|
'Authorization': 'Bearer {}'.format(resp['access_token']),
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, id, account, username, password,
|
||||||
|
test_endpoint=False, *args, **kwargs):
|
||||||
|
self.log = getLogger('UltraProvider[{}]'.format(id))
|
||||||
|
self.log.debug('__init__: id=%s, account=%s, username=%s, '
|
||||||
|
'password=***, test_endpoint=%s', id,
|
||||||
|
account, username, test_endpoint)
|
||||||
|
|
||||||
|
super(UltraProvider, self).__init__(id, *args, **kwargs)
|
||||||
|
self.base_uri = 'https://restapi.ultradns.com'
|
||||||
|
if test_endpoint:
|
||||||
|
self.base_uri = 'https://test-restapi.ultradns.com'
|
||||||
|
self._sess = Session()
|
||||||
|
self._login(username, password)
|
||||||
|
self._account = account
|
||||||
|
self._zones = None
|
||||||
|
self._zone_records = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def zones(self):
|
||||||
|
if self._zones is None:
|
||||||
|
offset = 0
|
||||||
|
limit = 100
|
||||||
|
zones = []
|
||||||
|
paging = True
|
||||||
|
while paging:
|
||||||
|
data = {'limit': limit, 'q': 'zone_type:PRIMARY',
|
||||||
|
'offset': offset}
|
||||||
|
try:
|
||||||
|
resp = self._get('/v2/zones', params=data)
|
||||||
|
except UltraNoZonesExistException:
|
||||||
|
paging = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
zones.extend(resp['zones'])
|
||||||
|
info = resp['resultInfo']
|
||||||
|
|
||||||
|
if info['offset'] + info['returnedCount'] < info['totalCount']:
|
||||||
|
offset += info['returnedCount']
|
||||||
|
else:
|
||||||
|
paging = False
|
||||||
|
|
||||||
|
self._zones = [z['properties']['name'] for z in zones]
|
||||||
|
|
||||||
|
return self._zones
|
||||||
|
|
||||||
|
def _data_for_multiple(self, _type, records):
|
||||||
|
return {
|
||||||
|
'ttl': records['ttl'],
|
||||||
|
'type': _type,
|
||||||
|
'values': records['rdata'],
|
||||||
|
}
|
||||||
|
|
||||||
|
_data_for_A = _data_for_multiple
|
||||||
|
_data_for_SPF = _data_for_multiple
|
||||||
|
_data_for_NS = _data_for_multiple
|
||||||
|
|
||||||
|
def _data_for_TXT(self, _type, records):
|
||||||
|
return {
|
||||||
|
'ttl': records['ttl'],
|
||||||
|
'type': _type,
|
||||||
|
'values': [r.replace(';', '\\;') for r in records['rdata']],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _data_for_AAAA(self, _type, records):
|
||||||
|
for i, v in enumerate(records['rdata']):
|
||||||
|
records['rdata'][i] = str(ip_address(v))
|
||||||
|
return {
|
||||||
|
'ttl': records['ttl'],
|
||||||
|
'type': _type,
|
||||||
|
'values': records['rdata'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _data_for_single(self, _type, record):
|
||||||
|
return {
|
||||||
|
'type': _type,
|
||||||
|
'ttl': record['ttl'],
|
||||||
|
'value': record['rdata'][0],
|
||||||
|
}
|
||||||
|
|
||||||
|
_data_for_PTR = _data_for_single
|
||||||
|
_data_for_CNAME = _data_for_single
|
||||||
|
|
||||||
|
def _data_for_CAA(self, _type, records):
|
||||||
|
return {
|
||||||
|
'type': _type,
|
||||||
|
'ttl': records['ttl'],
|
||||||
|
'values': [{'flags': x.split()[0],
|
||||||
|
'tag': x.split()[1],
|
||||||
|
'value': x.split()[2].strip('"')}
|
||||||
|
for x in records['rdata']]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _data_for_MX(self, _type, records):
|
||||||
|
return {
|
||||||
|
'type': _type,
|
||||||
|
'ttl': records['ttl'],
|
||||||
|
'values': [{'preference': x.split()[0],
|
||||||
|
'exchange': x.split()[1]}
|
||||||
|
for x in records['rdata']]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _data_for_SRV(self, _type, records):
|
||||||
|
return {
|
||||||
|
'type': _type,
|
||||||
|
'ttl': records['ttl'],
|
||||||
|
'values': [{
|
||||||
|
'priority': x.split()[0],
|
||||||
|
'weight': x.split()[1],
|
||||||
|
'port': x.split()[2],
|
||||||
|
'target': x.split()[3],
|
||||||
|
} for x in records['rdata']]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zone_records(self, zone):
|
||||||
|
if zone.name not in self._zone_records:
|
||||||
|
if zone.name not in self.zones:
|
||||||
|
return []
|
||||||
|
|
||||||
|
records = []
|
||||||
|
path = '/v2/zones/{}/rrsets'.format(zone.name)
|
||||||
|
offset = 0
|
||||||
|
limit = 100
|
||||||
|
paging = True
|
||||||
|
while paging:
|
||||||
|
resp = self._get(path,
|
||||||
|
params={'offset': offset, 'limit': limit})
|
||||||
|
records.extend(resp['rrSets'])
|
||||||
|
info = resp['resultInfo']
|
||||||
|
|
||||||
|
if info['offset'] + info['returnedCount'] < info['totalCount']:
|
||||||
|
offset += info['returnedCount']
|
||||||
|
else:
|
||||||
|
paging = False
|
||||||
|
|
||||||
|
self._zone_records[zone.name] = records
|
||||||
|
return self._zone_records[zone.name]
|
||||||
|
|
||||||
|
def _record_for(self, zone, name, _type, records, lenient):
|
||||||
|
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||||
|
data = data_for(_type, records)
|
||||||
|
record = Record.new(zone, name, data, source=self, lenient=lenient)
|
||||||
|
return record
|
||||||
|
|
||||||
|
def populate(self, zone, target=False, lenient=False):
|
||||||
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||||
|
target, lenient)
|
||||||
|
|
||||||
|
exists = False
|
||||||
|
before = len(zone.records)
|
||||||
|
records = self.zone_records(zone)
|
||||||
|
if records:
|
||||||
|
exists = True
|
||||||
|
values = defaultdict(lambda: defaultdict(None))
|
||||||
|
for record in records:
|
||||||
|
name = zone.hostname_from_fqdn(record['ownerName'])
|
||||||
|
if record['rrtype'] == 'SOA (6)':
|
||||||
|
continue
|
||||||
|
_type = self.RECORDS_TO_TYPE[record['rrtype']]
|
||||||
|
values[name][_type] = record
|
||||||
|
|
||||||
|
for name, types in values.items():
|
||||||
|
for _type, records in types.items():
|
||||||
|
record = self._record_for(zone, name, _type, records,
|
||||||
|
lenient)
|
||||||
|
zone.add_record(record, lenient=lenient)
|
||||||
|
|
||||||
|
self.log.info('populate: found %s records, exists=%s',
|
||||||
|
len(zone.records) - before, exists)
|
||||||
|
return exists
|
||||||
|
|
||||||
|
def _apply(self, plan):
|
||||||
|
desired = plan.desired
|
||||||
|
changes = plan.changes
|
||||||
|
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
|
||||||
|
len(changes))
|
||||||
|
|
||||||
|
name = desired.name
|
||||||
|
if name not in self.zones:
|
||||||
|
self.log.debug('_apply: no matching zone, creating')
|
||||||
|
data = {'properties': {'name': name,
|
||||||
|
'accountName': self._account,
|
||||||
|
'type': 'PRIMARY'},
|
||||||
|
'primaryCreateInfo': {
|
||||||
|
'createType': 'NEW'}}
|
||||||
|
self._post('/v2/zones', json=data)
|
||||||
|
self.zones.append(name)
|
||||||
|
self._zone_records[name] = {}
|
||||||
|
|
||||||
|
for change in changes:
|
||||||
|
class_name = change.__class__.__name__
|
||||||
|
getattr(self, '_apply_{}'.format(class_name))(change)
|
||||||
|
|
||||||
|
# Clear the cache
|
||||||
|
self._zone_records.pop(name, None)
|
||||||
|
|
||||||
|
def _contents_for_multiple_resource_distribution(self, record):
|
||||||
|
if len(record.values) > 1:
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': record.values,
|
||||||
|
'profile': {
|
||||||
|
'@context':
|
||||||
|
'http://schemas.ultradns.com/RDPool.jsonschema',
|
||||||
|
'order': 'FIXED',
|
||||||
|
'description': record.fqdn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': record.values
|
||||||
|
}
|
||||||
|
|
||||||
|
_contents_for_A = _contents_for_multiple_resource_distribution
|
||||||
|
_contents_for_AAAA = _contents_for_multiple_resource_distribution
|
||||||
|
|
||||||
|
def _contents_for_multiple(self, record):
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': record.values
|
||||||
|
}
|
||||||
|
|
||||||
|
_contents_for_NS = _contents_for_multiple
|
||||||
|
_contents_for_SPF = _contents_for_multiple
|
||||||
|
|
||||||
|
def _contents_for_TXT(self, record):
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': [v.replace('\\;', ';') for v in record.values]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _contents_for_CNAME(self, record):
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': [record.value]
|
||||||
|
}
|
||||||
|
|
||||||
|
_contents_for_PTR = _contents_for_CNAME
|
||||||
|
|
||||||
|
def _contents_for_SRV(self, record):
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': ['{} {} {} {}'.format(x.priority,
|
||||||
|
x.weight,
|
||||||
|
x.port,
|
||||||
|
x.target) for x in record.values]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _contents_for_CAA(self, record):
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': ['{} {} {}'.format(x.flags,
|
||||||
|
x.tag,
|
||||||
|
x.value) for x in record.values]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _contents_for_MX(self, record):
|
||||||
|
return {
|
||||||
|
'ttl': record.ttl,
|
||||||
|
'rdata': ['{} {}'.format(x.preference,
|
||||||
|
x.exchange) for x in record.values]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _gen_data(self, record):
|
||||||
|
zone_name = self._remove_prefix(record.fqdn, record.name + '.')
|
||||||
|
path = '/v2/zones/{}/rrsets/{}/{}'.format(zone_name,
|
||||||
|
record._type,
|
||||||
|
record.fqdn)
|
||||||
|
contents_for = getattr(self, '_contents_for_{}'.format(record._type))
|
||||||
|
return path, contents_for(record)
|
||||||
|
|
||||||
|
def _apply_Create(self, change):
|
||||||
|
new = change.new
|
||||||
|
self.log.debug("_apply_Create: name=%s type=%s ttl=%s",
|
||||||
|
new.name,
|
||||||
|
new._type,
|
||||||
|
new.ttl)
|
||||||
|
|
||||||
|
path, content = self._gen_data(new)
|
||||||
|
self._post(path, json=content)
|
||||||
|
|
||||||
|
def _apply_Update(self, change):
|
||||||
|
new = change.new
|
||||||
|
self.log.debug("_apply_Update: name=%s type=%s ttl=%s",
|
||||||
|
new.name,
|
||||||
|
new._type,
|
||||||
|
new.ttl)
|
||||||
|
|
||||||
|
path, content = self._gen_data(new)
|
||||||
|
self.log.debug(path)
|
||||||
|
self.log.debug(content)
|
||||||
|
self._put(path, json=content)
|
||||||
|
|
||||||
|
def _remove_prefix(self, text, prefix):
|
||||||
|
if text.startswith(prefix):
|
||||||
|
return text[len(prefix):]
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _apply_Delete(self, change):
|
||||||
|
existing = change.existing
|
||||||
|
|
||||||
|
for record in self.zone_records(existing.zone):
|
||||||
|
if record['rrtype'] == 'SOA (6)':
|
||||||
|
continue
|
||||||
|
if existing.fqdn == record['ownerName'] and \
|
||||||
|
existing._type == self.RECORDS_TO_TYPE[record['rrtype']]:
|
||||||
|
zone_name = self._remove_prefix(existing.fqdn,
|
||||||
|
existing.name + '.')
|
||||||
|
path = '/v2/zones/{}/rrsets/{}/{}'.format(zone_name,
|
||||||
|
existing._type,
|
||||||
|
existing.fqdn)
|
||||||
|
self._delete(path, json_response=False)
|
||||||
94
tests/fixtures/ultra-records-page-1.json
vendored
Normal file
94
tests/fixtures/ultra-records-page-1.json
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"zoneName": "octodns1.test.",
|
||||||
|
"rrSets": [
|
||||||
|
{
|
||||||
|
"ownerName": "_srv._tcp.octodns1.test.",
|
||||||
|
"rrtype": "SRV (33)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"0 20 443 cname.octodns1.test."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "a.octodns1.test.",
|
||||||
|
"rrtype": "A (1)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"1.1.1.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "aaaa.octodns1.test.",
|
||||||
|
"rrtype": "AAAA (28)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"0:0:0:0:0:0:0:1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "caa.octodns1.test.",
|
||||||
|
"rrtype": "CAA (257)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"0 issue \"symantec.com\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "cname.octodns1.test.",
|
||||||
|
"rrtype": "CNAME (5)",
|
||||||
|
"ttl": 60,
|
||||||
|
"rdata": [
|
||||||
|
"a.octodns1.test."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "mail.octodns1.test.",
|
||||||
|
"rrtype": "MX (15)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"1 aspmx.l.google.com.",
|
||||||
|
"5 alt1.aspmx.l.google.com."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "octodns1.test.",
|
||||||
|
"rrtype": "NS (2)",
|
||||||
|
"ttl": 86400,
|
||||||
|
"rdata": [
|
||||||
|
"pdns1.ultradns.biz.",
|
||||||
|
"pdns1.ultradns.com.",
|
||||||
|
"pdns1.ultradns.net.",
|
||||||
|
"pdns1.ultradns.org."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "octodns1.test.",
|
||||||
|
"rrtype": "SOA (6)",
|
||||||
|
"ttl": 86400,
|
||||||
|
"rdata": [
|
||||||
|
"pdns1.ultradns.com. phelps.netflix.com. 2020062003 86400 86400 86400 86400"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "ptr.octodns1.test.",
|
||||||
|
"rrtype": "PTR (12)",
|
||||||
|
"ttl": 300,
|
||||||
|
"rdata": [
|
||||||
|
"foo.bar.com."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "spf.octodns1.test.",
|
||||||
|
"rrtype": "SPF (99)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"v=spf1 -all"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resultInfo": {
|
||||||
|
"totalCount": 12,
|
||||||
|
"offset": 0,
|
||||||
|
"returnedCount": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
34
tests/fixtures/ultra-records-page-2.json
vendored
Normal file
34
tests/fixtures/ultra-records-page-2.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"zoneName": "octodns1.test.",
|
||||||
|
"rrSets": [
|
||||||
|
{
|
||||||
|
"ownerName": "txt.octodns1.test.",
|
||||||
|
"rrtype": "TXT (16)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"foobar",
|
||||||
|
"v=spf1 -all"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "octodns1.test.",
|
||||||
|
"rrtype": "A (1)",
|
||||||
|
"ttl": 3600,
|
||||||
|
"rdata": [
|
||||||
|
"1.2.3.4",
|
||||||
|
"1.2.3.5",
|
||||||
|
"1.2.3.6"
|
||||||
|
],
|
||||||
|
"profile": {
|
||||||
|
"@context": "http://schemas.ultradns.com/RDPool.jsonschema",
|
||||||
|
"order": "FIXED",
|
||||||
|
"description": "octodns1.test."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resultInfo": {
|
||||||
|
"totalCount": 12,
|
||||||
|
"offset": 10,
|
||||||
|
"returnedCount": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
135
tests/fixtures/ultra-zones-page-1.json
vendored
Normal file
135
tests/fixtures/ultra-zones-page-1.json
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"queryInfo": {
|
||||||
|
"q": "zone_type:PRIMARY",
|
||||||
|
"sort": "NAME",
|
||||||
|
"reverse": false,
|
||||||
|
"limit": 10
|
||||||
|
},
|
||||||
|
"resultInfo": {
|
||||||
|
"totalCount": 20,
|
||||||
|
"offset": 0,
|
||||||
|
"returnedCount": 10
|
||||||
|
},
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns1.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:05Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns10.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns11.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns12.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns13.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns14.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns15.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns16.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns17.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns18.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:07Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
135
tests/fixtures/ultra-zones-page-2.json
vendored
Normal file
135
tests/fixtures/ultra-zones-page-2.json
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"queryInfo": {
|
||||||
|
"q": "zone_type:PRIMARY",
|
||||||
|
"sort": "NAME",
|
||||||
|
"reverse": false,
|
||||||
|
"limit": 10
|
||||||
|
},
|
||||||
|
"resultInfo": {
|
||||||
|
"totalCount": 20,
|
||||||
|
"offset": 10,
|
||||||
|
"returnedCount": 10
|
||||||
|
},
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns19.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:07Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns2.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:05Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns20.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:07Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns3.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:05Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns4.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:05Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns5.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:05Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns6.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:05Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns7.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns8.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "octodns9.test.",
|
||||||
|
"accountName": "Netflix - Automation",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "phelpstest",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T01:06Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
377
tests/test_octodns_provider_ultra.py
Normal file
377
tests/test_octodns_provider_ultra.py
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
from mock import Mock, call
|
||||||
|
from os.path import dirname, join
|
||||||
|
from requests_mock import ANY, mock as requests_mock
|
||||||
|
from six import text_type
|
||||||
|
from unittest import TestCase
|
||||||
|
from json import load as json_load
|
||||||
|
|
||||||
|
from octodns.record import Record
|
||||||
|
from octodns.provider.ultra import UltraProvider, UltraNoZonesExistException
|
||||||
|
from octodns.provider.yaml import YamlProvider
|
||||||
|
from octodns.zone import Zone
|
||||||
|
|
||||||
|
|
||||||
|
def _get_provider():
|
||||||
|
'''
|
||||||
|
Helper to return a provider after going through authentication sequence
|
||||||
|
'''
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post('https://restapi.ultradns.com/v2/authorization/token',
|
||||||
|
status_code=200,
|
||||||
|
text='{"token type": "Bearer", "refresh_token": "abc", '
|
||||||
|
'"access_token":"123", "expires_in": "3600"}')
|
||||||
|
return UltraProvider('test', 'testacct', 'user', 'pass')
|
||||||
|
|
||||||
|
|
||||||
|
class TestUltraProvider(TestCase):
|
||||||
|
expected = Zone('unit.tests.', [])
|
||||||
|
host = 'https://restapi.ultradns.com'
|
||||||
|
empty_body = [{"errorCode": 70002, "errorMessage": "Data not found."}]
|
||||||
|
|
||||||
|
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_login(self):
|
||||||
|
path = '/v2/authorization/token'
|
||||||
|
|
||||||
|
# Bad Auth
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post('{}{}'.format(self.host, path), status_code=401,
|
||||||
|
text='{"errorCode": 60001}')
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
UltraProvider('test', 'account', 'user', 'wrongpass')
|
||||||
|
self.assertEquals('Unauthorized', text_type(ctx.exception))
|
||||||
|
|
||||||
|
# Good Auth
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post('{}{}'.format(self.host, path), status_code=200,
|
||||||
|
text='{"token type": "Bearer", "refresh_token": "abc", '
|
||||||
|
'"access_token":"123", "expires_in": "3600"}')
|
||||||
|
UltraProvider('test', 'account', 'user', 'pass',
|
||||||
|
test_endpoint=False)
|
||||||
|
|
||||||
|
with requests_mock() as mock:
|
||||||
|
test_host = 'https://test-restapi.ultradns.com'
|
||||||
|
mock.post('{}{}'.format(test_host, path), status_code=200,
|
||||||
|
text='{"token type": "Bearer", "refresh_token": "abc", '
|
||||||
|
'"access_token":"123", "expires_in": "3600"}')
|
||||||
|
UltraProvider('test', 'account', 'user', 'pass',
|
||||||
|
test_endpoint=True)
|
||||||
|
|
||||||
|
def test_get_zones(self):
|
||||||
|
provider = _get_provider()
|
||||||
|
path = "/v2/zones"
|
||||||
|
|
||||||
|
# Test no zones exist error
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.get('{}{}'.format(self.host, path), status_code=404,
|
||||||
|
headers={'Authorization': 'Bearer 123'},
|
||||||
|
json=self.empty_body)
|
||||||
|
zones = provider.zones
|
||||||
|
self.assertEquals(list(), zones)
|
||||||
|
|
||||||
|
# Reset zone cache so they are queried again
|
||||||
|
provider._zones = None
|
||||||
|
|
||||||
|
with requests_mock() as mock:
|
||||||
|
payload = {
|
||||||
|
"resultInfo": {
|
||||||
|
"totalCount": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"returnedCount": 1
|
||||||
|
},
|
||||||
|
"zones": [
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"name": "testzone123.com.",
|
||||||
|
"accountName": "testaccount",
|
||||||
|
"type": "PRIMARY",
|
||||||
|
"dnssecStatus": "UNSIGNED",
|
||||||
|
"status": "ACTIVE",
|
||||||
|
"owner": "user",
|
||||||
|
"resourceRecordCount": 5,
|
||||||
|
"lastModifiedDateTime": "2020-06-19T00:47Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.get('{}{}'.format(self.host, path), status_code=200,
|
||||||
|
headers={'Authorization': 'Bearer 123'},
|
||||||
|
json=payload)
|
||||||
|
zones = provider.zones
|
||||||
|
self.assertEquals(1, len(zones))
|
||||||
|
self.assertEquals('testzone123.com.', zones[0])
|
||||||
|
|
||||||
|
# Test different paging behavior
|
||||||
|
provider._zones = None
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=0'
|
||||||
|
.format(self.host, path), status_code=200,
|
||||||
|
json={"resultInfo": {"totalCount": 15,
|
||||||
|
"offset": 0,
|
||||||
|
"returnedCount": 10},
|
||||||
|
"zones": []})
|
||||||
|
mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=10'
|
||||||
|
.format(self.host, path), status_code=200,
|
||||||
|
json={"resultInfo": {"totalCount": 15,
|
||||||
|
"offset": 10,
|
||||||
|
"returnedCount": 5},
|
||||||
|
"zones": []})
|
||||||
|
zones = provider.zones
|
||||||
|
self.assertEquals(mock.call_count, 2)
|
||||||
|
|
||||||
|
def test_request(self):
|
||||||
|
provider = _get_provider()
|
||||||
|
path = '/foo'
|
||||||
|
payload = {'a': 1}
|
||||||
|
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.get('{}{}'.format(self.host, path), status_code=401,
|
||||||
|
headers={'Authorization': 'Bearer 123'}, json={})
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
provider._get(path)
|
||||||
|
self.assertEquals('Unauthorized', text_type(ctx.exception))
|
||||||
|
|
||||||
|
# Test all GET patterns
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.get('{}{}'.format(self.host, path), status_code=200,
|
||||||
|
headers={'Authorization': 'Bearer 123'},
|
||||||
|
json=payload)
|
||||||
|
provider._get(path, json=payload)
|
||||||
|
|
||||||
|
mock.get('{}{}?a=1'.format(self.host, path), status_code=200,
|
||||||
|
headers={'Authorization': 'Bearer 123'})
|
||||||
|
provider._get(path, params=payload, json_response=False)
|
||||||
|
|
||||||
|
# Test all POST patterns
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post('{}{}'.format(self.host, path), status_code=200,
|
||||||
|
headers={'Authorization': 'Bearer 123'},
|
||||||
|
json=payload)
|
||||||
|
provider._post(path, json=payload)
|
||||||
|
|
||||||
|
mock.post('{}{}'.format(self.host, path), status_code=200,
|
||||||
|
headers={'Authorization': 'Bearer 123'},
|
||||||
|
text="{'a':1}")
|
||||||
|
provider._post(path, data=payload, json_response=False)
|
||||||
|
|
||||||
|
# Test all PUT patterns
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.put('{}{}'.format(self.host, path), status_code=200,
|
||||||
|
headers={'Authorization': 'Bearer 123'},
|
||||||
|
json=payload)
|
||||||
|
provider._put(path, json=payload)
|
||||||
|
|
||||||
|
# Test all DELETE patterns
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.delete('{}{}'.format(self.host, path), status_code=200,
|
||||||
|
headers={'Authorization': 'Bearer 123'})
|
||||||
|
provider._delete(path, json_response=False)
|
||||||
|
|
||||||
|
def test_zone_records(self):
|
||||||
|
provider = _get_provider()
|
||||||
|
zone_payload = {
|
||||||
|
"resultInfo": {"totalCount": 1,
|
||||||
|
"offset": 0,
|
||||||
|
"returnedCount": 1},
|
||||||
|
"zones": [{"properties": {"name": "octodns1.test."}}]}
|
||||||
|
|
||||||
|
records_payload = {
|
||||||
|
"zoneName": "octodns1.test.",
|
||||||
|
"rrSets": [
|
||||||
|
{
|
||||||
|
"ownerName": "octodns1.test.",
|
||||||
|
"rrtype": "NS (2)",
|
||||||
|
"ttl": 86400,
|
||||||
|
"rdata": [
|
||||||
|
"ns1.octodns1.test."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ownerName": "octodns1.test.",
|
||||||
|
"rrtype": "SOA (6)",
|
||||||
|
"ttl": 86400,
|
||||||
|
"rdata": [
|
||||||
|
"pdns1.ultradns.com. phelps.netflix.com. 1 10 10 10 10"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"resultInfo": {
|
||||||
|
"totalCount": 2,
|
||||||
|
"offset": 0,
|
||||||
|
"returnedCount": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zone_path = '/v2/zones'
|
||||||
|
rec_path = '/v2/zones/octodns1.test./rrsets'
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=0'
|
||||||
|
.format(self.host, zone_path),
|
||||||
|
status_code=200, json=zone_payload)
|
||||||
|
mock.get('{}{}?offset=0&limit=100'.format(self.host, rec_path),
|
||||||
|
status_code=200, json=records_payload)
|
||||||
|
|
||||||
|
zone = Zone('octodns1.test.', [])
|
||||||
|
self.assertTrue(provider.zone_records(zone))
|
||||||
|
self.assertEquals(mock.call_count, 2)
|
||||||
|
|
||||||
|
# Populate the same zone again and confirm cache is hit
|
||||||
|
self.assertTrue(provider.zone_records(zone))
|
||||||
|
self.assertEquals(mock.call_count, 2)
|
||||||
|
|
||||||
|
def test_populate(self):
|
||||||
|
provider = _get_provider()
|
||||||
|
|
||||||
|
# Non-existent zone doesn't populate anything
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.get(ANY, status_code=404, json=self.empty_body)
|
||||||
|
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
provider.populate(zone)
|
||||||
|
self.assertEquals(set(), zone.records)
|
||||||
|
|
||||||
|
# re-populating the same non-existent zone uses cache and makes no
|
||||||
|
# calls
|
||||||
|
again = Zone('unit.tests.', [])
|
||||||
|
provider.populate(again)
|
||||||
|
self.assertEquals(set(), again.records)
|
||||||
|
|
||||||
|
# Test zones with data
|
||||||
|
provider._zones = None
|
||||||
|
path = '/v2/zones'
|
||||||
|
with requests_mock() as mock:
|
||||||
|
with open('tests/fixtures/ultra-zones-page-1.json') as fh:
|
||||||
|
mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=0'
|
||||||
|
.format(self.host, path),
|
||||||
|
status_code=200, text=fh.read())
|
||||||
|
with open('tests/fixtures/ultra-zones-page-2.json') as fh:
|
||||||
|
mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=10'
|
||||||
|
.format(self.host, path),
|
||||||
|
status_code=200, text=fh.read())
|
||||||
|
with open('tests/fixtures/ultra-records-page-1.json') as fh:
|
||||||
|
rec_path = '/v2/zones/octodns1.test./rrsets'
|
||||||
|
mock.get('{}{}?offset=0&limit=100'.format(self.host, rec_path),
|
||||||
|
status_code=200, text=fh.read())
|
||||||
|
with open('tests/fixtures/ultra-records-page-2.json') as fh:
|
||||||
|
rec_path = '/v2/zones/octodns1.test./rrsets'
|
||||||
|
mock.get('{}{}?offset=10&limit=100'
|
||||||
|
.format(self.host, rec_path),
|
||||||
|
status_code=200, text=fh.read())
|
||||||
|
|
||||||
|
zone = Zone('octodns1.test.', [])
|
||||||
|
self.assertTrue(provider.populate(zone))
|
||||||
|
self.assertEquals('octodns1.test.', zone.name)
|
||||||
|
self.assertEquals(11, len(zone.records))
|
||||||
|
self.assertEquals(mock.call_count, 4)
|
||||||
|
|
||||||
|
def test_apply(self):
|
||||||
|
provider = _get_provider()
|
||||||
|
|
||||||
|
provider._request = Mock()
|
||||||
|
|
||||||
|
provider._request.side_effect = [
|
||||||
|
UltraNoZonesExistException('No Zones'),
|
||||||
|
None, # zone create
|
||||||
|
] + [None] * 13 # individual record creates
|
||||||
|
|
||||||
|
# non-existent zone, create everything
|
||||||
|
plan = provider.plan(self.expected)
|
||||||
|
self.assertEquals(13, len(plan.changes))
|
||||||
|
self.assertEquals(13, provider.apply(plan))
|
||||||
|
self.assertFalse(plan.exists)
|
||||||
|
|
||||||
|
provider._request.assert_has_calls([
|
||||||
|
# created the domain
|
||||||
|
call('POST', '/v2/zones', json={
|
||||||
|
'properties': {'name': 'unit.tests.',
|
||||||
|
'accountName': 'testacct',
|
||||||
|
'type': 'PRIMARY'},
|
||||||
|
'primaryCreateInfo': {'createType': 'NEW'}}),
|
||||||
|
# Validate multi-ip apex A record is correct
|
||||||
|
call('POST', '/v2/zones/unit.tests./rrsets/A/unit.tests.', json={
|
||||||
|
'ttl': 300,
|
||||||
|
'rdata': ['1.2.3.4', '1.2.3.5'],
|
||||||
|
'profile': {
|
||||||
|
'@context':
|
||||||
|
'http://schemas.ultradns.com/RDPool.jsonschema',
|
||||||
|
'order': 'FIXED',
|
||||||
|
'description': 'unit.tests.'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
# make sure semicolons are not escaped when sending data
|
||||||
|
call('POST', '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.',
|
||||||
|
json={'ttl': 600,
|
||||||
|
'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']}),
|
||||||
|
], True)
|
||||||
|
# expected number of total calls
|
||||||
|
self.assertEquals(15, provider._request.call_count)
|
||||||
|
|
||||||
|
# Create sample rrset payload to attempt to alter
|
||||||
|
page1 = json_load(open('tests/fixtures/ultra-records-page-1.json'))
|
||||||
|
page2 = json_load(open('tests/fixtures/ultra-records-page-2.json'))
|
||||||
|
mock_rrsets = list()
|
||||||
|
mock_rrsets.extend(page1['rrSets'])
|
||||||
|
mock_rrsets.extend(page2['rrSets'])
|
||||||
|
|
||||||
|
# Seed a bunch of records into a zone and verify update / delete ops
|
||||||
|
provider._request.reset_mock()
|
||||||
|
provider._zones = ['octodns1.test.']
|
||||||
|
provider.zone_records = Mock(return_value=mock_rrsets)
|
||||||
|
|
||||||
|
provider._request.side_effect = [None] * 13
|
||||||
|
|
||||||
|
wanted = Zone('octodns1.test.', [])
|
||||||
|
wanted.add_record(Record.new(wanted, '', {
|
||||||
|
'ttl': 60, # Change TTL
|
||||||
|
'type': 'A',
|
||||||
|
'value': '5.6.7.8' # Change number of IPs (3 -> 1)
|
||||||
|
}))
|
||||||
|
# TODO: Figure out why this isn't happening
|
||||||
|
wanted.add_record(Record.new(wanted, '', {
|
||||||
|
'ttl': 3600, # TTL change
|
||||||
|
'type': 'NS',
|
||||||
|
'values': [ # Add additional NS records
|
||||||
|
"pdns1.ultradns.biz.",
|
||||||
|
"pdns1.ultradns.com.",
|
||||||
|
"pdns1.ultradns.net.",
|
||||||
|
"pdns1.ultradns.org.",
|
||||||
|
"pdns2.ultradns.biz.",
|
||||||
|
"pdns2.ultradns.com.",
|
||||||
|
"pdns2.ultradns.net.",
|
||||||
|
"pdns2.ultradns.org.",
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
wanted.add_record(Record.new(wanted, 'txt', {
|
||||||
|
'ttl': 3600,
|
||||||
|
'type': 'TXT',
|
||||||
|
'values': [ # Alter TXT value
|
||||||
|
"foobar",
|
||||||
|
"v=spf1 include:mail.server.net ?all"
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
|
||||||
|
plan = provider.plan(wanted)
|
||||||
|
# TODO: 11 expected but NS isn't being respected
|
||||||
|
self.assertEquals(10, len(plan.changes))
|
||||||
|
self.assertEquals(10, provider.apply(plan))
|
||||||
|
self.assertTrue(plan.exists)
|
||||||
Reference in New Issue
Block a user