Browse Source

Merge pull request #103 from github/caa-support

CAA support
pull/105/head
Ross McFarland 8 years ago
committed by GitHub
parent
commit
a0975e4f29
21 changed files with 448 additions and 58 deletions
  1. +3
    -3
      README.md
  2. +23
    -1
      octodns/provider/cloudflare.py
  3. +27
    -2
      octodns/provider/dnsimple.py
  4. +16
    -0
      octodns/provider/dyn.py
  5. +21
    -2
      octodns/provider/ns1.py
  6. +23
    -2
      octodns/provider/powerdns.py
  7. +21
    -2
      octodns/provider/route53.py
  8. +56
    -14
      octodns/record.py
  9. +2
    -2
      requirements.txt
  10. +5
    -0
      tests/config/unit.tests.yaml
  11. +21
    -2
      tests/fixtures/cloudflare-dns_records-page-2.json
  12. +17
    -1
      tests/fixtures/dnsimple-page-2.json
  13. +12
    -0
      tests/fixtures/powerdns-full-data.json
  14. +6
    -6
      tests/test_octodns_provider_cloudflare.py
  15. +3
    -3
      tests/test_octodns_provider_dnsimple.py
  16. +20
    -2
      tests/test_octodns_provider_dyn.py
  17. +14
    -0
      tests/test_octodns_provider_ns1.py
  18. +3
    -3
      tests/test_octodns_provider_powerdns.py
  19. +21
    -8
      tests/test_octodns_provider_route53.py
  20. +1
    -1
      tests/test_octodns_provider_yaml.py
  21. +133
    -4
      tests/test_octodns_record.py

+ 3
- 3
README.md View File

