Skip to content

Commit a5a8d00

Browse files
authored
Merge pull request #116 from Northover/CAA
CAA record support
2 parents 5662e79 + feb13cf commit a5a8d00

File tree

5 files changed

+211
-55
lines changed

5 files changed

+211
-55
lines changed

docs/tm/records/records.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ ALIASRecord
1818
:members:
1919
:undoc-members:
2020

21+
CAARecord
22+
==========
23+
.. autoclass:: dyn.tm.records.CAARecord
24+
:members:
25+
:undoc-members:
26+
2127
CERTRecord
2228
==========
2329
.. autoclass:: dyn.tm.records.CERTRecord

dyn/core.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"""
77
import base64
88
import copy
9-
import time
109
import locale
1110
import logging
11+
import re
1212
import threading
13+
import time
1314
from datetime import datetime
1415

1516
from . import __version__
@@ -114,6 +115,7 @@ def __init__(self, host=None, port=443, ssl=True, history=False,
114115
self._encoding = locale.getdefaultlocale()[-1] or 'UTF-8'
115116
self._token = self._conn = self._last_response = None
116117
self._permissions = None
118+
self._tasks = {}
117119

118120
@classmethod
119121
def new_session(cls, *args, **kwargs):
@@ -243,6 +245,39 @@ def _handle_error(self, uri, method, raw_args):
243245
"""
244246
return None
245247

248+
def _retry(self, msgs, final=False):
249+
"""Retry logic around throttled or blocked tasks"""
250+
251+
throttle_err = 'RATE_LIMIT_EXCEEDED'
252+
throttled = any(throttle_err == err['ERR_CD'] for err in msgs)
253+
254+
if throttled:
255+
# We're rate limited, so wait 5 seconds and try again
256+
return dict(retry=True, wait=5, final=final)
257+
258+
blocked_err = 'Operation blocked by current task'
259+
blocked = any(blocked_err in err['INFO'] for err in msgs)
260+
261+
pat = re.compile(r'^task_id:\s+(\d+)$')
262+
if blocked:
263+
try:
264+
# Get the task id
265+
task = next(pat.match(i['INFO']).group(1) for i in msgs
266+
if pat.match(i.get('INFO', '')))
267+
except:
268+
# Task id could not be recovered
269+
wait = 1
270+
else:
271+
# Exponential backoff for individual blocked tasks
272+
wait = self._tasks.get(task, 1)
273+
self._tasks[task] = wait * 2 + 1
274+
275+
# Give up if final or wait > 30 seconds
276+
return dict(retry=True, wait=wait, final=wait > 30 or final)
277+
278+
# Neither blocked nor throttled?
279+
return dict(retry=False, wait=0, final=True)
280+
246281
def _handle_response(self, response, uri, method, raw_args, final):
247282
"""Handle the processing of the API's response"""
248283
body = response.read()
@@ -258,12 +293,15 @@ def _handle_response(self, response, uri, method, raw_args, final):
258293
ret_val['status']))
259294

260295
self._meta_update(uri, method, ret_val)
261-
# Handle retrying if ZoneProp is blocking the current task
262-
error_msg = 'Operation blocked by current task'
263-
if ret_val['status'] == 'failure' and error_msg in \
264-
ret_val['msgs'][0]['INFO'] and not final:
265-
time.sleep(8)
266-
return self.execute(uri, method, raw_args, final=True)
296+
297+
retry = {}
298+
# Try to retry?
299+
if ret_val['status'] == 'failure' and not final:
300+
retry = self._retry(ret_val['msgs'], final)
301+
302+
if retry.get('retry', False):
303+
time.sleep(retry['wait'])
304+
return self.execute(uri, method, raw_args, final=retry['final'])
267305
else:
268306
return self._process_response(ret_val, method)
269307

dyn/tm/records.py

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
from ..compat import force_unicode
1010

