mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
@@ -150,12 +150,12 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| Provider | Record Support | GeoDNS Support | Notes |
|
||||
|--|--|--|--|
|
||||
| [AzureProvider](/octodns/provider/azuredns.py) | A, AAAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | A, AAAA, CNAME, MX, NS, SPF, TXT | No | |
|
||||
| [DnsimpleProvider](/octodns/provider/dnsimple.py) | All | No | |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | A, AAAA, CAA, CNAME, MX, NS, SPF, TXT | No | CAA tags restricted |
|
||||
| [DnsimpleProvider](/octodns/provider/dnsimple.py) | All | No | CAA tags restricted |
|
||||
| [DynProvider](/octodns/provider/dyn.py) | All | Yes | |
|
||||
| [Ns1Provider](/octodns/provider/ns1.py) | All | No | |
|
||||
| [PowerDnsProvider](/octodns/provider/powerdns.py) | All | No | |
|
||||
| [Route53](/octodns/provider/route53.py) | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | Yes | |
|
||||
| [Route53](/octodns/provider/route53.py) | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | Yes | |
|
||||
| [TinyDNSSource](/octodns/source/tinydns.py) | A, CNAME, MX, NS, PTR | No | read-only |
|
||||
| [YamlProvider](/octodns/provider/yaml.py) | All | Yes | config |
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class CloudflareProvider(BaseProvider):
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
# TODO: support SRV
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'SPF', 'TXT'))
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SPF', 'TXT'))
|
||||
|
||||
MIN_TTL = 120
|
||||
TIMEOUT = 15
|
||||
@@ -104,6 +104,20 @@ class CloudflareProvider(BaseProvider):
|
||||
'values': [r['content'].replace(';', '\;') for r in records],
|
||||
}
|
||||
|
||||
def _data_for_CAA(self, _type, records):
|
||||
values = []
|
||||
for r in records:
|
||||
values.append({
|
||||
'flags': r['flags'],
|
||||
'tag': r['tag'],
|
||||
'value': r['content'],
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
only = records[0]
|
||||
return {
|
||||
@@ -197,6 +211,14 @@ class CloudflareProvider(BaseProvider):
|
||||
_contents_for_NS = _contents_for_multiple
|
||||
_contents_for_SPF = _contents_for_multiple
|
||||
|
||||
def _contents_for_CAA(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'flags': value.flags,
|
||||
'tag': value.tag,
|
||||
'value': value.value,
|
||||
}
|
||||
|
||||
def _contents_for_TXT(self, record):
|
||||
for value in record.values:
|
||||
yield {'content': value.replace('\;', ';')}
|
||||
|
||||
@@ -91,8 +91,8 @@ class DnsimpleProvider(BaseProvider):
|
||||
account: 42
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR',
|
||||
'SPF', 'SRV', 'SSHFP', 'TXT'))
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
||||
'PTR', 'SPF', 'SRV', 'SSHFP', 'TXT'))
|
||||
|
||||
def __init__(self, id, token, account, *args, **kwargs):
|
||||
self.log = logging.getLogger('DnsimpleProvider[{}]'.format(id))
|
||||
@@ -114,6 +114,21 @@ class DnsimpleProvider(BaseProvider):
|
||||
_data_for_SPF = _data_for_multiple
|
||||
_data_for_TXT = _data_for_multiple
|
||||
|
||||
def _data_for_CAA(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
flags, tag, value = record['content'].split(' ')
|
||||
values.append({
|
||||
'flags': flags,
|
||||
'tag': tag,
|
||||
'value': value[1:-1],
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
record = records[0]
|
||||
return {
|
||||
@@ -275,6 +290,16 @@ class DnsimpleProvider(BaseProvider):
|
||||
_params_for_SPF = _params_for_multiple
|
||||
_params_for_TXT = _params_for_multiple
|
||||
|
||||
def _params_for_CAA(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'content': '{} {} "{}"'.format(value.flags, value.tag,
|
||||
value.value),
|
||||
'name': record.name,
|
||||
'ttl': record.ttl,
|
||||
'type': record._type
|
||||
}
|
||||
|
||||
def _params_for_single(self, record):
|
||||
yield {
|
||||
'content': record.value,
|
||||
|
||||
@@ -111,6 +111,7 @@ class DynProvider(BaseProvider):
|
||||
'a_records': 'A',
|
||||
'aaaa_records': 'AAAA',
|
||||
'alias_records': 'ALIAS',
|
||||
'caa_records': 'CAA',
|
||||
'cname_records': 'CNAME',
|
||||
'mx_records': 'MX',
|
||||
'naptr_records': 'NAPTR',
|
||||
@@ -194,6 +195,14 @@ class DynProvider(BaseProvider):
|
||||
'value': record.alias
|
||||
}
|
||||
|
||||
def _data_for_CAA(self, _type, records):
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records[0].ttl,
|
||||
'values': [{'flags': r.flags, 'tag': r.tag, 'value': r.value}
|
||||
for r in records],
|
||||
}
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
record = records[0]
|
||||
return {
|
||||
@@ -382,6 +391,13 @@ class DynProvider(BaseProvider):
|
||||
|
||||
_kwargs_for_AAAA = _kwargs_for_A
|
||||
|
||||
def _kwargs_for_CAA(self, record):
|
||||
return [{
|
||||
'flags': v.flags,
|
||||
'tag': v.tag,
|
||||
'value': v.value,
|
||||
} for v in record.values]
|
||||
|
||||
def _kwargs_for_CNAME(self, record):
|
||||
return [{
|
||||
'cname': record.value,
|
||||
|
||||
+21
-2
@@ -23,8 +23,8 @@ class Ns1Provider(BaseProvider):
|
||||
api_key: env/NS1_API_KEY
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR',
|
||||
'SPF', 'SRV', 'TXT'))
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
||||
'PTR', 'SPF', 'SRV', 'TXT'))
|
||||
|
||||
ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found'
|
||||
|
||||
@@ -53,6 +53,21 @@ class Ns1Provider(BaseProvider):
|
||||
|
||||
_data_for_TXT = _data_for_SPF
|
||||
|
||||
def _data_for_CAA(self, _type, record):
|
||||
values = []
|
||||
for answer in record['short_answers']:
|
||||
flags, tag, value = answer.split(' ', 2)
|
||||
values.append({
|
||||
'flags': flags,
|
||||
'tag': tag,
|
||||
'value': value,
|
||||
})
|
||||
return {
|
||||
'ttl': record['ttl'],
|
||||
'type': _type,
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def _data_for_CNAME(self, _type, record):
|
||||
return {
|
||||
'ttl': record['ttl'],
|
||||
@@ -159,6 +174,10 @@ class Ns1Provider(BaseProvider):
|
||||
|
||||
_params_for_TXT = _params_for_SPF
|
||||
|
||||
def _params_for_CAA(self, record):
|
||||
values = [(v.flags, v.tag, v.value) for v in record.values]
|
||||
return {'answers': values, 'ttl': record.ttl}
|
||||
|
||||
def _params_for_CNAME(self, record):
|
||||
return {'answers': [record.value], 'ttl': record.ttl}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ from .base import BaseProvider
|
||||
|
||||
class PowerDnsBaseProvider(BaseProvider):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR',
|
||||
'SPF', 'SSHFP', 'SRV', 'TXT'))
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
||||
'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
|
||||
TIMEOUT = 5
|
||||
|
||||
def __init__(self, id, host, api_key, port=8081, scheme="http", *args,
|
||||
@@ -61,6 +61,21 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
_data_for_AAAA = _data_for_multiple
|
||||
_data_for_NS = _data_for_multiple
|
||||
|
||||
def _data_for_CAA(self, rrset):
|
||||
values = []
|
||||
for record in rrset['records']:
|
||||
flags, tag, value = record['content'].split(' ', 2)
|
||||
values.append({
|
||||
'flags': flags,
|
||||
'tag': tag,
|
||||
'value': value[1:-1],
|
||||
})
|
||||
return {
|
||||
'type': rrset['type'],
|
||||
'values': values,
|
||||
'ttl': rrset['ttl']
|
||||
}
|
||||
|
||||
def _data_for_single(self, rrset):
|
||||
return {
|
||||
'type': rrset['type'],
|
||||
@@ -194,6 +209,12 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
_records_for_AAAA = _records_for_multiple
|
||||
_records_for_NS = _records_for_multiple
|
||||
|
||||
def _records_for_CAA(self, record):
|
||||
return [{
|
||||
'content': '{} {} "{}"'.format(v.flags, v.tag, v.value),
|
||||
'disabled': False
|
||||
} for v in record.values]
|
||||
|
||||
def _records_for_single(self, record):
|
||||
return [{'content': record.value, 'disabled': False}]
|
||||
|
||||
|
||||
@@ -90,6 +90,10 @@ class _Route53Record(object):
|
||||
_values_for_AAAA = _values_for_values
|
||||
_values_for_NS = _values_for_values
|
||||
|
||||
def _values_for_CAA(self, record):
|
||||
return ['{} {} "{}"'.format(v.flags, v.tag, v.value)
|
||||
for v in record.values]
|
||||
|
||||
def _values_for_value(self, record):
|
||||
return [record.value]
|
||||
|
||||
@@ -222,8 +226,8 @@ class Route53Provider(BaseProvider):
|
||||
In general the account used will need full permissions on Route53.
|
||||
'''
|
||||
SUPPORTS_GEO = True
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SPF',
|
||||
'SRV', 'TXT'))
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR',
|
||||
'SPF', 'SRV', 'TXT'))
|
||||
|
||||
# This should be bumped when there are underlying changes made to the
|
||||
# health check config.
|
||||
@@ -319,6 +323,21 @@ class Route53Provider(BaseProvider):
|
||||
_data_for_A = _data_for_geo
|
||||
_data_for_AAAA = _data_for_geo
|
||||
|
||||
def _data_for_CAA(self, rrset):
|
||||
values = []
|
||||
for rr in rrset['ResourceRecords']:
|
||||
flags, tag, value = rr['Value'].split(' ')
|
||||
values.append({
|
||||
'flags': flags,
|
||||
'tag': tag,
|
||||
'value': value[1:-1],
|
||||
})
|
||||
return {
|
||||
'type': rrset['Type'],
|
||||
'values': values,
|
||||
'ttl': int(rrset['TTL'])
|
||||
}
|
||||
|
||||
def _data_for_single(self, rrset):
|
||||
return {
|
||||
'type': rrset['Type'],
|
||||
|
||||
+56
-14
@@ -81,29 +81,16 @@ class Record(object):
|
||||
'A': ARecord,
|
||||
'AAAA': AaaaRecord,
|
||||
'ALIAS': AliasRecord,
|
||||
# cert
|
||||
'CAA': CaaRecord,
|
||||
'CNAME': CnameRecord,
|
||||
# dhcid
|
||||
# dname
|
||||
# dnskey
|
||||
# ds
|
||||
# ipseckey
|
||||
# key
|
||||
# kx
|
||||
# loc
|
||||
'MX': MxRecord,
|
||||
'NAPTR': NaptrRecord,
|
||||
'NS': NsRecord,
|
||||
# nsap
|
||||
'PTR': PtrRecord,
|
||||
# px
|
||||
# rp
|
||||
# soa - would it even make sense?
|
||||
'SPF': SpfRecord,
|
||||
'SRV': SrvRecord,
|
||||
'SSHFP': SshfpRecord,
|
||||
'TXT': TxtRecord,
|
||||
# url
|
||||
}[_type]
|
||||
except KeyError:
|
||||
raise Exception('Unknown record type: "{}"'.format(_type))
|
||||
@@ -398,6 +385,61 @@ class AliasRecord(_ValueMixin, Record):
|
||||
return value
|
||||
|
||||
|
||||
class CaaValue(object):
|
||||
# https://tools.ietf.org/html/rfc6844#page-5
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
reasons = []
|
||||
try:
|
||||
flags = int(value.get('flags', 0))
|
||||
if flags < 0 or flags > 255:
|
||||
reasons.append('invalid flags "{}"'.format(flags))
|
||||
except ValueError:
|
||||
reasons.append('invalid flags "{}"'.format(value['flags']))
|
||||
|
||||
if 'tag' not in value:
|
||||
reasons.append('missing tag')
|
||||
if 'value' not in value:
|
||||
reasons.append('missing value')
|
||||
|
||||
return reasons
|
||||
|
||||
def __init__(self, value):
|
||||
self.flags = int(value.get('flags', 0))
|
||||
self.tag = value['tag']
|
||||
self.value = value['value']
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return {
|
||||
'flags': self.flags,
|
||||
'tag': self.tag,
|
||||
'value': self.value,
|
||||
}
|
||||
|
||||
def __cmp__(self, other):
|
||||
if self.flags == other.flags:
|
||||
if self.tag == other.tag:
|
||||
return cmp(self.value, other.value)
|
||||
return cmp(self.tag, other.tag)
|
||||
return cmp(self.flags, other.flags)
|
||||
|
||||
def __repr__(self):
|
||||
return '{} {} "{}"'.format(self.flags, self.tag, self.value)
|
||||
|
||||
|
||||
class CaaRecord(_ValuesMixin, Record):
|
||||
_type = 'CAA'
|
||||
|
||||
@classmethod
|
||||
def _validate_value(cls, value):
|
||||
return CaaValue._validate_value(value)
|
||||
|
||||
def _process_values(self, values):
|
||||
return [CaaValue(v) for v in values]
|
||||
|
||||
|
||||
class CnameRecord(_ValueMixin, Record):
|
||||
_type = 'CNAME'
|
||||
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ PyYaml==3.12
|
||||
azure-mgmt-dns==1.0.1
|
||||
azure-common==1.1.6
|
||||
boto3==1.4.6
|
||||
botocore==1.6.0
|
||||
botocore==1.6.8
|
||||
dnspython==1.15.0
|
||||
docutils==0.14
|
||||
dyn==1.7.10
|
||||
dyn==1.8.0
|
||||
futures==3.1.1
|
||||
incf.countryutils==1.0
|
||||
ipaddress==1.0.18
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
values:
|
||||
- 6.2.3.4.
|
||||
- 7.2.3.4.
|
||||
- type: CAA
|
||||
values:
|
||||
- flags: 0
|
||||
tag: issue
|
||||
value: ca.unit.tests
|
||||
_srv._tcp:
|
||||
ttl: 600
|
||||
type: SRV
|
||||
|
||||
+21
-2
@@ -118,14 +118,33 @@
|
||||
"meta": {
|
||||
"auto_added": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc223b34cd5611334422ab3322997667",
|
||||
"type": "CAA",
|
||||
"name": "unit.tests",
|
||||
"content": "ca.unit.tests",
|
||||
"flags": 0,
|
||||
"tag": "issue",
|
||||
"proxiable": false,
|
||||
"proxied": false,
|
||||
"ttl": 3600,
|
||||
"locked": false,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:42.961566Z",
|
||||
"created_on": "2017-03-11T18:01:42.961566Z",
|
||||
"meta": {
|
||||
"auto_added": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"result_info": {
|
||||
"page": 2,
|
||||
"per_page": 10,
|
||||
"total_pages": 2,
|
||||
"count": 7,
|
||||
"total_count": 17
|
||||
"count": 8,
|
||||
"total_count": 19
|
||||
},
|
||||
"success": true,
|
||||
"errors": [],
|
||||
|
||||
Vendored
+17
-1
@@ -159,12 +159,28 @@
|
||||
"system_record": false,
|
||||
"created_at": "2017-03-09T15:55:09Z",
|
||||
"updated_at": "2017-03-09T15:55:09Z"
|
||||
},
|
||||
{
|
||||
"id": 12188803,
|
||||
"zone_id": "unit.tests",
|
||||
"parent_id": null,
|
||||
"name": "",
|
||||
"content": "0 issue \"ca.unit.tests\"",
|
||||
"ttl": 3600,
|
||||
"priority": null,
|
||||
"type": "CAA",
|
||||
"regions": [
|
||||
"global"
|
||||
],
|
||||
"system_record": false,
|
||||
"created_at": "2017-03-09T15:55:09Z",
|
||||
"updated_at": "2017-03-09T15:55:09Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 2,
|
||||
"per_page": 20,
|
||||
"total_entries": 29,
|
||||
"total_entries": 30,
|
||||
"total_pages": 2
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -230,6 +230,18 @@
|
||||
],
|
||||
"ttl": 300,
|
||||
"type": "A"
|
||||
},
|
||||
{
|
||||
"comments": [],
|
||||
"name": "unit.tests.",
|
||||
"records": [
|
||||
{
|
||||
"content": "0 issue \"ca.unit.tests\"",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"ttl": 3600,
|
||||
"type": "CAA"
|
||||
}
|
||||
],
|
||||
"serial": 2017012803,
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestCloudflareProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(9, len(zone.records))
|
||||
self.assertEquals(10, len(zone.records))
|
||||
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
@@ -126,7 +126,7 @@ class TestCloudflareProvider(TestCase):
|
||||
# re-populating the same zone/records comes out of cache, no calls
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(9, len(again.records))
|
||||
self.assertEquals(10, len(again.records))
|
||||
|
||||
def test_apply(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
@@ -140,12 +140,12 @@ class TestCloudflareProvider(TestCase):
|
||||
'id': 42,
|
||||
}
|
||||
}, # zone create
|
||||
] + [None] * 16 # individual record creates
|
||||
] + [None] * 17 # individual record creates
|
||||
|
||||
# non-existant zone, create everything
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(9, len(plan.changes))
|
||||
self.assertEquals(9, provider.apply(plan))
|
||||
self.assertEquals(10, len(plan.changes))
|
||||
self.assertEquals(10, provider.apply(plan))
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
# created the domain
|
||||
@@ -170,7 +170,7 @@ class TestCloudflareProvider(TestCase):
|
||||
}),
|
||||
], True)
|
||||
# expected number of total calls
|
||||
self.assertEquals(18, provider._request.call_count)
|
||||
self.assertEquals(19, provider._request.call_count)
|
||||
|
||||
provider._request.reset_mock()
|
||||
|
||||
|
||||
@@ -78,14 +78,14 @@ class TestDnsimpleProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(14, len(zone.records))
|
||||
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(14, len(again.records))
|
||||
self.assertEquals(15, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
@@ -147,7 +147,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
}),
|
||||
])
|
||||
# expected number of total calls
|
||||
self.assertEquals(26, provider._client._request.call_count)
|
||||
self.assertEquals(27, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
|
||||
@@ -109,6 +109,14 @@ class TestDynProvider(TestCase):
|
||||
'weight': 22,
|
||||
'port': 20,
|
||||
'target': 'foo-2.unit.tests.'
|
||||
}]}),
|
||||
('', {
|
||||
'type': 'CAA',
|
||||
'ttl': 308,
|
||||
'values': [{
|
||||
'flags': 0,
|
||||
'tag': 'issue',
|
||||
'value': 'ca.unit.tests'
|
||||
}]})):
|
||||
expected.add_record(Record.new(expected, name, data))
|
||||
|
||||
@@ -321,6 +329,16 @@ class TestDynProvider(TestCase):
|
||||
'ttl': 307,
|
||||
'zone': 'unit.tests',
|
||||
}],
|
||||
'caa_records': [{
|
||||
'fqdn': 'unit.tests',
|
||||
'rdata': {'flags': 0,
|
||||
'tag': 'issue',
|
||||
'value': 'ca.unit.tests'},
|
||||
'record_id': 12,
|
||||
'record_type': 'cAA',
|
||||
'ttl': 308,
|
||||
'zone': 'unit.tests',
|
||||
}],
|
||||
}}
|
||||
]
|
||||
got = Zone('unit.tests.', [])
|
||||
@@ -414,10 +432,10 @@ class TestDynProvider(TestCase):
|
||||
update_mock.assert_called()
|
||||
add_mock.assert_called()
|
||||
# Once for each dyn record (8 Records, 2 of which have dual values)
|
||||
self.assertEquals(14, len(add_mock.call_args_list))
|
||||
self.assertEquals(15, len(add_mock.call_args_list))
|
||||
execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}),
|
||||
call('/Zone/unit.tests/', 'GET', {})])
|
||||
self.assertEquals(9, len(plan.changes))
|
||||
self.assertEquals(10, len(plan.changes))
|
||||
|
||||
execute_mock.reset_mock()
|
||||
|
||||
|
||||
@@ -96,6 +96,15 @@ class TestNs1Provider(TestCase):
|
||||
'type': 'NS',
|
||||
'values': ['ns3.unit.tests.', 'ns4.unit.tests.'],
|
||||
}))
|
||||
expected.add(Record.new(zone, '', {
|
||||
'ttl': 40,
|
||||
'type': 'CAA',
|
||||
'value': {
|
||||
'flags': 0,
|
||||
'tag': 'issue',
|
||||
'value': 'ca.unit.tests',
|
||||
},
|
||||
}))
|
||||
|
||||
nsone_records = [{
|
||||
'type': 'A',
|
||||
@@ -141,6 +150,11 @@ class TestNs1Provider(TestCase):
|
||||
'ttl': 39,
|
||||
'short_answers': ['ns3.unit.tests.', 'ns4.unit.tests.'],
|
||||
'domain': 'sub.unit.tests.',
|
||||
}, {
|
||||
'type': 'CAA',
|
||||
'ttl': 40,
|
||||
'short_answers': ['0 issue ca.unit.tests'],
|
||||
'domain': 'unit.tests.',
|
||||
}]
|
||||
|
||||
@patch('nsone.NSONE.loadZone')
|
||||
|
||||
@@ -79,7 +79,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
expected_n = len(expected.records) - 1
|
||||
self.assertEquals(14, expected_n)
|
||||
self.assertEquals(15, expected_n)
|
||||
|
||||
# No diffs == no changes
|
||||
with requests_mock() as mock:
|
||||
@@ -87,7 +87,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(14, len(zone.records))
|
||||
self.assertEquals(15, len(zone.records))
|
||||
changes = expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
@@ -167,7 +167,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
self.assertEquals(15, len(expected.records))
|
||||
self.assertEquals(16, len(expected.records))
|
||||
|
||||
# A small change to a single record
|
||||
with requests_mock() as mock:
|
||||
|
||||
@@ -77,6 +77,12 @@ class TestRoute53Provider(TestCase):
|
||||
{'ttl': 67, 'type': 'NS', 'values': ['8.2.3.4.', '9.2.3.4.']}),
|
||||
('sub',
|
||||
{'ttl': 68, 'type': 'NS', 'values': ['5.2.3.4.', '6.2.3.4.']}),
|
||||
('',
|
||||
{'ttl': 69, 'type': 'CAA', 'value': {
|
||||
'flags': 0,
|
||||
'tag': 'issue',
|
||||
'value': 'ca.unit.tests'
|
||||
}}),
|
||||
):
|
||||
record = Record.new(expected, name, data)
|
||||
expected.add_record(record)
|
||||
@@ -300,6 +306,13 @@ class TestRoute53Provider(TestCase):
|
||||
'Value': 'ns1.unit.tests.',
|
||||
}],
|
||||
'TTL': 69,
|
||||
}, {
|
||||
'Name': 'unit.tests.',
|
||||
'Type': 'CAA',
|
||||
'ResourceRecords': [{
|
||||
'Value': '0 issue "ca.unit.tests"',
|
||||
}],
|
||||
'TTL': 69,
|
||||
}],
|
||||
'IsTruncated': False,
|
||||
'MaxItems': '100',
|
||||
@@ -347,7 +360,7 @@ class TestRoute53Provider(TestCase):
|
||||
{'HostedZoneId': 'z42'})
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(8, len(plan.changes))
|
||||
self.assertEquals(9, len(plan.changes))
|
||||
for change in plan.changes:
|
||||
self.assertIsInstance(change, Create)
|
||||
stubber.assert_no_pending_responses()
|
||||
@@ -366,7 +379,7 @@ class TestRoute53Provider(TestCase):
|
||||
'SubmittedAt': '2017-01-29T01:02:03Z',
|
||||
}}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY})
|
||||
|
||||
self.assertEquals(8, provider.apply(plan))
|
||||
self.assertEquals(9, provider.apply(plan))
|
||||
stubber.assert_no_pending_responses()
|
||||
|
||||
# Delete by monkey patching in a populate that includes an extra record
|
||||
@@ -579,7 +592,7 @@ class TestRoute53Provider(TestCase):
|
||||
{})
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(8, len(plan.changes))
|
||||
self.assertEquals(9, len(plan.changes))
|
||||
for change in plan.changes:
|
||||
self.assertIsInstance(change, Create)
|
||||
stubber.assert_no_pending_responses()
|
||||
@@ -626,7 +639,7 @@ class TestRoute53Provider(TestCase):
|
||||
'SubmittedAt': '2017-01-29T01:02:03Z',
|
||||
}}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY})
|
||||
|
||||
self.assertEquals(8, provider.apply(plan))
|
||||
self.assertEquals(9, provider.apply(plan))
|
||||
stubber.assert_no_pending_responses()
|
||||
|
||||
def test_health_checks_pagination(self):
|
||||
@@ -1174,16 +1187,16 @@ class TestRoute53Provider(TestCase):
|
||||
@patch('octodns.provider.route53.Route53Provider._really_apply')
|
||||
def test_apply_1(self, really_apply_mock):
|
||||
|
||||
# 17 RRs with max of 18 should only get applied in one call
|
||||
provider, plan = self._get_test_plan(18)
|
||||
# 18 RRs with max of 19 should only get applied in one call
|
||||
provider, plan = self._get_test_plan(19)
|
||||
provider.apply(plan)
|
||||
really_apply_mock.assert_called_once()
|
||||
|
||||
@patch('octodns.provider.route53.Route53Provider._really_apply')
|
||||
def test_apply_2(self, really_apply_mock):
|
||||
|
||||
# 17 RRs with max of 17 should only get applied in two calls
|
||||
provider, plan = self._get_test_plan(17)
|
||||
# 18 RRs with max of 17 should only get applied in two calls
|
||||
provider, plan = self._get_test_plan(18)
|
||||
provider.apply(plan)
|
||||
self.assertEquals(2, really_apply_mock.call_count)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# without it we see everything
|
||||
source.populate(zone)
|
||||
self.assertEquals(15, len(zone.records))
|
||||
self.assertEquals(16, len(zone.records))
|
||||
|
||||
# Assumption here is that a clean round-trip means that everything
|
||||
# worked as expected, data that went in came back out and could be
|
||||
|
||||
@@ -7,10 +7,10 @@ from __future__ import absolute_import, division, print_function, \
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import ARecord, AaaaRecord, AliasRecord, CnameRecord, \
|
||||
Create, Delete, GeoValue, MxRecord, NaptrRecord, NaptrValue, NsRecord, \
|
||||
Record, SshfpRecord, SpfRecord, SrvRecord, TxtRecord, Update, \
|
||||
ValidationError
|
||||
from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \
|
||||
CnameRecord, Create, Delete, GeoValue, MxRecord, NaptrRecord, \
|
||||
NaptrValue, NsRecord, Record, SshfpRecord, SpfRecord, SrvRecord, \
|
||||
TxtRecord, Update, ValidationError
|
||||
from octodns.zone import Zone
|
||||
|
||||
from helpers import GeoProvider, SimpleProvider
|
||||
@@ -206,6 +206,66 @@ class TestRecord(TestCase):
|
||||
# __repr__ doesn't blow up
|
||||
a.__repr__()
|
||||
|
||||
def test_caa(self):
|
||||
a_values = [{
|
||||
'flags': 0,
|
||||
'tag': 'issue',
|
||||
'value': 'ca.example.net',
|
||||
}, {
|
||||
'flags': 128,
|
||||
'tag': 'iodef',
|
||||
'value': 'mailto:security@example.com',
|
||||
}]
|
||||
a_data = {'ttl': 30, 'values': a_values}
|
||||
a = CaaRecord(self.zone, 'a', a_data)
|
||||
self.assertEquals('a', a.name)
|
||||
self.assertEquals('a.unit.tests.', a.fqdn)
|
||||
self.assertEquals(30, a.ttl)
|
||||
self.assertEquals(a_values[0]['flags'], a.values[0].flags)
|
||||
self.assertEquals(a_values[0]['tag'], a.values[0].tag)
|
||||
self.assertEquals(a_values[0]['value'], a.values[0].value)
|
||||
self.assertEquals(a_values[1]['flags'], a.values[1].flags)
|
||||
self.assertEquals(a_values[1]['tag'], a.values[1].tag)
|
||||
self.assertEquals(a_values[1]['value'], a.values[1].value)
|
||||
self.assertEquals(a_data, a.data)
|
||||
|
||||
b_value = {
|
||||
'tag': 'iodef',
|
||||
'value': 'http://iodef.example.com/',
|
||||
}
|
||||
b_data = {'ttl': 30, 'value': b_value}
|
||||
b = CaaRecord(self.zone, 'b', b_data)
|
||||
self.assertEquals(0, b.values[0].flags)
|
||||
self.assertEquals(b_value['tag'], b.values[0].tag)
|
||||
self.assertEquals(b_value['value'], b.values[0].value)
|
||||
b_data['value']['flags'] = 0
|
||||
self.assertEquals(b_data, b.data)
|
||||
|
||||
target = SimpleProvider()
|
||||
# No changes with self
|
||||
self.assertFalse(a.changes(a, target))
|
||||
# Diff in flags causes change
|
||||
other = CaaRecord(self.zone, 'a', {'ttl': 30, 'values': a_values})
|
||||
other.values[0].flags = 128
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
# Diff in tag causes change
|
||||
other.values[0].flags = a.values[0].flags
|
||||
other.values[0].tag = 'foo'
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
# Diff in value causes change
|
||||
other.values[0].tag = a.values[0].tag
|
||||
other.values[0].value = 'bar'
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
|
||||
# __repr__ doesn't blow up
|
||||
a.__repr__()
|
||||
|
||||
def test_cname(self):
|
||||
self.assertSingleValue(CnameRecord, 'target.foo.com.',
|
||||
'other.foo.com.')
|
||||
@@ -861,6 +921,75 @@ class TestRecordValidation(TestCase):
|
||||
})
|
||||
self.assertEquals(['missing trailing .'], ctx.exception.reasons)
|
||||
|
||||
def test_CAA(self):
|
||||
# doesn't blow up
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'CAA',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'flags': 128,
|
||||
'tag': 'iodef',
|
||||
'value': 'http://foo.bar.com/'
|
||||
}
|
||||
})
|
||||
|
||||
# invalid flags
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'CAA',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'flags': -42,
|
||||
'tag': 'iodef',
|
||||
'value': 'http://foo.bar.com/',
|
||||
}
|
||||
})
|
||||
self.assertEquals(['invalid flags "-42"'], ctx.exception.reasons)
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'CAA',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'flags': 442,
|
||||
'tag': 'iodef',
|
||||
'value': 'http://foo.bar.com/',
|
||||
}
|
||||
})
|
||||
self.assertEquals(['invalid flags "442"'], ctx.exception.reasons)
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'CAA',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'flags': 'nope',
|
||||
'tag': 'iodef',
|
||||
'value': 'http://foo.bar.com/',
|
||||
}
|
||||
})
|
||||
self.assertEquals(['invalid flags "nope"'], ctx.exception.reasons)
|
||||
|
||||
# missing tag
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'CAA',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'value': 'http://foo.bar.com/',
|
||||
}
|
||||
})
|
||||
self.assertEquals(['missing tag'], ctx.exception.reasons)
|
||||
|
||||
# missing value
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'CAA',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'tag': 'iodef',
|
||||
}
|
||||
})
|
||||
self.assertEquals(['missing value'], ctx.exception.reasons)
|
||||
|
||||
def test_CNAME(self):
|
||||
# doesn't blow up
|
||||
Record.new(self.zone, 'www', {
|
||||
|
||||
Reference in New Issue
Block a user