@ -150,12 +150,12 @@ The above command pulled the existing data out of Route53 and placed the results
| Provider | Record Support | GeoDNS Support | Notes | | Provider | Record Support | GeoDNS Support | Notes |
|--|--|--|--| |--|--|--|--|
| [AzureProvider](/octodns/provider/azuredns.py) | A, AAAA, CNAME, MX, NS, PTR, SRV, TXT | No | | | [AzureProvider](/octodns/provider/azuredns.py) | A, AAAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
| [CloudflareProvider](/octodns/provider/cloudflare.py) | A, AAAA, 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 | | | [DynProvider](/octodns/provider/dyn.py) | All | Yes | |
| [Ns1Provider](/octodns/provider/ns1.py) | All | No | | | [Ns1Provider](/octodns/provider/ns1.py) | All | No | |
| [PowerDnsProvider](/octodns/provider/powerdns.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 | | [TinyDNSSource](/octodns/source/tinydns.py) | A, CNAME, MX, NS, PTR | No | read-only |
| [YamlProvider](/octodns/provider/yaml.py) | All | Yes | config | | [YamlProvider](/octodns/provider/yaml.py) | All | Yes | config |


+ 23
- 1
octodns/provider/cloudflare.py View File

@ -36,7 +36,7 @@ class CloudflareProvider(BaseProvider):
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
# TODO: support SRV # 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 MIN_TTL = 120
TIMEOUT = 15 TIMEOUT = 15
@ -104,6 +104,20 @@ class CloudflareProvider(BaseProvider):
'values': [r['content'].replace(';', '\;') for r in records], '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): def _data_for_CNAME(self, _type, records):
only = records[0] only = records[0]
return { return {
@ -197,6 +211,14 @@ class CloudflareProvider(BaseProvider):
_contents_for_NS = _contents_for_multiple _contents_for_NS = _contents_for_multiple
_contents_for_SPF = _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): def _contents_for_TXT(self, record):
for value in record.values: for value in record.values:
yield {'content': value.replace('\;', ';')} yield {'content': value.replace('\;', ';')}


+ 27
- 2
octodns/provider/dnsimple.py View File

@ -91,8 +91,8 @@ class DnsimpleProvider(BaseProvider):
account: 42 account: 42
''' '''
SUPPORTS_GEO = False 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): def __init__(self, id, token, account, *args, **kwargs):
self.log = logging.getLogger('DnsimpleProvider[{}]'.format(id)) self.log = logging.getLogger('DnsimpleProvider[{}]'.format(id))
@ -114,6 +114,21 @@ class DnsimpleProvider(BaseProvider):
_data_for_SPF = _data_for_multiple _data_for_SPF = _data_for_multiple
_data_for_TXT = _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): def _data_for_CNAME(self, _type, records):
record = records[0] record = records[0]
return { return {
@ -275,6 +290,16 @@ class DnsimpleProvider(BaseProvider):
_params_for_SPF = _params_for_multiple _params_for_SPF = _params_for_multiple
_params_for_TXT = _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): def _params_for_single(self, record):
yield { yield {
'content': record.value, 'content': record.value,


+ 16
- 0
octodns/provider/dyn.py View File

@ -111,6 +111,7 @@ class DynProvider(BaseProvider):
'a_records': 'A', 'a_records': 'A',
'aaaa_records': 'AAAA', 'aaaa_records': 'AAAA',
'alias_records': 'ALIAS', 'alias_records': 'ALIAS',
'caa_records': 'CAA',
'cname_records': 'CNAME', 'cname_records': 'CNAME',
'mx_records': 'MX', 'mx_records': 'MX',
'naptr_records': 'NAPTR', 'naptr_records': 'NAPTR',
@ -194,6 +195,14 @@ class DynProvider(BaseProvider):
'value': record.alias '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): def _data_for_CNAME(self, _type, records):
record = records[0] record = records[0]
return { return {
@ -382,6 +391,13 @@ class DynProvider(BaseProvider):
_kwargs_for_AAAA = _kwargs_for_A _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): def _kwargs_for_CNAME(self, record):
return [{ return [{
'cname': record.value, 'cname': record.value,


+ 21
- 2
octodns/provider/ns1.py View File

@ -23,8 +23,8 @@ class Ns1Provider(BaseProvider):
api_key: env/NS1_API_KEY api_key: env/NS1_API_KEY
''' '''
SUPPORTS_GEO = False 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' ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found'
@ -53,6 +53,21 @@ class Ns1Provider(BaseProvider):
_data_for_TXT = _data_for_SPF _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): def _data_for_CNAME(self, _type, record):
return { return {
'ttl': record['ttl'], 'ttl': record['ttl'],
@ -159,6 +174,10 @@ class Ns1Provider(BaseProvider):
_params_for_TXT = _params_for_SPF _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): def _params_for_CNAME(self, record):
return {'answers': [record.value], 'ttl': record.ttl} return {'answers': [record.value], 'ttl': record.ttl}


+ 23
- 2
octodns/provider/powerdns.py View File

@ -14,8 +14,8 @@ from .base import BaseProvider
class PowerDnsBaseProvider(BaseProvider): class PowerDnsBaseProvider(BaseProvider):
SUPPORTS_GEO = False 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 TIMEOUT = 5
def __init__(self, id, host, api_key, port=8081, scheme="http", *args, 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_AAAA = _data_for_multiple
_data_for_NS = _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): def _data_for_single(self, rrset):
return { return {
'type': rrset['type'], 'type': rrset['type'],
@ -194,6 +209,12 @@ class PowerDnsBaseProvider(BaseProvider):
_records_for_AAAA = _records_for_multiple _records_for_AAAA = _records_for_multiple
_records_for_NS = _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): def _records_for_single(self, record):
return [{'content': record.value, 'disabled': False}] return [{'content': record.value, 'disabled': False}]


+ 21
- 2
octodns/provider/route53.py View File

@ -90,6 +90,10 @@ class _Route53Record(object):
_values_for_AAAA = _values_for_values _values_for_AAAA = _values_for_values
_values_for_NS = _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): def _values_for_value(self, record):
return [record.value] return [record.value]
@ -222,8 +226,8 @@ class Route53Provider(BaseProvider):
In general the account used will need full permissions on Route53. In general the account used will need full permissions on Route53.
''' '''
SUPPORTS_GEO = True 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 # This should be bumped when there are underlying changes made to the
# health check config. # health check config.
@ -319,6 +323,21 @@ class Route53Provider(BaseProvider):
_data_for_A = _data_for_geo _data_for_A = _data_for_geo
_data_for_AAAA = _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): def _data_for_single(self, rrset):
return { return {
'type': rrset['Type'], 'type': rrset['Type'],


+ 56
- 14
octodns/record.py View File

@ -81,29 +81,16 @@ class Record(object):
'A': ARecord, 'A': ARecord,
'AAAA': AaaaRecord, 'AAAA': AaaaRecord,
'ALIAS': AliasRecord, 'ALIAS': AliasRecord,
# cert
'CAA': CaaRecord,
'CNAME': CnameRecord, 'CNAME': CnameRecord,
# dhcid
# dname
# dnskey
# ds
# ipseckey
# key
# kx
# loc
'MX': MxRecord, 'MX': MxRecord,
'NAPTR': NaptrRecord, 'NAPTR': NaptrRecord,
'NS': NsRecord, 'NS': NsRecord,
# nsap
'PTR': PtrRecord, 'PTR': PtrRecord,
# px
# rp
# soa - would it even make sense?
'SPF': SpfRecord, 'SPF': SpfRecord,
'SRV': SrvRecord, 'SRV': SrvRecord,
'SSHFP': SshfpRecord, 'SSHFP': SshfpRecord,
'TXT': TxtRecord, 'TXT': TxtRecord,
# url
}[_type] }[_type]
except KeyError: except KeyError:
raise Exception('Unknown record type: "{}"'.format(_type)) raise Exception('Unknown record type: "{}"'.format(_type))
@ -398,6 +385,61 @@ class AliasRecord(_ValueMixin, Record):
return value 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): class CnameRecord(_ValueMixin, Record):
_type = 'CNAME' _type = 'CNAME'


+ 2
- 2
requirements.txt View File

@ -4,10 +4,10 @@ PyYaml==3.12
azure-mgmt-dns==1.0.1 azure-mgmt-dns==1.0.1
azure-common==1.1.6 azure-common==1.1.6
boto3==1.4.6 boto3==1.4.6
botocore==1.6.0
botocore==1.6.8
dnspython==1.15.0 dnspython==1.15.0
docutils==0.14 docutils==0.14
dyn==1.7.10
dyn==1.8.0
futures==3.1.1 futures==3.1.1
incf.countryutils==1.0 incf.countryutils==1.0
ipaddress==1.0.18 ipaddress==1.0.18


+ 5
- 0
tests/config/unit.tests.yaml View File

@ -31,6 +31,11 @@
values: values:
- 6.2.3.4. - 6.2.3.4.
- 7.2.3.4. - 7.2.3.4.
- type: CAA
values:
- flags: 0
tag: issue
value: ca.unit.tests
_srv._tcp: _srv._tcp:
ttl: 600 ttl: 600
type: SRV type: SRV


+ 21
- 2
tests/fixtures/cloudflare-dns_records-page-2.json View File

@ -118,14 +118,33 @@
"meta": { "meta": {
"auto_added": false "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": { "result_info": {
"page": 2, "page": 2,
"per_page": 10, "per_page": 10,
"total_pages": 2, "total_pages": 2,
"count": 7,
"total_count": 17
"count": 8,
"total_count": 19
}, },
"success": true, "success": true,
"errors": [], "errors": [],


+ 17
- 1
tests/fixtures/dnsimple-page-2.json View File

@ -159,12 +159,28 @@
"system_record": false, "system_record": false,
"created_at": "2017-03-09T15:55:09Z", "created_at": "2017-03-09T15:55:09Z",
"updated_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": { "pagination": {
"current_page": 2, "current_page": 2,
"per_page": 20, "per_page": 20,
"total_entries": 29,
"total_entries": 30,
"total_pages": 2 "total_pages": 2
} }
} }

+ 12
- 0
tests/fixtures/powerdns-full-data.json View File

@ -230,6 +230,18 @@
], ],
"ttl": 300, "ttl": 300,
"type": "A" "type": "A"
},
{
"comments": [],
"name": "unit.tests.",
"records": [
{
"content": "0 issue \"ca.unit.tests\"",
"disabled": false
}
],
"ttl": 3600,
"type": "CAA"
} }
], ],
"serial": 2017012803, "serial": 2017012803,