1111
__author__ = 'jnappi'
12-
__all__ = ['DNSRecord', 'ARecord', 'AAAARecord', 'ALIASRecord', 'CDSRecord',
13-
'CDNSKEYRecord', 'CERTRecord', 'CNAMERecord', 'CSYNCRecord',
14-
'DHCIDRecord', 'DNAMERecord', 'DNSKEYRecord', 'DSRecord',
15-
'KEYRecord', 'KXRecord', 'LOCRecord', 'IPSECKEYRecord', 'MXRecord',
16-
'NAPTRRecord', 'PTRRecord', 'PXRecord', 'NSAPRecord',
12+
__all__ = ['DNSRecord', 'ARecord', 'AAAARecord', 'ALIASRecord', 'CAARecord',
13+
'CDSRecord', 'CDNSKEYRecord', 'CERTRecord', 'CNAMERecord',
14+
'CSYNCRecord', 'DHCIDRecord', 'DNAMERecord', 'DNSKEYRecord',
15+
'DSRecord', 'KEYRecord', 'KXRecord', 'LOCRecord', 'IPSECKEYRecord',
16+
'MXRecord', 'NAPTRRecord', 'PTRRecord', 'PXRecord', 'NSAPRecord',
1717
'RPRecord', 'NSRecord', 'SOARecord', 'SPFRecord', 'SRVRecord',
1818
'TLSARecord', 'TXTRecord', 'SSHFPRecord', 'UNKNOWNRecord']
1919

@@ -1240,6 +1240,116 @@ def __repr__(self):
12401240
return self.__str__()
12411241

12421242

1243+
class CAARecord(DNSRecord):
1244+
"""Certification Authority Authorization (CAA) Resource Record
1245+
1246+
This record allows a DNS domain name holder to specify one or more
1247+
Certification Authorities (CAs) authorized to issue certificates for that
1248+
domain. CAA Resource Records allow a public Certification Authority to
1249+
implement additional controls to reduce the risk of unintended certificate
1250+
mis-issue. This document defines the syntax of the CAA record and rules
1251+
for processing CAA records by certificate issuers.
1252+
1253+
see: https://tools.ietf.org/html/rfc6844 """
1254+
1255+
def __init__(self, zone, fqdn, *args, **kwargs):
1256+
"""Create a :class:`~dyn.tm.records.CAARecord` object
1257+
1258+
:param zone: Name of zone where the record will be added
1259+
:param fqdn: Name of node where the record will be added
1260+
:param flags: A byte
1261+
:param tag: A string defining the tag component of the <tag>=<value>
1262+
record property. May be one of:
1263+
issue: The issue property entry authorizes the holder of
1264+
the domain name <Issuer Domain Name> or a party acting under
1265+
the explicit authority of the holder of that domain name to
1266+
issue certificates for the domain in which the property is
1267+
published.
1268+
1269+
issuewild: The issuewild property entry authorizes the
1270+
holder of the domain name <Issuer Domain Name> or a party
1271+
acting under the explicit authority of the holder of that
1272+
domain name to issue wildcard certificates for the domain in
1273+
which the property is published.
1274+
1275+
iodef: Specifies a URL to which an issuer MAY report
1276+
certificate issue requests that are inconsistent with the
1277+
issuer's Certification Practices or Certificate Policy, or
1278+
that a Certificate Evaluator may use to report observation
1279+
of a possible policy violation.
1280+
:param value: A string representing the value component of the
1281+
property. This will be an issuer domain name or a URL.
1282+
:param ttl: TTL for this record. Use 0 for zone default
1283+
"""
1284+
fields = ['flags', 'tag', 'value', 'ttl']
1285+
1286+
create = kwargs.pop('create', None)
1287+
if create is not None:
1288+
super(CAARecord, self).__init__(zone, fqdn, create)
1289+
self._build(kwargs)
1290+
self._record_type = 'CAARecord'
1291+
else:
1292+
super(CAARecord, self).__init__(zone, fqdn)
1293+
self._record_type = 'CAARecord'
1294+
arg_length = len(args) + len(kwargs)
1295+
if 'record_id' in kwargs:
1296+
self._get_record(kwargs['record_id'])
1297+
elif arg_length == 1:
1298+
self._get_record(*args, **kwargs)
1299+
elif any(field in kwargs for field in fields) or arg_length >= 1:
1300+
self._post(*args, **kwargs)
1301+
1302+
def _post(self, flags, tag, value, ttl=0):
1303+
self._flags = flags
1304+
self._tag = tag
1305+
self._value = value
1306+
self._ttl = ttl
1307+
self.api_args = dict(
1308+
rdata=dict(flags=flags, tag=tag, value=value),
1309+
ttl=ttl
1310+
)
1311+
self._create_record(self.api_args)
1312+
1313+
def rdata(self):
1314+
return dict(caa_rdata=super(CAARecord, self).rdata())
1315+
1316+
@property
1317+
def flags(self):
1318+
self._pull()
1319+
return self._flags
1320+
1321+
@flags.setter
1322+
def flags(self, value):
1323+
self.api_args['rdata']['flags'] = value
1324+
self._update_record(self.api_args)
1325+
if self._implicitPublish:
1326+
self._flags = value
1327+
1328+
@property
1329+
def tag(self):
1330+
self._pull()
1331+
return self._tag
1332+
1333+
@tag.setter
1334+
def tag(self, value):
1335+
self.api_args['rdata']['tag'] = value
1336+
self._update_record(self.api_args)
1337+
if self._implicitPublish:
1338+
self._tag = value
1339+
1340+
@property
1341+
def value(self):
1342+
self._pull()
1343+
return self._value
1344+
1345+
@value.setter
1346+
def value(self, value):
1347+
self.api_args['rdata']['value'] = value
1348+
self._update_record(self.api_args)
1349+
if self._implicitPublish:
1350+
self._value = value
1351+
1352+
12431353
class DSRecord(DNSRecord):
12441354
"""The Delegation Signer (DS) record type is used in DNSSEC to create the
12451355
chain of trust or authority from a signed parent zone to a signed child

dyn/tm/services/dsf.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from dyn.compat import force_unicode, string_types
77
from dyn.tm.utils import APIList, Active
88
from dyn.tm.errors import DynectInvalidArgumentError
9-
from dyn.tm.records import (ARecord, AAAARecord, ALIASRecord, CDSRecord,
10-
CDNSKEYRecord, CSYNCRecord, CERTRecord,
9+
from dyn.tm.records import (ARecord, AAAARecord, ALIASRecord, CAARecord,
10+
CDSRecord, CDNSKEYRecord, CSYNCRecord, CERTRecord,
1111
CNAMERecord, DHCIDRecord, DNAMERecord,
1212
DNSKEYRecord, DSRecord, KEYRecord, KXRecord,
1313
LOCRecord, IPSECKEYRecord, MXRecord, NAPTRRecord,
@@ -3578,7 +3578,7 @@ def __init__(self, zone, fqdn=None):
35783578
self.records = {}
35793579

35803580
self.recs = {'A': ARecord, 'AAAA': AAAARecord,
3581-
'ALIAS': ALIASRecord, 'CDS': CDSRecord,
3581+
'ALIAS': ALIASRecord, 'CAA': CAARecord, 'CDS': CDSRecord,
35823582
'CDNSKEY': CDNSKEYRecord, 'CSYNC': CSYNCRecord,
35833583
'CERT': CERTRecord, 'CNAME': CNAMERecord,
35843584
'DHCID': DHCIDRecord, 'DNAME': DNAMERecord,
@@ -3597,10 +3597,10 @@ def add_record(self, record_type='A', *args, **kwargs):
35973597
"""Adds an a record with the provided data to this :class:`Node`
35983598
35993599
:param record_type: The type of record you would like to add.
3600-
Valid record_type arguments are: 'A', 'AAAA', 'CERT', 'CNAME',
3601-
'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', 'IPSECKEY',
3602-
'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', 'SPF',
3603-
'SRV', and 'TXT'.
3600+
Valid record_type arguments are: 'A', 'AAAA', 'CAA', 'CERT',
3601+
'CNAME', 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC',
3602+
'IPSECKEY', 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA',
3603+
'SPF', 'SRV', and 'TXT'.
36043604
:param args: Non-keyword arguments to pass to the Record constructor
36053605
:param kwargs: Keyword arguments to pass to the Record constructor
36063606
"""
@@ -3650,13 +3650,14 @@ def get_all_records_by_type(self, record_type):
36503650
are owned by this node.
36513651
36523652
:param record_type: The type of :class:`DNSRecord` you wish returned.
3653-
Valid record_type arguments are: 'A', 'AAAA', 'CERT', 'CNAME',
3654-
'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', 'IPSECKEY',
3655-
'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', 'SPF',
3656-
'SRV', and 'TXT'.
3653+
Valid record_type arguments are: 'A', 'AAAA', 'CAA', 'CERT',
3654+
'CNAME', 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC',
3655+
'IPSECKEY', 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA',
3656+
'SPF', 'SRV', and 'TXT'.
36573657
:return: A list of :class:`DNSRecord`'s
36583658
"""
3659-
names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'CERT': 'CERTRecord',
3659+
names = {'A': 'ARecord', 'AAAA': 'AAAARecord',
3660+
'CAA': 'CAARecord', 'CERT': 'CERTRecord',
36603661
'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord',
36613662
'DNAME': 'DNAMERecord', 'DNSKEY': 'DNSKEYRecord',
36623663
'DS': 'DSRecord', 'KEY': 'KEYRecord', 'KX': 'KXRecord',

dyn/tm/zones.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dyn.tm.errors import (DynectCreateError, DynectGetError,
1010
DynectInvalidArgumentError)
1111
from dyn.tm.records import (ARecord, AAAARecord, ALIASRecord, CDSRecord,
12-
CDNSKEYRecord, CSYNCRecord, CERTRecord,
12+
CAARecord, CDNSKEYRecord, CSYNCRecord, CERTRecord,
1313
CNAMERecord, DHCIDRecord, DNAMERecord,
1414
DNSKEYRecord, DSRecord, KEYRecord, KXRecord,
1515
LOCRecord, IPSECKEYRecord, MXRecord, NAPTRRecord,
@@ -27,15 +27,15 @@
2727
'ExternalNameserver', 'ExternalNameserverEntry']
2828

2929
RECS = {'A': ARecord, 'AAAA': AAAARecord, 'ALIAS': ALIASRecord,
30-
'CDS': CDSRecord, 'CDNSKEY': CDNSKEYRecord, 'CSYNC': CSYNCRecord,
31-
'CERT': CERTRecord, 'CNAME': CNAMERecord, 'DHCID': DHCIDRecord,
32-
'DNAME': DNAMERecord, 'DNSKEY': DNSKEYRecord, 'DS': DSRecord,
33-
'KEY': KEYRecord, 'KX': KXRecord, 'LOC': LOCRecord,
30+
'CAA': CAARecord, 'CDS': CDSRecord, 'CDNSKEY': CDNSKEYRecord,
31+
'CSYNC': CSYNCRecord, 'CERT': CERTRecord, 'CNAME': CNAMERecord,
32+
'DHCID': DHCIDRecord, 'DNAME': DNAMERecord, 'DNSKEY': DNSKEYRecord,
33+
'DS': DSRecord, 'KEY': KEYRecord, 'KX': KXRecord, 'LOC': LOCRecord,
3434
'IPSECKEY': IPSECKEYRecord, 'MX': MXRecord, 'NAPTR': NAPTRRecord,
3535
'PTR': PTRRecord, 'PX': PXRecord, 'NSAP': NSAPRecord,
36-
'RP': RPRecord, 'NS': NSRecord, 'SOA': SOARecord,
37-
'SPF': SPFRecord, 'SRV': SRVRecord, 'TLSA': TLSARecord,
38-
'TXT': TXTRecord, 'SSHFP': SSHFPRecord, 'UNKNOWN': UNKNOWNRecord}
36+
'RP': RPRecord, 'NS': NSRecord, 'SOA': SOARecord, 'SPF': SPFRecord,
37+
'SRV': SRVRecord, 'TLSA': TLSARecord, 'TXT': TXTRecord,
38+
'SSHFP': SSHFPRecord, 'UNKNOWN': UNKNOWNRecord}
3939

4040

4141
def get_all_zones():
@@ -522,24 +522,23 @@ def get_all_records_by_type(self, record_type):
522522
are owned by this node.
523523
524524
:param record_type: The type of :class:`DNSRecord` you wish returned.
525-
Valid record_type arguments are: 'A', 'AAAA', 'CERT', 'CNAME',
526-
'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', 'IPSECKEY',
527-
'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', 'SPF',
528-
'SRV', and 'TXT'.
525+
Valid record_type arguments are: 'A', 'AAAA', 'CAA', 'CERT',
526+
'CNAME', 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC',
527+
'IPSECKEY', 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA',
528+
'SPF', 'SRV', and 'TXT'.
529529
:return: A :class:`List` of :class:`DNSRecord`'s
530530
"""
531531
names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'ALIAS': 'ALIASRecord',
532-
'CDS': 'CDSRecord', 'CDNSKEY': 'CDNSKEYRecord',
533-
'CERT': 'CERTRecord', 'CSYNC': 'CSYNCRecord',
534-
'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord',
535-
'DNAME': 'DNAMERecord', 'DNSKEY': 'DNSKEYRecord',
536-
'DS': 'DSRecord', 'KEY': 'KEYRecord', 'KX': 'KXRecord',
537-
'LOC': 'LOCRecord', 'IPSECKEY': 'IPSECKEYRecord',
538-
'MX': 'MXRecord', 'NAPTR': 'NAPTRRecord', 'PTR': 'PTRRecord',
539-
'PX': 'PXRecord', 'NSAP': 'NSAPRecord', 'RP': 'RPRecord',
540-
'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord',
541-
'SRV': 'SRVRecord', 'TLSA': 'TLSARecord', 'TXT': 'TXTRecord',
542-
'SSHFP': 'SSHFPRecord'}
532+
'CAA': 'CAARecord', 'CDS': 'CDSRecord', 'CDNSKEY':
533+
'CDNSKEYRecord', 'CERT': 'CERTRecord', 'CSYNC': 'CSYNCRecord',
534+
'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord', 'DNAME':
535+
'DNAMERecord', 'DNSKEY': 'DNSKEYRecord', 'DS': 'DSRecord',
536+
'KEY': 'KEYRecord', 'KX': 'KXRecord', 'LOC': 'LOCRecord',
537+
'IPSECKEY': 'IPSECKEYRecord', 'MX': 'MXRecord', 'NAPTR':
538+
'NAPTRRecord', 'PTR': 'PTRRecord', 'PX': 'PXRecord', 'NSAP':
539+
'NSAPRecord', 'RP': 'RPRecord', 'NS': 'NSRecord', 'SOA':
540+
'SOARecord', 'SPF': 'SPFRecord', 'SRV': 'SRVRecord', 'TLSA':
541+
'TLSARecord', 'TXT': 'TXTRecord', 'SSHFP': 'SSHFPRecord'}
543542

544543
constructor = RECS[record_type]
545544
uri = '/{}/{}/{}/'.format(names[record_type], self._name, self.fqdn)
@@ -1053,16 +1052,18 @@ def get_all_records_by_type(self, record_type):
10531052
'SRV', and 'TXT'.
10541053
:return: A list of :class:`DNSRecord`'s
10551054
"""
1056-
names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'CERT': 'CERTRecord',
1057-
'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord',
1058-
'DNAME': 'DNAMERecord', 'DNSKEY': 'DNSKEYRecord',
1059-
'DS': 'DSRecord', 'KEY': 'KEYRecord', 'KX': 'KXRecord',
1060-
'LOC': 'LOCRecord', 'IPSECKEY': 'IPSECKEYRecord',
1061-
'MX': 'MXRecord', 'NAPTR': 'NAPTRRecord', 'PTR': 'PTRRecord',
1055+
names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'CAA': 'CAARecord',
1056+
'CERT': 'CERTRecord', 'CNAME': 'CNAMERecord',
1057+
'DHCID': 'DHCIDRecord', 'DNAME': 'DNAMERecord',
1058+
'DNSKEY': 'DNSKEYRecord', 'DS': 'DSRecord',
1059+
'KEY': 'KEYRecord', 'KX': 'KXRecord', 'LOC': 'LOCRecord',
1060+
'IPSECKEY': 'IPSECKEYRecord', 'MX': 'MXRecord',
1061+
'NAPTR': 'NAPTRRecord', 'PTR': 'PTRRecord',
10621062
'PX': 'PXRecord', 'NSAP': 'NSAPRecord', 'RP': 'RPRecord',
10631063
'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord',
1064-
'SRV': 'SRVRecord', 'TLSA': 'TLSARecord', 'TXT': 'TXTRecord',
1065-
'SSHFP': 'SSHFPRecord', 'ALIAS': 'ALIASRecord'}
1064+
'SRV': 'SRVRecord', 'TLSA': 'TLSARecord',
1065+
'TXT': 'TXTRecord', 'SSHFP': 'SSHFPRecord',
1066+
'ALIAS': 'ALIASRecord'}
10661067
constructor = RECS[record_type]
10671068
uri = '/{}/{}/{}/'.format(names[record_type], self.zone,
10681069
self.fqdn)

0 commit comments

Comments
 (0)