mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Adding Octodns provider class for easyDNS
This provider class for easydns.com adds support for basic dns records through the easyDNS v3 API. Support for dynamic and geo based dns records is planned for a future update. Sample configuration for the easyDNS provider are: easydns: class: octodns.provider.easydns.EasyDNSProvider token: <token> apikey: <key> The token and key values are found on the easyDNS customer portal at: https://cp.easydns.com/manage/security/api/production_info.php Also, below are some optional configuration parameters which can be added to override the class defaults. By default the provider class connects with the LIVE easyDNS API, if you wish to perform testing with the easyDNS Sandbox API you can enable it by adding the following configuration parameter: sandbox: True Note, the API token and key are different for the sandbox than they are for the production API, you can obtain sandbox credentials at: https://cp.easydns.com/manage/security/api/sandbox_info.php Lastly, if you have created Domain Portfolios through the easyDNS CP you can configure which portfolio new domains will be added to by supplying the portfolio option with the name of your portfolio. portfolio: <portfolio name>
This commit is contained in:
@@ -186,6 +186,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| [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 |
|
||||
| [DynProvider](/octodns/provider/dyn.py) | dyn | All | Both | |
|
||||
| [EasyDNSProvider](/octodns/provider/easydns.py) | | A, AAAA, CAA, CNAME, MX, NAPTR, NS, SRV, TXT | No | |
|
||||
| [EtcHostsProvider](/octodns/provider/etc_hosts.py) | | A, AAAA, ALIAS, CNAME | No | |
|
||||
| [GoogleCloudProvider](/octodns/provider/googlecloud.py) | google-cloud-dns | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | No | |
|
||||
| [MythicBeastsProvider](/octodns/provider/mythicbeasts.py) | Mythic Beasts | A, AAAA, ALIAS, CNAME, MX, NS, SRV, SSHFP, CAA, TXT | No | |
|
||||
@@ -283,8 +284,8 @@ OctoDNS is licensed under the [MIT license](LICENSE).
|
||||
|
||||
The MIT license grant is not for GitHub's trademarks, which include the logo designs. GitHub reserves all trademark and copyright rights in and to all GitHub trademarks. GitHub's logos include, for instance, the stylized designs that include "logo" in the file title in the following folder: https://github.com/github/octodns/tree/master/docs/logos/
|
||||
|
||||
GitHub® and its stylized versions and the Invertocat mark are GitHub's Trademarks or registered Trademarks. When using GitHub's logos, be sure to follow the GitHub logo guidelines.
|
||||
GitHub<EFBFBD> and its stylized versions and the Invertocat mark are GitHub's Trademarks or registered Trademarks. When using GitHub's logos, be sure to follow the GitHub logo guidelines.
|
||||
|
||||
## Authors
|
||||
|
||||
OctoDNS was designed and authored by [Ross McFarland](https://github.com/ross) and [Joe Williams](https://github.com/joewilliams). It is now maintained, reviewed, and tested by Traffic Engineering team at GitHub.
|
||||
OctoDNS was designed and authored by [Ross McFarland](https://github.com/ross) and [Joe Williams](https://github.com/joewilliams). It is now maintained, reviewed, and tested by Traffic Engineering team at GitHub.
|
453
octodns/provider/easydns.py
Normal file
453
octodns/provider/easydns.py
Normal file
@@ -0,0 +1,453 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
from requests import Session
|
||||
from time import sleep
|
||||
import logging
|
||||
import base64
|
||||
|
||||
from ..record import Record
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
class EasyDNSClientException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class EasyDNSClientBadRequest(EasyDNSClientException):
|
||||
|
||||
def __init__(self):
|
||||
super(EasyDNSClientBadRequest, self).__init__('Bad request')
|
||||
|
||||
|
||||
class EasyDNSClientNotFound(EasyDNSClientException):
|
||||
|
||||
def __init__(self):
|
||||
super(EasyDNSClientNotFound, self).__init__('Not Found')
|
||||
|
||||
|
||||
class EasyDNSClientUnauthorized(EasyDNSClientException):
|
||||
|
||||
def __init__(self):
|
||||
super(EasyDNSClientUnauthorized, self).__init__('Unauthorized')
|
||||
|
||||
|
||||
class EasyDNSClient(object):
|
||||
# EasyDNS Sandbox API
|
||||
SANDBOX = 'https://sandbox.rest.easydns.net'
|
||||
# EasyDNS Live API
|
||||
LIVE = 'https://rest.easydns.net'
|
||||
# Default Currency CAD
|
||||
defaultCurrency = 'CAD'
|
||||
# Domain Portfolio
|
||||
domainPortfolio = 'myport'
|
||||
|
||||
def __init__(self, token, apikey, currency, portfolio, sandbox):
|
||||
self.log = logging.getLogger('EasyDNSProvider[{}]'.format(id))
|
||||
self.token = token
|
||||
self.apikey = apikey
|
||||
self.defaultCurrency = currency
|
||||
self.domainPortfolio = portfolio
|
||||
self.apienv = 'sandbox' if sandbox else 'live'
|
||||
authkey = '{}:{}'.format(self.token, self.apikey)
|
||||
self.authkey = base64.b64encode(authkey.encode("utf-8"))
|
||||
self.basepath = self.SANDBOX if sandbox else self.LIVE
|
||||
sess = Session()
|
||||
sess.headers.update({'Authorization': 'Basic {}'.format(self.authkey)})
|
||||
sess.headers.update({'accept': 'application/json'})
|
||||
self._sess = sess
|
||||
|
||||
def _request(self, method, path, params=None, data=None):
|
||||
url = '{}{}'.format(self.basepath, path)
|
||||
resp = self._sess.request(method, url, params=params, json=data)
|
||||
if resp.status_code == 400:
|
||||
self.log.debug('Response code 400, path=%s', path)
|
||||
if method == 'GET' and path[:8] == '/domain/':
|
||||
raise EasyDNSClientNotFound()
|
||||
raise EasyDNSClientBadRequest()
|
||||
if resp.status_code == 401:
|
||||
raise EasyDNSClientUnauthorized()
|
||||
if resp.status_code == 403 or resp.status_code == 404:
|
||||
raise EasyDNSClientNotFound()
|
||||
resp.raise_for_status()
|
||||
return resp
|
||||
|
||||
def domain(self, name):
|
||||
path = '/domain/{}'.format(name)
|
||||
return self._request('GET', path).json()
|
||||
|
||||
def domain_create(self, name):
|
||||
# EasyDNS allows for new domains to be created for the purpose of DNS
|
||||
# only, or with domain registration. This function creates a DNS only
|
||||
# record expectig the domain to be registered already
|
||||
path = '/domains/add/{}'.format(name)
|
||||
domainData = {'service': 'dns',
|
||||
'term': 1,
|
||||
'dns_only': 1,
|
||||
'portfolio': self.domainPortfolio,
|
||||
'currency': self.defaultCurrency}
|
||||
self._request('PUT', path, data=domainData).json()
|
||||
|
||||
# EasyDNS creates default records for MX, A and CNAME for new domains,
|
||||
# we need to delete those default record so we can sync with the source
|
||||
# records, first we'll sleep for a second before gathering new records
|
||||
# We also create default NS records, but they won't be deleted
|
||||
sleep(1)
|
||||
records = self.records(name, True)
|
||||
for record in records:
|
||||
if record['type'] in ('A', 'MX', 'CNAME'):
|
||||
self.record_delete(name, record['id'])
|
||||
|
||||
def records(self, zone_name, raw=False):
|
||||
if raw:
|
||||
path = '/zones/records/all/{}'.format(zone_name)
|
||||
else:
|
||||
path = '/zones/records/parsed/{}'.format(zone_name)
|
||||
|
||||
ret = []
|
||||
resp = self._request('GET', path).json()
|
||||
ret += resp['data']
|
||||
|
||||
# EasyDNS supports URL forwarding, stealth URL forwarding and DYNamic
|
||||
# A records so we'll convert them to their underlying DNS record
|
||||
# types before processing
|
||||
for record in ret:
|
||||
# change any apex record to empty string
|
||||
if record['host'] == '@':
|
||||
record['host'] = ''
|
||||
|
||||
# change any apex value to zone name
|
||||
if record['rdata'] == '@':
|
||||
record['rdata'] = '{}.'.format(zone_name)
|
||||
|
||||
# change "URL" & "STEALTH" to a "CNAME"
|
||||
if record['type'] == "URL" or record['type'] == "STEALTH":
|
||||
record['type'] = 'CNAME'
|
||||
|
||||
if record['type'] == "DYN":
|
||||
record['type'] = 'A'
|
||||
|
||||
return ret
|
||||
|
||||
def record_create(self, zone_name, params):
|
||||
path = '/zones/records/add/{}/{}'.format(zone_name, params['type'])
|
||||
# change empty name string to @, EasyDNS uses @ for apex record names
|
||||
params['host'] = params['name']
|
||||
if params['host'] == '':
|
||||
params['host'] = '@'
|
||||
self._request('PUT', path, data=params)
|
||||
|
||||
def record_delete(self, zone_name, record_id):
|
||||
path = '/zones/records/{}/{}'.format(zone_name, record_id)
|
||||
self._request('DELETE', path)
|
||||
|
||||
|
||||
class EasyDNSProvider(BaseProvider):
|
||||
'''
|
||||
EasyDNS provider using API v3
|
||||
|
||||
easydns:
|
||||
class: octodns.provider.easydns.EasyDNSProvider
|
||||
# Your EasyDNS API token (required)
|
||||
token: foo
|
||||
# Your EasyDNS API Key (required)
|
||||
apikey: bar
|
||||
# Use SandBox or Live environment, optional, defaults to live
|
||||
sandbox: False
|
||||
# Currency to use for creating domains, default CAD
|
||||
defaultCurrency: CAD
|
||||
# Domain Portfolio under which to create domains
|
||||
portfolio: myport
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'TXT',
|
||||
'SRV', 'NAPTR'))
|
||||
|
||||
def __init__(self, id, token, apikey, currency='CAD', portfolio='myport',
|
||||
sandbox=False, *args, **kwargs):
|
||||
self.log = logging.getLogger('EasyDNSProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, token=***', id)
|
||||
super(EasyDNSProvider, self).__init__(id, *args, **kwargs)
|
||||
self._client = EasyDNSClient(token, apikey, currency, portfolio,
|
||||
sandbox)
|
||||
self._zone_records = {}
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': [r['rdata'] for r in records]
|
||||
}
|
||||
|
||||
_data_for_A = _data_for_multiple
|
||||
_data_for_AAAA = _data_for_multiple
|
||||
|
||||
def _data_for_CAA(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
try:
|
||||
flags, tag, value = record['rdata'].split(' ', 2)
|
||||
except ValueError:
|
||||
continue
|
||||
values.append({
|
||||
'flags': flags,
|
||||
'tag': tag,
|
||||
'value': value,
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_NAPTR(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
try:
|
||||
order, preference, flags, service, regexp, replacement = \
|
||||
record['rdata'].split(' ', 5)
|
||||
except ValueError:
|
||||
continue
|
||||
values.append({
|
||||
'flags': flags[1:-1],
|
||||
'order': order,
|
||||
'preference': preference,
|
||||
'regexp': regexp[1:-1],
|
||||
'replacement': replacement,
|
||||
'service': service[1:-1],
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records[0]['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
record = records[0]
|
||||
return {
|
||||
'ttl': record['ttl'],
|
||||
'type': _type,
|
||||
'value': '{}'.format(record['rdata'])
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
values.append({
|
||||
'preference': record['prio'],
|
||||
'exchange': '{}'.format(record['rdata'])
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_NS(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
data = '{}'.format(record['rdata'])
|
||||
values.append(data)
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
record = records[0]
|
||||
for record in records:
|
||||
try:
|
||||
priority, weight, port, target = record['rdata'].split(' ', 3)
|
||||
except ValueError:
|
||||
rdata = record['rdata'].split(' ', 3)
|
||||
priority = 0
|
||||
weight = 0
|
||||
port = 0
|
||||
target = ''
|
||||
if len(rdata) != 0 and rdata[0] != '':
|
||||
priority = rdata[0]
|
||||
if len(rdata) >= 2:
|
||||
weight = rdata[1]
|
||||
if len(rdata) >= 3:
|
||||
port = rdata[2]
|
||||
values.append({
|
||||
'port': int(port),
|
||||
'priority': int(priority),
|
||||
'target': target,
|
||||
'weight': int(weight)
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records[0]['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
values = ['"' + value['rdata'].replace(';', '\\;') +
|
||||
'"' for value in records]
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def zone_records(self, zone):
|
||||
if zone.name not in self._zone_records:
|
||||
try:
|
||||
self._zone_records[zone.name] = \
|
||||
self._client.records(zone.name[:-1])
|
||||
except EasyDNSClientNotFound:
|
||||
return []
|
||||
|
||||
return self._zone_records[zone.name]
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in self.zone_records(zone):
|
||||
_type = record['type']
|
||||
if _type not in self.SUPPORTS:
|
||||
self.log.warning('populate: skipping unsupported %s record',
|
||||
_type)
|
||||
continue
|
||||
values[record['host']][record['type']].append(record)
|
||||
|
||||
before = len(zone.records)
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
data_for = getattr(self, '_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):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'rdata': value,
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
_params_for_A = _params_for_multiple
|
||||
_params_for_AAAA = _params_for_multiple
|
||||
_params_for_NS = _params_for_multiple
|
||||
|
||||
def _params_for_CAA(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'rdata': "{} {} {}".format(value.flags, value.tag,
|
||||
value.value),
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
def _params_for_NAPTR(self, record):
|
||||
for value in record.values:
|
||||
content = '{} {} "{}" "{}" "{}" {}'.format(value.order,
|
||||
value.preference,
|
||||
value.flags,
|
||||
value.service,
|
||||
value.regexp,
|
||||
value.replacement)
|
||||
yield {
|
||||
'rdata': content,
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
def _params_for_single(self, record):
|
||||
yield {
|
||||
'rdata': record.value,
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
_params_for_CNAME = _params_for_single
|
||||
|
||||
def _params_for_MX(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'rdata': value.exchange,
|
||||
'name': record.name,
|
||||
'prio': value.preference,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
def _params_for_SRV(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'rdata': "{} {} {} {}".format(value.priority, value.port,
|
||||
value.weight, value.target),
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type,
|
||||
}
|
||||
|
||||
def _params_for_TXT(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'rdata': '"' + value.replace('\\;', ';') + '"',
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
def _apply_Create(self, change):
|
||||
new = change.new
|
||||
params_for = getattr(self, '_params_for_{}'.format(new._type))
|
||||
for params in params_for(new):
|
||||
self._client.record_create(new.zone.name[:-1], params)
|
||||
|
||||
def _apply_Update(self, change):
|
||||
self._apply_Delete(change)
|
||||
self._apply_Create(change)
|
||||
|
||||
def _apply_Delete(self, change):
|
||||
existing = change.existing
|
||||
zone = existing.zone
|
||||
for record in self.zone_records(zone):
|
||||
self.log.debug('apply_Delete: zone=%s, type=%s, host=%s', zone,
|
||||
record['type'], record['host'])
|
||||
if existing.name == record['host'] and \
|
||||
existing._type == record['type']:
|
||||
self._client.record_delete(zone.name[:-1], record['id'])
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
changes = plan.changes
|
||||
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
|
||||
len(changes))
|
||||
|
||||
domain_name = desired.name[:-1]
|
||||
try:
|
||||
self._client.domain(domain_name)
|
||||
except EasyDNSClientNotFound:
|
||||
self.log.debug('_apply: no matching zone, creating domain')
|
||||
self._client.domain_create(domain_name)
|
||||
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
getattr(self, '_apply_{}'.format(class_name))(change)
|
||||
|
||||
# Clear out the cache if any
|
||||
self._zone_records.pop(desired.name, None)
|
274
tests/fixtures/easydns-records.json
vendored
Normal file
274
tests/fixtures/easydns-records.json
vendored
Normal file
@@ -0,0 +1,274 @@
|
||||
{
|
||||
"tm": 1000000000,
|
||||
"data": [
|
||||
{
|
||||
"id": "12340001",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "3600",
|
||||
"prio": "0",
|
||||
"type": "SOA",
|
||||
"rdata": "dns1.easydns.com. zone.easydns.com. 2020010101 3600 600 604800 0",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340002",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "300",
|
||||
"prio": "0",
|
||||
"type": "A",
|
||||
"rdata": "1.2.3.4",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340003",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "300",
|
||||
"prio": "0",
|
||||
"type": "DYN",
|
||||
"rdata": "1.2.3.5",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340004",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": null,
|
||||
"type": "NS",
|
||||
"rdata": "6.2.3.4.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340005",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": null,
|
||||
"type": "NS",
|
||||
"rdata": "7.2.3.4.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340006",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "3600",
|
||||
"prio": "0",
|
||||
"type": "CAA",
|
||||
"rdata": "0 issue ca.unit.tests",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340007",
|
||||
"domain": "unit.tests",
|
||||
"host": "_srv._tcp",
|
||||
"ttl": "600",
|
||||
"prio": "12",
|
||||
"type": "SRV",
|
||||
"rdata": "12 20 30 foo-2.unit.tests.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340008",
|
||||
"domain": "unit.tests",
|
||||
"host": "_srv._tcp",
|
||||
"ttl": "600",
|
||||
"prio": "12",
|
||||
"type": "SRV",
|
||||
"rdata": "10 20 30 foo-1.unit.tests.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340009",
|
||||
"domain": "unit.tests",
|
||||
"host": "aaaa",
|
||||
"ttl": "600",
|
||||
"prio": "0",
|
||||
"type": "AAAA",
|
||||
"rdata": "2601:644:500:e210:62f8:1dff:feb8:947a",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340010",
|
||||
"domain": "unit.tests",
|
||||
"host": "cname",
|
||||
"ttl": "300",
|
||||
"prio": null,
|
||||
"type": "URL",
|
||||
"rdata": "@",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340012",
|
||||
"domain": "unit.tests",
|
||||
"host": "mx",
|
||||
"ttl": "300",
|
||||
"prio": "10",
|
||||
"type": "MX",
|
||||
"rdata": "smtp-4.unit.tests.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340013",
|
||||
"domain": "unit.tests",
|
||||
"host": "mx",
|
||||
"ttl": "300",
|
||||
"prio": "20",
|
||||
"type": "MX",
|
||||
"rdata": "smtp-2.unit.tests.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340014",
|
||||
"domain": "unit.tests",
|
||||
"host": "mx",
|
||||
"ttl": "300",
|
||||
"prio": "30",
|
||||
"type": "MX",
|
||||
"rdata": "smtp-3.unit.tests.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340015",
|
||||
"domain": "unit.tests",
|
||||
"host": "mx",
|
||||
"ttl": "300",
|
||||
"prio": "40",
|
||||
"type": "MX",
|
||||
"rdata": "smtp-1.unit.tests.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340016",
|
||||
"domain": "unit.tests",
|
||||
"host": "naptr",
|
||||
"ttl": "600",
|
||||
"prio": null,
|
||||
"type": "NAPTR",
|
||||
"rdata": "100 100 'U' 'SIP+D2U' '!^.*$!sip:info@bar.example.com!' .",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340017",
|
||||
"domain": "unit.tests",
|
||||
"host": "naptr",
|
||||
"ttl": "600",
|
||||
"prio": null,
|
||||
"type": "NAPTR",
|
||||
"rdata": "10 100 'S' 'SIP+D2U' '!^.*$!sip:info@bar.example.com!' .",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340018",
|
||||
"domain": "unit.tests",
|
||||
"host": "sub",
|
||||
"ttl": "3600",
|
||||
"prio": null,
|
||||
"type": "NS",
|
||||
"rdata": "6.2.3.4.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340019",
|
||||
"domain": "unit.tests",
|
||||
"host": "sub",
|
||||
"ttl": "0",
|
||||
"prio": null,
|
||||
"type": "NS",
|
||||
"rdata": "7.2.3.4.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340020",
|
||||
"domain": "unit.tests",
|
||||
"host": "www",
|
||||
"ttl": "300",
|
||||
"prio": "0",
|
||||
"type": "A",
|
||||
"rdata": "2.2.3.6",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340021",
|
||||
"domain": "unit.tests",
|
||||
"host": "www.sub",
|
||||
"ttl": "300",
|
||||
"prio": "0",
|
||||
"type": "A",
|
||||
"rdata": "2.2.3.6",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340022",
|
||||
"domain": "unit.tests",
|
||||
"host": "included",
|
||||
"ttl": "3600",
|
||||
"prio": null,
|
||||
"type": "CNAME",
|
||||
"rdata": "unit.tests.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340011",
|
||||
"domain": "unit.tests",
|
||||
"host": "txt",
|
||||
"ttl": "600",
|
||||
"prio": "0",
|
||||
"type": "TXT",
|
||||
"rdata": "Bah bah black sheep",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340023",
|
||||
"domain": "unit.tests",
|
||||
"host": "txt",
|
||||
"ttl": "600",
|
||||
"prio": "0",
|
||||
"type": "TXT",
|
||||
"rdata": "have you any wool.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340024",
|
||||
"domain": "unit.tests",
|
||||
"host": "txt",
|
||||
"ttl": "600",
|
||||
"prio": "0",
|
||||
"type": "TXT",
|
||||
"rdata": "v=DKIM1;k=rsa;s=email;h=sha256;p=A\/kinda+of\/long\/string+with+numb3rs",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}
|
||||
],
|
||||
"count": 24,
|
||||
"total": 24,
|
||||
"start": 0,
|
||||
"max": 1000,
|
||||
"status": 200
|
||||
}
|
449
tests/test_octodns_provider_easydns.py
Normal file
449
tests/test_octodns_provider_easydns.py
Normal file
@@ -0,0 +1,449 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
import json
|
||||
from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
from octodns.provider.easydns import EasyDNSClientNotFound, \
|
||||
EasyDNSProvider
|
||||
from octodns.provider.yaml import YamlProvider
|
||||
from octodns.zone import Zone
|
||||
|
||||
|
||||
class TestEasyDNSProvider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
|
||||
def test_populate(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
|
||||
# Bad auth
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=401,
|
||||
text='{"id":"unauthorized",'
|
||||
'"message":"Unable to authenticate you."}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('Unauthorized', text_type(ctx.exception))
|
||||
|
||||
# Bad request
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=400,
|
||||
text='{"id":"invalid",'
|
||||
'"message":"Bad request"}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('Bad request', text_type(ctx.exception))
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=502, text='Things caught fire')
|
||||
|
||||
with self.assertRaises(HTTPError) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=404,
|
||||
text='{"id":"not_found","message":"The resource you '
|
||||
'were accessing could not be found."}')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
|
||||
# No diffs == no changes
|
||||
with requests_mock() as mock:
|
||||
base = 'https://rest.easydns.net/zones/records/'
|
||||
with open('tests/fixtures/easydns-records.json') as fh:
|
||||
mock.get('{}{}'.format(base, 'parsed/unit.tests'),
|
||||
text=fh.read())
|
||||
with open('tests/fixtures/easydns-records.json') as fh:
|
||||
mock.get('{}{}'.format(base, 'all/unit.tests'),
|
||||
text=fh.read())
|
||||
|
||||
provider.populate(zone)
|
||||
self.assertEquals(13, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# 2nd populate makes no network calls/all from cache
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(13, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
|
||||
def test_domain(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
|
||||
with requests_mock() as mock:
|
||||
base = 'https://rest.easydns.net/'
|
||||
mock.get('{}{}'.format(base, 'domain/unit.tests'), status_code=400,
|
||||
text='{"id":"not_found","message":"The resource you '
|
||||
'were accessing could not be found."}')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider._client.domain('unit.tests')
|
||||
|
||||
self.assertEquals('Not Found', text_type(ctx.exception))
|
||||
|
||||
def test_apply_not_found(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, 'test1', {
|
||||
"name": "test1",
|
||||
"ttl": 300,
|
||||
"type": "A",
|
||||
"value": "1.2.3.4",
|
||||
}))
|
||||
|
||||
with requests_mock() as mock:
|
||||
base = 'https://rest.easydns.net/'
|
||||
mock.get('{}{}'.format(base, 'domain/unit.tests'), status_code=404,
|
||||
text='{"id":"not_found","message":"The resource you '
|
||||
'were accessing could not be found."}')
|
||||
mock.put('{}{}'.format(base, 'domains/add/unit.tests'),
|
||||
status_code=200,
|
||||
text='{"id":"OK","message":"Zone created."}')
|
||||
mock.get('{}{}'.format(base, 'zones/records/parsed/unit.tests'),
|
||||
status_code=404,
|
||||
text='{"id":"not_found","message":"The resource you '
|
||||
'were accessing could not be found."}')
|
||||
mock.get('{}{}'.format(base, 'zones/records/all/unit.tests'),
|
||||
status_code=404,
|
||||
text='{"id":"not_found","message":"The resource you '
|
||||
'were accessing could not be found."}')
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
self.assertFalse(plan.exists)
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider.apply(plan)
|
||||
|
||||
self.assertEquals('Not Found', text_type(ctx.exception))
|
||||
|
||||
def test_domain_create(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
domain_after_creation = {
|
||||
"tm": 1000000000,
|
||||
"data": [{
|
||||
"id": "12341001",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": "0",
|
||||
"type": "SOA",
|
||||
"rdata": "dns1.easydns.com. zone.easydns.com. "
|
||||
"2020010101 3600 600 604800 0",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}, {
|
||||
"id": "12341002",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": "0",
|
||||
"type": "NS",
|
||||
"rdata": "LOCAL.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}, {
|
||||
"id": "12341003",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": "0",
|
||||
"type": "MX",
|
||||
"rdata": "LOCAL.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}],
|
||||
"count": 3,
|
||||
"total": 3,
|
||||
"start": 0,
|
||||
"max": 1000,
|
||||
"status": 200
|
||||
}
|
||||
with requests_mock() as mock:
|
||||
base = 'https://rest.easydns.net/'
|
||||
mock.put('{}{}'.format(base, 'domains/add/unit.tests'),
|
||||
status_code=201, text='{"id":"OK"}')
|
||||
mock.get('{}{}'.format(base, 'zones/records/all/unit.tests'),
|
||||
text=json.dumps(domain_after_creation))
|
||||
mock.delete(ANY, text='{"id":"OK"}')
|
||||
provider._client.domain_create('unit.tests')
|
||||
|
||||
def test_caa(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
|
||||
# Invalid rdata records
|
||||
caa_record_invalid = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "3600",
|
||||
"prio": "0",
|
||||
"type": "CAA",
|
||||
"rdata": "0",
|
||||
}]
|
||||
|
||||
# Valid rdata records
|
||||
caa_record_valid = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "3600",
|
||||
"prio": "0",
|
||||
"type": "CAA",
|
||||
"rdata": "0 issue ca.unit.tests",
|
||||
}]
|
||||
|
||||
provider._data_for_CAA('CAA', caa_record_invalid)
|
||||
provider._data_for_CAA('CAA', caa_record_valid)
|
||||
|
||||
def test_naptr(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
|
||||
# Invalid rdata records
|
||||
naptr_record_invalid = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "naptr",
|
||||
"ttl": "600",
|
||||
"prio": "10",
|
||||
"type": "NAPTR",
|
||||
"rdata": "100",
|
||||
}]
|
||||
|
||||
# Valid rdata records
|
||||
naptr_record_valid = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "naptr",
|
||||
"ttl": "600",
|
||||
"prio": "10",
|
||||
"type": "NAPTR",
|
||||
"rdata": "10 10 'U' 'SIP+D2U' '!^.*$!sip:info@bar.example.com!' .",
|
||||
}]
|
||||
|
||||
provider._data_for_NAPTR('NAPTR', naptr_record_invalid)
|
||||
provider._data_for_NAPTR('NAPTR', naptr_record_valid)
|
||||
|
||||
def test_srv(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
|
||||
# Invalid rdata records
|
||||
srv_invalid = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "_srv._tcp",
|
||||
"ttl": "600",
|
||||
"type": "SRV",
|
||||
"rdata": "",
|
||||
}]
|
||||
srv_invalid2 = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "_srv._tcp",
|
||||
"ttl": "600",
|
||||
"type": "SRV",
|
||||
"rdata": "11",
|
||||
}]
|
||||
srv_invalid3 = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "_srv._tcp",
|
||||
"ttl": "600",
|
||||
"type": "SRV",
|
||||
"rdata": "12 30",
|
||||
}]
|
||||
srv_invalid4 = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "_srv._tcp",
|
||||
"ttl": "600",
|
||||
"type": "SRV",
|
||||
"rdata": "13 40 1234",
|
||||
}]
|
||||
|
||||
# Valid rdata
|
||||
srv_valid = [{
|
||||
"domain": "unit.tests",
|
||||
"host": "_srv._tcp",
|
||||
"ttl": "600",
|
||||
"type": "SRV",
|
||||
"rdata": "100 20 5678 foo-2.unit.tests.",
|
||||
}]
|
||||
|
||||
srv_invalid_content = provider._data_for_SRV('SRV', srv_invalid)
|
||||
srv_invalid_content2 = provider._data_for_SRV('SRV', srv_invalid2)
|
||||
srv_invalid_content3 = provider._data_for_SRV('SRV', srv_invalid3)
|
||||
srv_invalid_content4 = provider._data_for_SRV('SRV', srv_invalid4)
|
||||
srv_valid_content = provider._data_for_SRV('SRV', srv_valid)
|
||||
|
||||
self.assertEqual(srv_valid_content['values'][0]['priority'], 100)
|
||||
self.assertEqual(srv_invalid_content['values'][0]['priority'], 0)
|
||||
self.assertEqual(srv_invalid_content2['values'][0]['priority'], 11)
|
||||
self.assertEqual(srv_invalid_content3['values'][0]['priority'], 12)
|
||||
self.assertEqual(srv_invalid_content4['values'][0]['priority'], 13)
|
||||
|
||||
self.assertEqual(srv_valid_content['values'][0]['weight'], 20)
|
||||
self.assertEqual(srv_invalid_content['values'][0]['weight'], 0)
|
||||
self.assertEqual(srv_invalid_content2['values'][0]['weight'], 0)
|
||||
self.assertEqual(srv_invalid_content3['values'][0]['weight'], 30)
|
||||
self.assertEqual(srv_invalid_content4['values'][0]['weight'], 40)
|
||||
|
||||
self.assertEqual(srv_valid_content['values'][0]['port'], 5678)
|
||||
self.assertEqual(srv_invalid_content['values'][0]['port'], 0)
|
||||
self.assertEqual(srv_invalid_content2['values'][0]['port'], 0)
|
||||
self.assertEqual(srv_invalid_content3['values'][0]['port'], 0)
|
||||
self.assertEqual(srv_invalid_content4['values'][0]['port'], 1234)
|
||||
|
||||
self.assertEqual(srv_valid_content['values'][0]['target'],
|
||||
'foo-2.unit.tests.')
|
||||
self.assertEqual(srv_invalid_content['values'][0]['target'], '')
|
||||
self.assertEqual(srv_invalid_content2['values'][0]['target'], '')
|
||||
self.assertEqual(srv_invalid_content3['values'][0]['target'], '')
|
||||
self.assertEqual(srv_invalid_content4['values'][0]['target'], '')
|
||||
|
||||
def test_apply(self):
|
||||
provider = EasyDNSProvider('test', 'token', 'apikey')
|
||||
|
||||
resp = Mock()
|
||||
resp.json = Mock()
|
||||
provider._client._request = Mock(return_value=resp)
|
||||
|
||||
domain_after_creation = {
|
||||
"tm": 1000000000,
|
||||
"data": [{
|
||||
"id": "12341001",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": "0",
|
||||
"type": "SOA",
|
||||
"rdata": "dns1.easydns.com. zone.easydns.com. 2020010101"
|
||||
" 3600 600 604800 0",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}, {
|
||||
"id": "12341002",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": "0",
|
||||
"type": "NS",
|
||||
"rdata": "LOCAL.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}, {
|
||||
"id": "12341003",
|
||||
"domain": "unit.tests",
|
||||
"host": "@",
|
||||
"ttl": "0",
|
||||
"prio": "0",
|
||||
"type": "MX",
|
||||
"rdata": "LOCAL.",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}],
|
||||
"count": 3,
|
||||
"total": 3,
|
||||
"start": 0,
|
||||
"max": 1000,
|
||||
"status": 200
|
||||
}
|
||||
|
||||
# non-existent domain, create everything
|
||||
resp.json.side_effect = [
|
||||
EasyDNSClientNotFound, # no zone in populate
|
||||
domain_after_creation
|
||||
]
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded, no unsupported
|
||||
n = len(self.expected.records) - 6
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
self.assertEquals(23, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
# delete 1 and update 1
|
||||
provider._client.records = Mock(return_value=[
|
||||
{
|
||||
"id": "12342001",
|
||||
"domain": "unit.tests",
|
||||
"host": "www",
|
||||
"ttl": "300",
|
||||
"prio": "0",
|
||||
"type": "A",
|
||||
"rdata": "2.2.3.9",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}, {
|
||||
"id": "12342002",
|
||||
"domain": "unit.tests",
|
||||
"host": "www",
|
||||
"ttl": "300",
|
||||
"prio": "0",
|
||||
"type": "A",
|
||||
"rdata": "2.2.3.8",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}, {
|
||||
"id": "12342003",
|
||||
"domain": "unit.tests",
|
||||
"host": "test1",
|
||||
"ttl": "3600",
|
||||
"prio": "0",
|
||||
"type": "A",
|
||||
"rdata": "1.2.3.4",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}
|
||||
])
|
||||
|
||||
# Domain exists, we don't care about return
|
||||
resp.json.side_effect = ['{}']
|
||||
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, 'test1', {
|
||||
"name": "test1",
|
||||
"ttl": 300,
|
||||
"type": "A",
|
||||
"value": "1.2.3.4",
|
||||
}))
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
self.assertTrue(plan.exists)
|
||||
self.assertEquals(2, len(plan.changes))
|
||||
self.assertEquals(2, provider.apply(plan))
|
||||
# recreate for update, and delete for the 2 parts of the other
|
||||
provider._client._request.assert_has_calls([
|
||||
call('PUT', '/zones/records/add/unit.tests/A', data={
|
||||
'rdata': '1.2.3.4',
|
||||
'name': 'test1',
|
||||
'ttl': 300,
|
||||
'type': 'A',
|
||||
'host': 'test1',
|
||||
}),
|
||||
call('DELETE', '/zones/records/unit.tests/12342001'),
|
||||
call('DELETE', '/zones/records/unit.tests/12342002'),
|
||||
call('DELETE', '/zones/records/unit.tests/12342003')
|
||||
], any_order=True)
|
Reference in New Issue
Block a user