mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Added tests, clean up and small modifications
This commit is contained in:
@@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function, \
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from requests import HTTPError, Session
|
from requests import Session
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from ..record import Record
|
from ..record import Record
|
||||||
@@ -18,8 +18,8 @@ def add_trailing_dot(value):
|
|||||||
'''
|
'''
|
||||||
Add trailing dots to values
|
Add trailing dots to values
|
||||||
'''
|
'''
|
||||||
assert value
|
assert value, 'Missing value'
|
||||||
assert value[-1] != '.'
|
assert value[-1] != '.', 'Value already has trailing dot'
|
||||||
return value + '.'
|
return value + '.'
|
||||||
|
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ def remove_trailing_dot(value):
|
|||||||
'''
|
'''
|
||||||
Remove trailing dots from values
|
Remove trailing dots from values
|
||||||
'''
|
'''
|
||||||
assert value
|
assert value, 'Missing value'
|
||||||
assert value[-1] == '.'
|
assert value[-1] == '.', 'Value already missing trailing dot'
|
||||||
return value[:-1]
|
return value[:-1]
|
||||||
|
|
||||||
|
|
||||||
@@ -44,13 +44,15 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
|
|
||||||
SUPPORTS_GEO = False
|
SUPPORTS_GEO = False
|
||||||
SUPPORTS_DYNAMIC = False
|
SUPPORTS_DYNAMIC = False
|
||||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS',
|
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NS',
|
||||||
'SRV', 'SSHFP', 'CAA', 'TXT'))
|
'SRV', 'SSHFP', 'CAA', 'TXT'))
|
||||||
BASE = 'https://dnsapi.mythic-beasts.com/'
|
BASE = 'https://dnsapi.mythic-beasts.com/'
|
||||||
TIMEOUT = 15
|
|
||||||
|
|
||||||
def __init__(self, identifier, passwords, *args, **kwargs):
|
def __init__(self, identifier, passwords, *args, **kwargs):
|
||||||
self.log = getLogger('MythicBeastsProvider[{}]'.format(identifier))
|
self.log = getLogger('MythicBeastsProvider[{}]'.format(identifier))
|
||||||
|
|
||||||
|
assert isinstance(passwords, dict), 'Missing passwords'
|
||||||
|
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
'__init__: id=%s, registered zones; %s',
|
'__init__: id=%s, registered zones; %s',
|
||||||
identifier,
|
identifier,
|
||||||
@@ -62,23 +64,28 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
self._sess = sess
|
self._sess = sess
|
||||||
|
|
||||||
def _request(self, method, path, data=None):
|
def _request(self, method, path, data=None):
|
||||||
self.log.debug('_request: method=%s, path=%s', method, path)
|
self.log.debug('_request: method=%s, path=%s data=%s',
|
||||||
|
method, path, data)
|
||||||
|
|
||||||
url = self.BASE
|
resp = self._sess.request(method, path, data=data)
|
||||||
resp = self._sess.request(method, url, data=data, timeout=self.TIMEOUT)
|
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
'_request: status=%d data=%s',
|
'_request: status=%d data=%s',
|
||||||
resp.status_code,
|
resp.status_code,
|
||||||
resp.text)
|
resp.text[:20])
|
||||||
if resp.status_code != 200:
|
|
||||||
self.log.info('request failed: %s, response %s', data, resp.text)
|
if resp.status_code == 401:
|
||||||
resp.raise_for_status()
|
raise Exception('Mythic Beasts unauthorized for domain: {}'
|
||||||
|
.format(data['domain']))
|
||||||
|
resp.raise_for_status()
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def _post(self, data=None):
|
def _post(self, data=None):
|
||||||
return self._request('POST', self.BASE, data=data)
|
return self._request('POST', self.BASE, data=data)
|
||||||
|
|
||||||
def records(self, zone):
|
def records(self, zone):
|
||||||
|
assert zone in self._passwords, 'Missing password for domain: {}' \
|
||||||
|
.format(remove_trailing_dot(zone))
|
||||||
|
|
||||||
return self._post({
|
return self._post({
|
||||||
'domain': remove_trailing_dot(zone),
|
'domain': remove_trailing_dot(zone),
|
||||||
'password': self._passwords[zone],
|
'password': self._passwords[zone],
|
||||||
@@ -113,16 +120,17 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
[raw_values['value'] for raw_values in data['raw_values']]:
|
[raw_values['value'] for raw_values in data['raw_values']]:
|
||||||
match = re.match('^([0-9]+)\\s+(\\S+)$', raw_value, re.IGNORECASE)
|
match = re.match('^([0-9]+)\\s+(\\S+)$', raw_value, re.IGNORECASE)
|
||||||
|
|
||||||
if match is not None:
|
assert match is not None, 'Unable to parse MX data'
|
||||||
exchange = match.group(2)
|
|
||||||
|
|
||||||
if not exchange.endswith('.'):
|
exchange = match.group(2)
|
||||||
exchange = '{}.{}'.format(exchange, data['zone'])
|
|
||||||
|
|
||||||
values.append({
|
if not exchange.endswith('.'):
|
||||||
'preference': match.group(1),
|
exchange = '{}.{}'.format(exchange, data['zone'])
|
||||||
'exchange': exchange,
|
|
||||||
})
|
values.append({
|
||||||
|
'preference': match.group(1),
|
||||||
|
'exchange': exchange,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'type': _type,
|
'type': _type,
|
||||||
@@ -155,54 +163,61 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _data_for_SRV(_type, data):
|
def _data_for_SRV(_type, data):
|
||||||
ttl = data['raw_values'][0]['ttl']
|
ttl = max([raw_values['ttl'] for raw_values in data['raw_values']])
|
||||||
raw_value = data['raw_values'][0]['value']
|
values = []
|
||||||
|
|
||||||
match = re.match(
|
for raw_value in \
|
||||||
'^([0-9]+)\\s+([0-9]+)\\s+([0-9]+)\\s+(\\S+)$',
|
[raw_values['value'] for raw_values in data['raw_values']]:
|
||||||
raw_value,
|
|
||||||
re.IGNORECASE)
|
match = re.match(
|
||||||
|
'^([0-9]+)\\s+([0-9]+)\\s+([0-9]+)\\s+(\\S+)$',
|
||||||
|
raw_value,
|
||||||
|
re.IGNORECASE)
|
||||||
|
|
||||||
|
assert match is not None, 'Unable to parse SRV data'
|
||||||
|
|
||||||
if match is not None:
|
|
||||||
target = match.group(4)
|
target = match.group(4)
|
||||||
if not target.endswith('.'):
|
if not target.endswith('.'):
|
||||||
target = '{}.{}'.format(target, data['zone'])
|
target = '{}.{}'.format(target, data['zone'])
|
||||||
|
|
||||||
value = {
|
values.append({
|
||||||
'priority': match.group(1),
|
'priority': match.group(1),
|
||||||
'weight': match.group(2),
|
'weight': match.group(2),
|
||||||
'port': match.group(3),
|
'port': match.group(3),
|
||||||
'target': target,
|
'target': target,
|
||||||
}
|
})
|
||||||
|
|
||||||
return MythicBeastsProvider._data_for_single(
|
return {
|
||||||
'SRV',
|
'type': _type,
|
||||||
{'raw_values': [
|
'values': values,
|
||||||
{'value': value, 'ttl': ttl}
|
'ttl': ttl,
|
||||||
]})
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _data_for_SSHFP(_type, data):
|
def _data_for_SSHFP(_type, data):
|
||||||
ttl = data['raw_values'][0]['ttl']
|
ttl = max([raw_values['ttl'] for raw_values in data['raw_values']])
|
||||||
raw_value = data['raw_values'][0]['value']
|
values = []
|
||||||
|
|
||||||
match = re.match(
|
for raw_value in \
|
||||||
'^([0-9]+)\\s+([0-9]+)\\s+(\\S+)$',
|
[raw_values['value'] for raw_values in data['raw_values']]:
|
||||||
raw_value,
|
match = re.match(
|
||||||
re.IGNORECASE)
|
'^([0-9]+)\\s+([0-9]+)\\s+(\\S+)$',
|
||||||
|
raw_value,
|
||||||
|
re.IGNORECASE)
|
||||||
|
|
||||||
if match is not None:
|
assert match is not None, 'Unable to parse SSHFP data'
|
||||||
value = {
|
|
||||||
|
values.append({
|
||||||
'algorithm': match.group(1),
|
'algorithm': match.group(1),
|
||||||
'fingerprint_type': match.group(2),
|
'fingerprint_type': match.group(2),
|
||||||
'fingerprint': match.group(3),
|
'fingerprint': match.group(3),
|
||||||
}
|
})
|
||||||
|
|
||||||
return MythicBeastsProvider._data_for_single(
|
return {
|
||||||
'SSHFP',
|
'type': _type,
|
||||||
{'raw_values': [
|
'values': values,
|
||||||
{'value': value, 'ttl': ttl}
|
'ttl': ttl,
|
||||||
]})
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _data_for_CAA(_type, data):
|
def _data_for_CAA(_type, data):
|
||||||
@@ -214,17 +229,17 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
raw_value,
|
raw_value,
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
|
|
||||||
if match is not None:
|
assert match is not None, 'Unable to parse CAA data'
|
||||||
value = {
|
|
||||||
'flags': match.group(1),
|
value = {
|
||||||
'tag': match.group(2),
|
'flags': match.group(1),
|
||||||
'value': match.group(3),
|
'tag': match.group(2),
|
||||||
}
|
'value': match.group(3),
|
||||||
return MythicBeastsProvider._data_for_single(
|
}
|
||||||
'CAA',
|
|
||||||
{'raw_values': [
|
return MythicBeastsProvider._data_for_single(
|
||||||
{'value': value, 'ttl': ttl}
|
'CAA',
|
||||||
]})
|
{'raw_values': [{'value': value, 'ttl': ttl}]})
|
||||||
|
|
||||||
_data_for_NS = _data_for_multiple
|
_data_for_NS = _data_for_multiple
|
||||||
_data_for_TXT = _data_for_multiple
|
_data_for_TXT = _data_for_multiple
|
||||||
@@ -235,84 +250,69 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||||
target, lenient)
|
target, lenient)
|
||||||
|
|
||||||
resp = None
|
resp = self.records(zone.name)
|
||||||
try:
|
|
||||||
resp = self.records(zone.name)
|
|
||||||
except HTTPError as err:
|
|
||||||
if err.response.status_code == 401:
|
|
||||||
# Nicer error message for auth problems
|
|
||||||
raise Exception(
|
|
||||||
'Mythic Beasts authentication problem with {}'.format(
|
|
||||||
zone.name))
|
|
||||||
else:
|
|
||||||
# just re-throw
|
|
||||||
raise
|
|
||||||
|
|
||||||
before = len(zone.records)
|
before = len(zone.records)
|
||||||
exists = False
|
exists = False
|
||||||
data = dict()
|
data = dict()
|
||||||
|
|
||||||
if resp:
|
exists = True
|
||||||
exists = True
|
for line in resp.content.splitlines():
|
||||||
for line in resp.content.splitlines():
|
match = re.match(
|
||||||
match = re.match(
|
'^(\\S+)\\s+(\\d+)\\s+(\\S+)\\s+(.*)$',
|
||||||
'^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(.*)$',
|
line,
|
||||||
line,
|
re.IGNORECASE)
|
||||||
re.IGNORECASE)
|
|
||||||
|
|
||||||
if match is not None:
|
if match is None:
|
||||||
if match.group(1) == '@':
|
self.log.debug('failed to match line: %s', line)
|
||||||
_name = ''
|
continue
|
||||||
else:
|
|
||||||
_name = match.group(1)
|
|
||||||
|
|
||||||
_type = match.group(3)
|
if match.group(1) == '@':
|
||||||
_ttl = int(match.group(2))
|
_name = ''
|
||||||
_value = match.group(4).strip()
|
else:
|
||||||
|
_name = match.group(1)
|
||||||
|
|
||||||
if _type == 'SOA':
|
_type = match.group(3)
|
||||||
continue
|
_ttl = int(match.group(2))
|
||||||
|
_value = match.group(4).strip()
|
||||||
|
|
||||||
try:
|
if _type == 'TXT':
|
||||||
if getattr(self, '_data_for_{}'.format(_type)) is not None:
|
_value = _value.replace(';', '\\;')
|
||||||
|
|
||||||
if _type not in data:
|
if hasattr(self, '_data_for_{}'.format(_type)):
|
||||||
data[_type] = dict()
|
|
||||||
|
|
||||||
if _name not in data[_type]:
|
if _type not in data:
|
||||||
data[_type][_name] = {
|
data[_type] = dict()
|
||||||
'raw_values': [{'value': _value, 'ttl': _ttl}],
|
|
||||||
'name': _name,
|
|
||||||
'zone': zone.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
else:
|
if _name not in data[_type]:
|
||||||
data[_type][_name].get('raw_values').append(
|
data[_type][_name] = {
|
||||||
{'value': _value, 'ttl': _ttl}
|
'raw_values': [{'value': _value, 'ttl': _ttl}],
|
||||||
)
|
'name': _name,
|
||||||
except AttributeError:
|
'zone': zone.name,
|
||||||
self.log.debug('skipping {} as not supported', _type)
|
}
|
||||||
continue
|
|
||||||
|
|
||||||
for _type in data:
|
else:
|
||||||
for _name in data[_type]:
|
data[_type][_name].get('raw_values').append(
|
||||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
{'value': _value, 'ttl': _ttl}
|
||||||
|
|
||||||
self.log.debug(
|
|
||||||
'record: %s,\t%s',
|
|
||||||
_type,
|
|
||||||
data[_type][_name])
|
|
||||||
|
|
||||||
record = Record.new(
|
|
||||||
zone,
|
|
||||||
_name,
|
|
||||||
data_for(_type, data[_type][_name]),
|
|
||||||
source=self
|
|
||||||
)
|
)
|
||||||
zone.add_record(record, lenient=lenient)
|
else:
|
||||||
|
self.log.debug('skipping %s as not supported', _type)
|
||||||
|
|
||||||
|
for _type in data:
|
||||||
|
for _name in data[_type]:
|
||||||
|
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||||
|
|
||||||
|
record = Record.new(
|
||||||
|
zone,
|
||||||
|
_name,
|
||||||
|
data_for(_type, data[_type][_name]),
|
||||||
|
source=self
|
||||||
|
)
|
||||||
|
zone.add_record(record, lenient=lenient)
|
||||||
|
|
||||||
self.log.debug('populate: found %s records, exists=%s',
|
self.log.debug('populate: found %s records, exists=%s',
|
||||||
len(zone.records) - before, exists)
|
len(zone.records) - before, exists)
|
||||||
|
|
||||||
return exists
|
return exists
|
||||||
|
|
||||||
def _compile_commands(self, action, change):
|
def _compile_commands(self, action, change):
|
||||||
@@ -322,8 +322,7 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
|
|
||||||
if action == 'ADD':
|
if action == 'ADD':
|
||||||
record = change.new
|
record = change.new
|
||||||
|
else:
|
||||||
elif action == 'DELETE':
|
|
||||||
record = change.existing
|
record = change.existing
|
||||||
|
|
||||||
hostname = remove_trailing_dot(record.fqdn)
|
hostname = remove_trailing_dot(record.fqdn)
|
||||||
@@ -354,13 +353,14 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
))
|
))
|
||||||
|
|
||||||
elif _type == 'SRV':
|
elif _type == 'SRV':
|
||||||
data = values[0].data
|
for value in values:
|
||||||
commands.append('{} {} {} {} {}'.format(
|
data = value.data
|
||||||
base,
|
commands.append('{} {} {} {} {}'.format(
|
||||||
data['priority'],
|
base,
|
||||||
data['weight'],
|
data['priority'],
|
||||||
data['port'],
|
data['weight'],
|
||||||
data['target']))
|
data['port'],
|
||||||
|
data['target']))
|
||||||
|
|
||||||
elif _type == 'MX':
|
elif _type == 'MX':
|
||||||
for value in values:
|
for value in values:
|
||||||
@@ -371,13 +371,11 @@ class MythicBeastsProvider(BaseProvider):
|
|||||||
data['exchange']))
|
data['exchange']))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
if hasattr(self, '_data_for_{}'.format(_type)):
|
||||||
if getattr(self, '_data_for_{}'.format(_type)) is not None:
|
commands.append('{} {}'.format(
|
||||||
commands.append('{} {}'.format(
|
base, values[0]))
|
||||||
base, values[0]))
|
else:
|
||||||
except AttributeError:
|
self.log.debug('skipping %s as not supported', _type)
|
||||||
self.log.debug('skipping {} as not supported', _type)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return commands
|
return commands
|
||||||
|
|
||||||
|
25
tests/fixtures/mythicbeasts-list.txt
vendored
Normal file
25
tests/fixtures/mythicbeasts-list.txt
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
@ 3600 NS 6.2.3.4.
|
||||||
|
@ 3600 NS 7.2.3.4.
|
||||||
|
@ 300 A 1.2.3.4
|
||||||
|
@ 300 A 1.2.3.5
|
||||||
|
@ 3600 SSHFP 1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73
|
||||||
|
@ 3600 SSHFP 1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49
|
||||||
|
@ 3600 CAA 0 issue ca.unit.tests
|
||||||
|
_srv._tcp 600 SRV 10 20 30 foo-1.unit.tests.
|
||||||
|
_srv._tcp 600 SRV 12 20 30 foo-2.unit.tests.
|
||||||
|
aaaa 600 AAAA 2601:644:500:e210:62f8:1dff:feb8:947a
|
||||||
|
cname 300 CNAME unit.tests.
|
||||||
|
excluded 300 CNAME unit.tests.
|
||||||
|
ignored 300 A 9.9.9.9
|
||||||
|
included 3600 CNAME unit.tests.
|
||||||
|
mx 300 MX 10 smtp-4.unit.tests.
|
||||||
|
mx 300 MX 20 smtp-2.unit.tests.
|
||||||
|
mx 300 MX 30 smtp-3.unit.tests.
|
||||||
|
mx 300 MX 40 smtp-1.unit.tests.
|
||||||
|
sub 3600 NS 6.2.3.4.
|
||||||
|
sub 3600 NS 7.2.3.4.
|
||||||
|
txt 600 TXT "Bah bah black sheep"
|
||||||
|
txt 600 TXT "have you any wool."
|
||||||
|
txt 600 TXT "v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs"
|
||||||
|
www 300 A 2.2.3.6
|
||||||
|
www.sub 300 A 2.2.3.6
|
356
tests/test_octodns_provider_mythicbeasts.py
Normal file
356
tests/test_octodns_provider_mythicbeasts.py
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function, \
|
||||||
|
unicode_literals
|
||||||
|
|
||||||
|
from os.path import dirname, join
|
||||||
|
|
||||||
|
from requests_mock import ANY, mock as requests_mock
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from octodns.provider.mythicbeasts import MythicBeastsProvider, \
|
||||||
|
add_trailing_dot, remove_trailing_dot
|
||||||
|
from octodns.provider.yaml import YamlProvider
|
||||||
|
from octodns.zone import Zone
|
||||||
|
from octodns.record import Create, Update, Delete, Record
|
||||||
|
|
||||||
|
|
||||||
|
class TestMythicBeastsProvider(TestCase):
|
||||||
|
expected = Zone('unit.tests.', [])
|
||||||
|
source = YamlProvider('test_expected', join(dirname(__file__), 'config'))
|
||||||
|
source.populate(expected)
|
||||||
|
|
||||||
|
# Dump anything we don't support from expected
|
||||||
|
for record in list(expected.records):
|
||||||
|
if record._type not in MythicBeastsProvider.SUPPORTS:
|
||||||
|
expected._remove_record(record)
|
||||||
|
|
||||||
|
def test_trailing_dot(self):
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
add_trailing_dot('unit.tests.')
|
||||||
|
self.assertEquals('Value already has trailing dot',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
remove_trailing_dot('unit.tests')
|
||||||
|
self.assertEquals('Value already missing trailing dot',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
self.assertEquals(add_trailing_dot('unit.tests'), 'unit.tests.')
|
||||||
|
self.assertEquals(remove_trailing_dot('unit.tests.'), 'unit.tests')
|
||||||
|
|
||||||
|
def test_data_for_single(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [{'value': 'a:a::c', 'ttl': 0}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_single = MythicBeastsProvider._data_for_single('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_single, dict))
|
||||||
|
self.assertEquals('a:a::c', test_single['value'])
|
||||||
|
|
||||||
|
def test_data_for_multiple(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [
|
||||||
|
{'value': 'b:b::d', 'ttl': 60},
|
||||||
|
{'value': 'a:a::c', 'ttl': 60}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_multiple = MythicBeastsProvider._data_for_multiple('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_multiple, dict))
|
||||||
|
self.assertEquals(2, len(test_multiple['values']))
|
||||||
|
|
||||||
|
def test_data_for_MX(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [
|
||||||
|
{'value': '10 un.unit', 'ttl': 60},
|
||||||
|
{'value': '20 dau.unit', 'ttl': 60},
|
||||||
|
{'value': '30 tri.unit', 'ttl': 60}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_MX = MythicBeastsProvider._data_for_MX('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_MX, dict))
|
||||||
|
self.assertEquals(3, len(test_MX['values']))
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
test_MX = MythicBeastsProvider._data_for_MX(
|
||||||
|
'',
|
||||||
|
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||||
|
)
|
||||||
|
self.assertEquals('Unable to parse MX data',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
def test_data_for_CNAME(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [{'value': 'cname', 'ttl': 60}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_cname = MythicBeastsProvider._data_for_CNAME('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_cname, dict))
|
||||||
|
self.assertEquals('cname.unit.tests.', test_cname['value'])
|
||||||
|
|
||||||
|
def test_data_for_ANAME(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [{'value': 'aname', 'ttl': 60}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_aname = MythicBeastsProvider._data_for_ANAME('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_aname, dict))
|
||||||
|
self.assertEquals('aname', test_aname['value'])
|
||||||
|
|
||||||
|
def test_data_for_SRV(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [
|
||||||
|
{'value': '10 20 30 un.srv.unit', 'ttl': 60},
|
||||||
|
{'value': '20 30 40 dau.srv.unit', 'ttl': 60},
|
||||||
|
{'value': '30 30 50 tri.srv.unit', 'ttl': 60}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_SRV = MythicBeastsProvider._data_for_SRV('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_SRV, dict))
|
||||||
|
self.assertEquals(3, len(test_SRV['values']))
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
test_SRV = MythicBeastsProvider._data_for_SRV(
|
||||||
|
'',
|
||||||
|
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||||
|
)
|
||||||
|
self.assertEquals('Unable to parse SRV data',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
def test_data_for_SSHFP(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [
|
||||||
|
{'value': '1 1 0123456789abcdef', 'ttl': 60},
|
||||||
|
{'value': '1 2 0123456789abcdef', 'ttl': 60},
|
||||||
|
{'value': '2 3 0123456789abcdef', 'ttl': 60}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_SSHFP = MythicBeastsProvider._data_for_SSHFP('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_SSHFP, dict))
|
||||||
|
self.assertEquals(3, len(test_SSHFP['values']))
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
test_SSHFP = MythicBeastsProvider._data_for_SSHFP(
|
||||||
|
'',
|
||||||
|
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||||
|
)
|
||||||
|
self.assertEquals('Unable to parse SSHFP data',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
def test_data_for_CAA(self):
|
||||||
|
test_data = {
|
||||||
|
'raw_values': [{'value': '1 issue letsencrypt.org', 'ttl': 60}],
|
||||||
|
'zone': 'unit.tests.',
|
||||||
|
}
|
||||||
|
test_CAA = MythicBeastsProvider._data_for_CAA('', test_data)
|
||||||
|
self.assertTrue(isinstance(test_CAA, dict))
|
||||||
|
self.assertEquals(3, len(test_CAA['value']))
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
test_CAA = MythicBeastsProvider._data_for_CAA(
|
||||||
|
'',
|
||||||
|
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||||
|
)
|
||||||
|
self.assertEquals('Unable to parse CAA data',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
def test_alias_command_generation(self):
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
zone.add_record(Record.new(zone, 'prawf', {
|
||||||
|
'ttl': 60,
|
||||||
|
'type': 'ALIAS',
|
||||||
|
'value': 'alias.unit.tests.',
|
||||||
|
}))
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=200, text='')
|
||||||
|
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
|
||||||
|
plan = provider.plan(zone)
|
||||||
|
change = plan.changes[0]
|
||||||
|
|
||||||
|
command = provider._compile_commands('ADD', change)
|
||||||
|
self.assertEquals(
|
||||||
|
['ADD prawf.unit.tests 60 ANAME alias.unit.tests.'],
|
||||||
|
command
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_txt_command_generation(self):
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
zone.add_record(Record.new(zone, 'prawf', {
|
||||||
|
'ttl': 60,
|
||||||
|
'type': 'TXT',
|
||||||
|
'value': 'prawf prawf dyma prawf',
|
||||||
|
}))
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=200, text='')
|
||||||
|
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
|
||||||
|
plan = provider.plan(zone)
|
||||||
|
change = plan.changes[0]
|
||||||
|
|
||||||
|
command = provider._compile_commands('ADD', change)
|
||||||
|
self.assertEquals(
|
||||||
|
['ADD prawf.unit.tests 60 TXT prawf prawf dyma prawf'],
|
||||||
|
command
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_command_generation(self):
|
||||||
|
class FakeChangeRecord(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.__fqdn = 'prawf.unit.tests.'
|
||||||
|
self._type = 'NOOP'
|
||||||
|
self.value = 'prawf'
|
||||||
|
self.ttl = 60
|
||||||
|
|
||||||
|
@property
|
||||||
|
def new(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def existing(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fqdn(self):
|
||||||
|
return self.__fqdn
|
||||||
|
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=200, text='')
|
||||||
|
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
record = FakeChangeRecord()
|
||||||
|
command = provider._compile_commands('ADD', record)
|
||||||
|
self.assertEquals([], command)
|
||||||
|
|
||||||
|
def test_populate(self):
|
||||||
|
provider = None
|
||||||
|
|
||||||
|
# Null passwords dict
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
provider = MythicBeastsProvider('test', None)
|
||||||
|
self.assertEquals('Missing passwords', err.exception.message)
|
||||||
|
|
||||||
|
# Missing password
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=401, text='ERR Not authenticated')
|
||||||
|
|
||||||
|
with self.assertRaises(AssertionError) as err:
|
||||||
|
provider = MythicBeastsProvider('test', dict())
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
provider.populate(zone)
|
||||||
|
self.assertEquals(
|
||||||
|
'Missing password for domain: unit.tests',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
# Failed authentication
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=401, text='ERR Not authenticated')
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as err:
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
provider.populate(zone)
|
||||||
|
self.assertEquals(
|
||||||
|
'Mythic Beasts unauthorized for domain: unit.tests',
|
||||||
|
err.exception.message)
|
||||||
|
|
||||||
|
# Check unmatched lines are ignored
|
||||||
|
test_data = 'This should not match'
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=200, text=test_data)
|
||||||
|
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
provider.populate(zone)
|
||||||
|
self.assertEquals(0, len(zone.records))
|
||||||
|
|
||||||
|
# Check unsupported records are skipped
|
||||||
|
test_data = '@ 60 NOOP prawf\n@ 60 SPF prawf prawf prawf'
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=200, text=test_data)
|
||||||
|
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
provider.populate(zone)
|
||||||
|
self.assertEquals(0, len(zone.records))
|
||||||
|
|
||||||
|
# Check no changes between what we support and what's parsed
|
||||||
|
# from the unit.tests. config YAML. Also make sure we see the same
|
||||||
|
# for both after we've thrown away records we don't support
|
||||||
|
with requests_mock() as mock:
|
||||||
|
with open('tests/fixtures/mythicbeasts-list.txt') as file_handle:
|
||||||
|
mock.post(ANY, status_code=200, text=file_handle.read())
|
||||||
|
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
provider.populate(zone)
|
||||||
|
|
||||||
|
self.assertEquals(15, len(zone.records))
|
||||||
|
self.assertEquals(15, len(self.expected.records))
|
||||||
|
changes = self.expected.changes(zone, provider)
|
||||||
|
self.assertEquals(0, len(changes))
|
||||||
|
|
||||||
|
def test_apply(self):
|
||||||
|
provider = MythicBeastsProvider('test', {
|
||||||
|
'unit.tests.': 'mypassword'
|
||||||
|
})
|
||||||
|
zone = Zone('unit.tests.', [])
|
||||||
|
|
||||||
|
# Create blank zone
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=200, text='')
|
||||||
|
provider.populate(zone)
|
||||||
|
|
||||||
|
self.assertEquals(0, len(zone.records))
|
||||||
|
|
||||||
|
# Check deleting and adding/changing test record
|
||||||
|
existing = 'prawf 300 TXT prawf prawf prawf\ndileu 300 TXT dileu'
|
||||||
|
|
||||||
|
with requests_mock() as mock:
|
||||||
|
mock.post(ANY, status_code=200, text=existing)
|
||||||
|
|
||||||
|
# Mash up a new zone with records so a plan
|
||||||
|
# is generated with changes and applied. For some reason
|
||||||
|
# passing self.expected, or just changing each record's zone
|
||||||
|
# doesn't work. Nor does this without a single add_record after
|
||||||
|
wanted = Zone('unit.tests.', [])
|
||||||
|
for record in list(self.expected.records):
|
||||||
|
data = {'type': record._type}
|
||||||
|
data.update(record.data)
|
||||||
|
wanted.add_record(Record.new(wanted, record.name, data))
|
||||||
|
|
||||||
|
wanted.add_record(Record.new(wanted, 'prawf', {
|
||||||
|
'ttl': 60,
|
||||||
|
'type': 'TXT',
|
||||||
|
'value': 'prawf yw e',
|
||||||
|
}))
|
||||||
|
|
||||||
|
plan = provider.plan(wanted)
|
||||||
|
|
||||||
|
# Octo ignores NS records (15-1)
|
||||||
|
self.assertEquals(1, len(filter(lambda u: isinstance(u, Update),
|
||||||
|
plan.changes)))
|
||||||
|
self.assertEquals(1, len(filter(lambda d: isinstance(d, Delete),
|
||||||
|
plan.changes)))
|
||||||
|
self.assertEquals(14, len(filter(lambda c: isinstance(c, Create),
|
||||||
|
plan.changes)))
|
||||||
|
self.assertEquals(16, provider.apply(plan))
|
||||||
|
self.assertTrue(plan.exists)
|
Reference in New Issue
Block a user