diff --git a/octodns/provider/mythicbeasts.py b/octodns/provider/mythicbeasts.py index 3d1f5ec..af07ddb 100644 --- a/octodns/provider/mythicbeasts.py +++ b/octodns/provider/mythicbeasts.py @@ -32,16 +32,59 @@ def remove_trailing_dot(value): return value[:-1] +class MythicBeastsUnauthorizedException(Exception): + def __init__(self, zone, *args): + self.zone = zone + self.message = 'Mythic Beasts unauthorized for zone: {}'.format( + self.zone + ) + + super(MythicBeastsUnauthorizedException, self).__init__( + self.message, self.zone, *args) + + class MythicBeastsProvider(BaseProvider): ''' Mythic Beasts DNS API Provider - mythicbeasts: - class: octodns.provider.mythicbeasts.MythicBeastsProvider - zones: - my-zone: 'password' + Config settings: + + --- + providers: + config: + ... + mythicbeasts: + class: octodns.provider.mythicbeasts.MythicBeastsProvider + passwords: + my.domain.: 'password' + + zones: + my.domain.: + targets: + - mythic ''' + RE_MX = re.compile(r'^(?P[0-9]+)\s+(?P\S+)$', + re.IGNORECASE) + + RE_SRV = re.compile(r'^(?P[0-9]+)\s+(?P[0-9]+)\s+' + r'(?P[0-9]+)\s+(?P\S+)$', + re.IGNORECASE) + + RE_SSHFP = re.compile(r'^(?P[0-9]+)\s+' + r'(?P[0-9]+)\s+' + r'(?P\S+)$', + re.IGNORECASE) + + RE_CAA = re.compile(r'^(?P[0-9]+)\s+' + r'(?Pissue|issuewild|iodef)\s+' + r'(?P\S+)$', + re.IGNORECASE) + + RE_POPLINE = re.compile(r'^(?P\S+)\s+(?P\d+)\s+' + r'(?P\S+)\s+(?P.*)$', + re.IGNORECASE) + SUPPORTS_GEO = False SUPPORTS_DYNAMIC = False SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NS', @@ -51,7 +94,7 @@ class MythicBeastsProvider(BaseProvider): def __init__(self, identifier, passwords, *args, **kwargs): self.log = getLogger('MythicBeastsProvider[{}]'.format(identifier)) - assert isinstance(passwords, dict), 'Missing passwords' + assert isinstance(passwords, dict), 'Passwords must be a dictionary' self.log.debug( '__init__: id=%s, registered zones; %s', @@ -74,8 +117,7 @@ class MythicBeastsProvider(BaseProvider): resp.text[:20]) if resp.status_code == 401: - raise Exception('Mythic Beasts unauthorized for domain: {}' - .format(data['domain'])) + raise MythicBeastsUnauthorizedException(data['domain']) resp.raise_for_status() return resp @@ -118,17 +160,17 @@ class MythicBeastsProvider(BaseProvider): for raw_value in \ [raw_values['value'] for raw_values in data['raw_values']]: - match = re.match('^([0-9]+)\\s+(\\S+)$', raw_value, re.IGNORECASE) + match = MythicBeastsProvider.RE_MX.match(raw_value) assert match is not None, 'Unable to parse MX data' - exchange = match.group(2) + exchange = match.group('exchange') if not exchange.endswith('.'): exchange = '{}.{}'.format(exchange, data['zone']) values.append({ - 'preference': match.group(1), + 'preference': match.group('preference'), 'exchange': exchange, }) @@ -169,21 +211,18 @@ class MythicBeastsProvider(BaseProvider): for raw_value in \ [raw_values['value'] for raw_values in data['raw_values']]: - match = re.match( - '^([0-9]+)\\s+([0-9]+)\\s+([0-9]+)\\s+(\\S+)$', - raw_value, - re.IGNORECASE) + match = MythicBeastsProvider.RE_SRV.match(raw_value) assert match is not None, 'Unable to parse SRV data' - target = match.group(4) + target = match.group('target') if not target.endswith('.'): target = '{}.{}'.format(target, data['zone']) values.append({ - 'priority': match.group(1), - 'weight': match.group(2), - 'port': match.group(3), + 'priority': match.group('priority'), + 'weight': match.group('weight'), + 'port': match.group('port'), 'target': target, }) @@ -200,17 +239,14 @@ class MythicBeastsProvider(BaseProvider): for raw_value in \ [raw_values['value'] for raw_values in data['raw_values']]: - match = re.match( - '^([0-9]+)\\s+([0-9]+)\\s+(\\S+)$', - raw_value, - re.IGNORECASE) + match = MythicBeastsProvider.RE_SSHFP.match(raw_value) assert match is not None, 'Unable to parse SSHFP data' values.append({ - 'algorithm': match.group(1), - 'fingerprint_type': match.group(2), - 'fingerprint': match.group(3), + 'algorithm': match.group('algorithm'), + 'fingerprint_type': match.group('fingerprint_type'), + 'fingerprint': match.group('fingerprint'), }) return { @@ -224,17 +260,14 @@ class MythicBeastsProvider(BaseProvider): ttl = data['raw_values'][0]['ttl'] raw_value = data['raw_values'][0]['value'] - match = re.match( - '^([0-9]+)\\s+(issue|issuewild|iodef)\\s+(\\S+)$', - raw_value, - re.IGNORECASE) + match = MythicBeastsProvider.RE_CAA.match(raw_value) assert match is not None, 'Unable to parse CAA data' value = { - 'flags': match.group(1), - 'tag': match.group(2), - 'value': match.group(3), + 'flags': match.group('flags'), + 'tag': match.group('tag'), + 'value': match.group('value'), } return MythicBeastsProvider._data_for_single( @@ -258,10 +291,7 @@ class MythicBeastsProvider(BaseProvider): exists = True for line in resp.content.splitlines(): - match = re.match( - '^(\\S+)\\s+(\\d+)\\s+(\\S+)\\s+(.*)$', - line, - re.IGNORECASE) + match = MythicBeastsProvider.RE_POPLINE.match(line) if match is None: self.log.debug('failed to match line: %s', line) @@ -270,11 +300,11 @@ class MythicBeastsProvider(BaseProvider): if match.group(1) == '@': _name = '' else: - _name = match.group(1) + _name = match.group('name') - _type = match.group(3) - _ttl = int(match.group(2)) - _value = match.group(4).strip() + _type = match.group('type') + _ttl = int(match.group('ttl')) + _value = match.group('value').strip() if _type == 'TXT': _value = _value.replace(';', '\\;') @@ -318,13 +348,7 @@ class MythicBeastsProvider(BaseProvider): def _compile_commands(self, action, change): commands = [] - record = None - - if action == 'ADD': - record = change.new - else: - record = change.existing - + record = change.record hostname = remove_trailing_dot(record.fqdn) ttl = record.ttl _type = record._type diff --git a/tests/test_octodns_provider_mythicbeasts.py b/tests/test_octodns_provider_mythicbeasts.py index a1ef5d5..5deba3b 100644 --- a/tests/test_octodns_provider_mythicbeasts.py +++ b/tests/test_octodns_provider_mythicbeasts.py @@ -211,11 +211,7 @@ class TestMythicBeastsProvider(TestCase): self.ttl = 60 @property - def new(self): - return self - - @property - def existing(self): + def record(self): return self @property @@ -238,7 +234,8 @@ class TestMythicBeastsProvider(TestCase): # Null passwords dict with self.assertRaises(AssertionError) as err: provider = MythicBeastsProvider('test', None) - self.assertEquals('Missing passwords', err.exception.message) + self.assertEquals('Passwords must be a dictionary', + err.exception.message) # Missing password with requests_mock() as mock: @@ -263,7 +260,7 @@ class TestMythicBeastsProvider(TestCase): zone = Zone('unit.tests.', []) provider.populate(zone) self.assertEquals( - 'Mythic Beasts unauthorized for domain: unit.tests', + 'Mythic Beasts unauthorized for zone: unit.tests', err.exception.message) # Check unmatched lines are ignored