+ 6
- 6
tests/test_octodns_provider_cloudflare.py View File

@ -118,7 +118,7 @@ class TestCloudflareProvider(TestCase):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals(9, len(zone.records))
self.assertEquals(10, len(zone.records))
changes = self.expected.changes(zone, provider) changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes)) self.assertEquals(0, len(changes))
@ -126,7 +126,7 @@ class TestCloudflareProvider(TestCase):
# re-populating the same zone/records comes out of cache, no calls # re-populating the same zone/records comes out of cache, no calls
again = Zone('unit.tests.', []) again = Zone('unit.tests.', [])
provider.populate(again) provider.populate(again)
self.assertEquals(9, len(again.records))
self.assertEquals(10, len(again.records))
def test_apply(self): def test_apply(self):
provider = CloudflareProvider('test', 'email', 'token') provider = CloudflareProvider('test', 'email', 'token')
@ -140,12 +140,12 @@ class TestCloudflareProvider(TestCase):
'id': 42, 'id': 42,
} }
}, # zone create }, # zone create
] + [None] * 16 # individual record creates
] + [None] * 17 # individual record creates
# non-existant zone, create everything # non-existant zone, create everything
plan = provider.plan(self.expected) plan = provider.plan(self.expected)
self.assertEquals(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([ provider._request.assert_has_calls([
# created the domain # created the domain
@ -170,7 +170,7 @@ class TestCloudflareProvider(TestCase):
}), }),
], True) ], True)
# expected number of total calls # expected number of total calls
self.assertEquals(18, provider._request.call_count)
self.assertEquals(19, provider._request.call_count)
provider._request.reset_mock() provider._request.reset_mock()


+ 3
- 3
tests/test_octodns_provider_dnsimple.py View File

@ -78,14 +78,14 @@ class TestDnsimpleProvider(TestCase):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals(14, len(zone.records))
self.assertEquals(15, len(zone.records))
changes = self.expected.changes(zone, provider) changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes)) self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache # 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', []) again = Zone('unit.tests.', [])
provider.populate(again) provider.populate(again)
self.assertEquals(14, len(again.records))
self.assertEquals(15, len(again.records))
# bust the cache # bust the cache
del provider._zone_records[zone.name] del provider._zone_records[zone.name]
@ -147,7 +147,7 @@ class TestDnsimpleProvider(TestCase):
}), }),
]) ])
# expected number of total calls # 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() provider._client._request.reset_mock()


+ 20
- 2
tests/test_octodns_provider_dyn.py View File

@ -109,6 +109,14 @@ class TestDynProvider(TestCase):
'weight': 22, 'weight': 22,
'port': 20, 'port': 20,
'target': 'foo-2.unit.tests.' '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)) expected.add_record(Record.new(expected, name, data))
@ -321,6 +329,16 @@ class TestDynProvider(TestCase):
'ttl': 307, 'ttl': 307,
'zone': 'unit.tests', '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.', []) got = Zone('unit.tests.', [])
@ -414,10 +432,10 @@ class TestDynProvider(TestCase):
update_mock.assert_called() update_mock.assert_called()
add_mock.assert_called() add_mock.assert_called()
# Once for each dyn record (8 Records, 2 of which have dual values) # 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', {}), execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}),
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() execute_mock.reset_mock()


+ 14
- 0
tests/test_octodns_provider_ns1.py View File

@ -96,6 +96,15 @@ class TestNs1Provider(TestCase):
'type': 'NS', 'type': 'NS',
'values': ['ns3.unit.tests.', 'ns4.unit.tests.'], '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 = [{ nsone_records = [{
'type': 'A', 'type': 'A',
@ -141,6 +150,11 @@ class TestNs1Provider(TestCase):
'ttl': 39, 'ttl': 39,
'short_answers': ['ns3.unit.tests.', 'ns4.unit.tests.'], 'short_answers': ['ns3.unit.tests.', 'ns4.unit.tests.'],
'domain': 'sub.unit.tests.', 'domain': 'sub.unit.tests.',
}, {
'type': 'CAA',
'ttl': 40,
'short_answers': ['0 issue ca.unit.tests'],
'domain': 'unit.tests.',
}] }]
@patch('nsone.NSONE.loadZone') @patch('nsone.NSONE.loadZone')


+ 3
- 3
tests/test_octodns_provider_powerdns.py View File

@ -79,7 +79,7 @@ class TestPowerDnsProvider(TestCase):
source = YamlProvider('test', join(dirname(__file__), 'config')) source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected) source.populate(expected)
expected_n = len(expected.records) - 1 expected_n = len(expected.records) - 1
self.assertEquals(14, expected_n)
self.assertEquals(15, expected_n)
# No diffs == no changes # No diffs == no changes
with requests_mock() as mock: with requests_mock() as mock:
@ -87,7 +87,7 @@ class TestPowerDnsProvider(TestCase):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals(14, len(zone.records))
self.assertEquals(15, len(zone.records))
changes = expected.changes(zone, provider) changes = expected.changes(zone, provider)
self.assertEquals(0, len(changes)) self.assertEquals(0, len(changes))
@ -167,7 +167,7 @@ class TestPowerDnsProvider(TestCase):
expected = Zone('unit.tests.', []) expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config')) source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected) source.populate(expected)
self.assertEquals(15, len(expected.records))
self.assertEquals(16, len(expected.records))
# A small change to a single record # A small change to a single record
with requests_mock() as mock: with requests_mock() as mock:


+ 21
- 8
tests/test_octodns_provider_route53.py View File

@ -77,6 +77,12 @@ class TestRoute53Provider(TestCase):
{'ttl': 67, 'type': 'NS', 'values': ['8.2.3.4.', '9.2.3.4.']}), {'ttl': 67, 'type': 'NS', 'values': ['8.2.3.4.', '9.2.3.4.']}),
('sub', ('sub',
{'ttl': 68, 'type': 'NS', 'values': ['5.2.3.4.', '6.2.3.4.']}), {'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) record = Record.new(expected, name, data)
expected.add_record(record) expected.add_record(record)
@ -300,6 +306,13 @@ class TestRoute53Provider(TestCase):
'Value': 'ns1.unit.tests.', 'Value': 'ns1.unit.tests.',
}], }],
'TTL': 69, 'TTL': 69,
}, {
'Name': 'unit.tests.',
'Type': 'CAA',
'ResourceRecords': [{
'Value': '0 issue "ca.unit.tests"',
}],
'TTL': 69,
}], }],
'IsTruncated': False, 'IsTruncated': False,
'MaxItems': '100', 'MaxItems': '100',
@ -347,7 +360,7 @@ class TestRoute53Provider(TestCase):
{'HostedZoneId': 'z42'}) {'HostedZoneId': 'z42'})
plan = provider.plan(self.expected) plan = provider.plan(self.expected)
self.assertEquals(8, len(plan.changes))
self.assertEquals(9, len(plan.changes))
for change in plan.changes: for change in plan.changes:
self.assertIsInstance(change, Create) self.assertIsInstance(change, Create)
stubber.assert_no_pending_responses() stubber.assert_no_pending_responses()
@ -366,7 +379,7 @@ class TestRoute53Provider(TestCase):
'SubmittedAt': '2017-01-29T01:02:03Z', 'SubmittedAt': '2017-01-29T01:02:03Z',
}}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY}) }}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY})
self.assertEquals(8, provider.apply(plan))
self.assertEquals(9, provider.apply(plan))
stubber.assert_no_pending_responses() stubber.assert_no_pending_responses()
# Delete by monkey patching in a populate that includes an extra record # Delete by monkey patching in a populate that includes an extra record
@ -579,7 +592,7 @@ class TestRoute53Provider(TestCase):
{}) {})
plan = provider.plan(self.expected) plan = provider.plan(self.expected)
self.assertEquals(8, len(plan.changes))
self.assertEquals(9, len(plan.changes))
for change in plan.changes: for change in plan.changes:
self.assertIsInstance(change, Create) self.assertIsInstance(change, Create)
stubber.assert_no_pending_responses() stubber.assert_no_pending_responses()
@ -626,7 +639,7 @@ class TestRoute53Provider(TestCase):
'SubmittedAt': '2017-01-29T01:02:03Z', 'SubmittedAt': '2017-01-29T01:02:03Z',
}}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY}) }}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY})
self.assertEquals(8, provider.apply(plan))
self.assertEquals(9, provider.apply(plan))
stubber.assert_no_pending_responses() stubber.assert_no_pending_responses()
def test_health_checks_pagination(self): def test_health_checks_pagination(self):
@ -1174,16 +1187,16 @@ class TestRoute53Provider(TestCase):
@patch('octodns.provider.route53.Route53Provider._really_apply') @patch('octodns.provider.route53.Route53Provider._really_apply')
def test_apply_1(self, really_apply_mock): 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) provider.apply(plan)
really_apply_mock.assert_called_once() really_apply_mock.assert_called_once()
@patch('octodns.provider.route53.Route53Provider._really_apply') @patch('octodns.provider.route53.Route53Provider._really_apply')
def test_apply_2(self, really_apply_mock): 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) provider.apply(plan)
self.assertEquals(2, really_apply_mock.call_count) self.assertEquals(2, really_apply_mock.call_count)


+ 1
- 1
tests/test_octodns_provider_yaml.py View File

@ -30,7 +30,7 @@ class TestYamlProvider(TestCase):
# without it we see everything # without it we see everything
source.populate(zone) 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 # Assumption here is that a clean round-trip means that everything
# worked as expected, data that went in came back out and could be # worked as expected, data that went in came back out and could be


+ 133
- 4
tests/test_octodns_record.py View File

@ -7,10 +7,10 @@ from __future__ import absolute_import, division, print_function, \
from unittest import TestCase 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 octodns.zone import Zone
from helpers import GeoProvider, SimpleProvider from helpers import GeoProvider, SimpleProvider
@ -206,6 +206,66 @@ class TestRecord(TestCase):
# __repr__ doesn't blow up # __repr__ doesn't blow up
a.__repr__() 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): def test_cname(self):
self.assertSingleValue(CnameRecord, 'target.foo.com.', self.assertSingleValue(CnameRecord, 'target.foo.com.',
'other.foo.com.') 'other.foo.com.')
@ -861,6 +921,75 @@ class TestRecordValidation(TestCase):
}) })
self.assertEquals(['missing trailing .'], ctx.exception.reasons) 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): def test_CNAME(self):
# doesn't blow up # doesn't blow up
Record.new(self.zone, 'www', { Record.new(self.zone, 'www', {


Loading…
Cancel
Save