mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Working push for A records.
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from requests import HTTPError, Session, post
|
||||
import json
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
|
||||
@@ -40,6 +37,10 @@ class RackspaceProvider(BaseProvider):
|
||||
sess.headers.update({'X-Auth-Token': auth_token})
|
||||
self._sess = sess
|
||||
|
||||
# Map record type, name, and data to an id when populating so that
|
||||
# we can find the id for update and delete operations.
|
||||
self._id_map = {}
|
||||
|
||||
def _get_auth_token(self, username, api_key):
|
||||
ret = post('https://identity.api.rackspacecloud.com/v2.0/tokens',
|
||||
json={"auth": {"RAX-KSKEY:apiKeyCredentials": {"username": username, "apiKey": api_key}}},
|
||||
@@ -91,9 +92,15 @@ class RackspaceProvider(BaseProvider):
|
||||
def _post(self, path, data=None):
|
||||
return self._request('POST', path, data=data)
|
||||
|
||||
def _put(self, path, data=None):
|
||||
return self._request('PUT', path, data=data)
|
||||
|
||||
def _patch(self, path, data=None):
|
||||
return self._request('PATCH', path, data=data)
|
||||
|
||||
def _delete(self, path, data=None):
|
||||
return self._request('DELETE', path, data=data)
|
||||
|
||||
def _data_for_multiple(self, rrset):
|
||||
# TODO: geo not supported
|
||||
return {
|
||||
@@ -204,7 +211,7 @@ class RackspaceProvider(BaseProvider):
|
||||
resp_data = None
|
||||
try:
|
||||
domain_id = self._get_zone_id_for(zone)
|
||||
resp_data = self._request('GET', '/domains/{}/records'.format(domain_id), pagination_key='records')
|
||||
resp_data = self._request('GET', 'domains/{}/records'.format(domain_id), pagination_key='records')
|
||||
self.log.debug('populate: loaded')
|
||||
except HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
@@ -224,9 +231,9 @@ class RackspaceProvider(BaseProvider):
|
||||
if resp_data:
|
||||
records = self._group_records(resp_data)
|
||||
for record_type, records_of_type in records.items():
|
||||
if record_type == 'SOA':
|
||||
continue
|
||||
for raw_record_name, record_set in records_of_type.items():
|
||||
if record_type == 'SOA':
|
||||
continue
|
||||
data_for = getattr(self, '_data_for_{}'.format(record_type))
|
||||
record_name = zone.hostname_from_fqdn(raw_record_name)
|
||||
record = Record.new(zone, record_name, data_for(record_set),
|
||||
@@ -239,6 +246,7 @@ class RackspaceProvider(BaseProvider):
|
||||
def _group_records(self, all_records):
|
||||
records = defaultdict(lambda: defaultdict(list))
|
||||
for record in all_records:
|
||||
self._id_map[(record['type'], record['name'], record['data'])] = record['id']
|
||||
records[record['type']][record['name']].append(record)
|
||||
return records
|
||||
|
||||
@@ -294,61 +302,45 @@ class RackspaceProvider(BaseProvider):
|
||||
} for v in record.values]
|
||||
|
||||
def _mod_Create(self, change):
|
||||
new = change.new
|
||||
records_for = getattr(self, '_records_for_{}'.format(new._type))
|
||||
return {
|
||||
'name': new.fqdn,
|
||||
'type': new._type,
|
||||
'ttl': new.ttl,
|
||||
'changetype': 'REPLACE',
|
||||
'records': records_for(new)
|
||||
}
|
||||
out = []
|
||||
for value in change.new.values:
|
||||
out.append({
|
||||
'name': change.new.fqdn,
|
||||
'type': change.new._type,
|
||||
'data': value,
|
||||
'ttl': change.new.ttl,
|
||||
})
|
||||
return out
|
||||
|
||||
_mod_Update = _mod_Create
|
||||
def _mod_Update(self, change):
|
||||
# A reduction in number of values in an update record needs
|
||||
# to get upgraded into a Delete change for the removed values.
|
||||
deleted_values = set(change.existing.values) - set(change.new.values)
|
||||
delete_out = self._delete_given_change_values(change, deleted_values)
|
||||
|
||||
update_out = []
|
||||
for value in change.new.values:
|
||||
key = (change.existing._type, change.existing.fqdn, value)
|
||||
rsid = self._id_map[key]
|
||||
update_out.append({
|
||||
'id': rsid,
|
||||
'name': change.new.fqdn,
|
||||
'data': value,
|
||||
'ttl': change.new.ttl,
|
||||
})
|
||||
return update_out, delete_out
|
||||
|
||||
def _mod_Delete(self, change):
|
||||
existing = change.existing
|
||||
records_for = getattr(self, '_records_for_{}'.format(existing._type))
|
||||
return {
|
||||
'name': existing.fqdn,
|
||||
'type': existing._type,
|
||||
'ttl': existing.ttl,
|
||||
'changetype': 'DELETE',
|
||||
'records': records_for(existing)
|
||||
}
|
||||
return self._delete_given_change_values(change, change.existing.values)
|
||||
|
||||
def _get_nameserver_record(self, existing):
|
||||
return None
|
||||
|
||||
def _extra_changes(self, existing, _):
|
||||
self.log.debug('_extra_changes: zone=%s', existing.name)
|
||||
|
||||
ns = self._get_nameserver_record(existing)
|
||||
if not ns:
|
||||
return []
|
||||
|
||||
# sorting mostly to make things deterministic for testing, but in
|
||||
# theory it let us find what we're after quickier (though sorting would
|
||||
# ve more exepensive.)
|
||||
for record in sorted(existing.records):
|
||||
if record == ns:
|
||||
# We've found the top-level NS record, return any changes
|
||||
change = record.changes(ns, self)
|
||||
self.log.debug('_extra_changes: change=%s', change)
|
||||
if change:
|
||||
# We need to modify an existing record
|
||||
return [change]
|
||||
# No change is necessary
|
||||
return []
|
||||
# No existing top-level NS
|
||||
self.log.debug('_extra_changes: create')
|
||||
return [Create(ns)]
|
||||
|
||||
def _get_error(self, http_error):
|
||||
try:
|
||||
return http_error.response.json()['error']
|
||||
except Exception:
|
||||
return ''
|
||||
def _delete_given_change_values(self, change, values):
|
||||
out = []
|
||||
for value in values:
|
||||
key = (change.existing._type, change.existing.fqdn, value)
|
||||
rsid = self._id_map[key]
|
||||
out.append('id=' + rsid)
|
||||
del self._id_map[key]
|
||||
return out
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
@@ -356,46 +348,29 @@ class RackspaceProvider(BaseProvider):
|
||||
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
|
||||
len(changes))
|
||||
|
||||
mods = []
|
||||
domain_id = self._get_zone_id_for(desired)
|
||||
creates = []
|
||||
updates = []
|
||||
deletes = []
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
mods.append(getattr(self, '_mod_{}'.format(class_name))(change))
|
||||
self.log.debug('_apply: sending change request')
|
||||
if change.__class__.__name__ == 'Create':
|
||||
creates += self._mod_Create(change)
|
||||
elif change.__class__.__name__ == 'Update':
|
||||
add_updates, add_deletes = self._mod_Update(change)
|
||||
updates += add_updates
|
||||
deletes += add_deletes
|
||||
elif change.__class__.__name__ == 'Delete':
|
||||
deletes += self._mod_Delete(change)
|
||||
|
||||
try:
|
||||
self._patch('zones/{}'.format(desired.name),
|
||||
data={'rrsets': mods})
|
||||
self.log.debug('_apply: patched')
|
||||
except HTTPError as e:
|
||||
error = self._get_error(e)
|
||||
if e.response.status_code != 422 or \
|
||||
not error.startswith('Could not find domain '):
|
||||
self.log.error('_apply: status=%d, text=%s',
|
||||
e.response.status_code,
|
||||
e.response.text)
|
||||
raise
|
||||
self.log.info('_apply: creating zone=%s', desired.name)
|
||||
# 422 means powerdns doesn't know anything about the requsted
|
||||
# domain. We'll try to create it with the correct records instead
|
||||
# of update. Hopefully all the mods are creates :-)
|
||||
data = {
|
||||
'name': desired.name,
|
||||
'kind': 'Master',
|
||||
'masters': [],
|
||||
'nameservers': [],
|
||||
'rrsets': mods,
|
||||
'soa_edit_api': 'INCEPTION-INCREMENT',
|
||||
'serial': 0,
|
||||
}
|
||||
try:
|
||||
self._post('zones', data)
|
||||
except HTTPError as e:
|
||||
self.log.error('_apply: status=%d, text=%s',
|
||||
e.response.status_code,
|
||||
e.response.text)
|
||||
raise
|
||||
self.log.debug('_apply: created')
|
||||
if creates:
|
||||
data = {"records": sorted(creates, key=lambda v: v['name'])}
|
||||
self._post('domains/{}/records'.format(domain_id), data=data)
|
||||
|
||||
self.log.debug('_apply: complete')
|
||||
if updates:
|
||||
data = {"records": sorted(updates, key=lambda v: v['name'])}
|
||||
self._put('domains/{}/records'.format(domain_id), data=data)
|
||||
|
||||
if deletes:
|
||||
params = "&".join(sorted(deletes))
|
||||
self._delete('domains/{}/records?{}'.format(domain_id, params))
|
||||
|
||||
|
||||
29
tests/fixtures/rackspace-sample-recordset-existing-nameservers.json
vendored
Normal file
29
tests/fixtures/rackspace-sample-recordset-existing-nameservers.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"totalEntries" : 3,
|
||||
"records" : [{
|
||||
"name" : "unit.tests.",
|
||||
"id" : "A-6822995",
|
||||
"type" : "A",
|
||||
"data" : "1.2.3.4",
|
||||
"updated" : "2011-06-24T01:12:53.000+0000",
|
||||
"ttl" : 60,
|
||||
"created" : "2011-06-24T01:12:53.000+0000"
|
||||
}, {
|
||||
"name" : "unit.tests.",
|
||||
"id" : "NS-454454",
|
||||
"type" : "NS",
|
||||
"data" : "8.8.8.8.",
|
||||
"updated" : "2011-06-24T01:12:51.000+0000",
|
||||
"ttl" : 600,
|
||||
"created" : "2011-06-24T01:12:51.000+0000"
|
||||
}, {
|
||||
"name" : "unit.tests.",
|
||||
"id" : "NS-454455",
|
||||
"type" : "NS",
|
||||
"data" : "9.9.9.9.",
|
||||
"updated" : "2011-06-24T01:12:52.000+0000",
|
||||
"ttl" : 600,
|
||||
"created" : "2011-06-24T01:12:52.000+0000"
|
||||
}],
|
||||
"links" : []
|
||||
}
|
||||
@@ -5,10 +5,11 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
import json
|
||||
import re
|
||||
from json import loads, dumps
|
||||
from os.path import dirname, join
|
||||
from unittest import TestCase
|
||||
from urlparse import urlparse
|
||||
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
@@ -18,9 +19,11 @@ from octodns.provider.yaml import YamlProvider
|
||||
from octodns.record import Record
|
||||
from octodns.zone import Zone
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
EMPTY_TEXT = '''
|
||||
{
|
||||
"totalEntries" : 6,
|
||||
"totalEntries" : 0,
|
||||
"records" : []
|
||||
}
|
||||
'''
|
||||
@@ -37,51 +40,66 @@ with open('./tests/fixtures/rackspace-sample-recordset-page1.json') as fh:
|
||||
with open('./tests/fixtures/rackspace-sample-recordset-page2.json') as fh:
|
||||
RECORDS_PAGE_2 = fh.read()
|
||||
|
||||
def load_provider():
|
||||
with requests_mock() as mock:
|
||||
mock.post(ANY, status_code=200, text=AUTH_RESPONSE)
|
||||
return RackspaceProvider('test', 'api-key')
|
||||
with open('./tests/fixtures/rackspace-sample-recordset-existing-nameservers.json') as fh:
|
||||
RECORDS_EXISTING_NAMESERVERS = fh.read()
|
||||
|
||||
class TestRackspaceProvider(TestCase):
|
||||
def setUp(self):
|
||||
with requests_mock() as mock:
|
||||
mock.post(ANY, status_code=200, text=AUTH_RESPONSE)
|
||||
self.provider = RackspaceProvider('test', 'api-key')
|
||||
self.assertTrue(mock.called_once)
|
||||
|
||||
class TestRackspaceSource(TestCase):
|
||||
|
||||
def test_provider(self):
|
||||
provider = load_provider()
|
||||
|
||||
# Bad auth
|
||||
def test_bad_auth(self):
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=401, text='Unauthorized')
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.provider.populate(zone)
|
||||
self.assertTrue('unauthorized' in ctx.exception.message)
|
||||
self.assertTrue(mock.called_once)
|
||||
|
||||
# General error
|
||||
def test_server_error(self):
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=502, text='Things caught fire')
|
||||
|
||||
with self.assertRaises(HTTPError) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.provider.populate(zone)
|
||||
self.assertEquals(502, ctx.exception.response.status_code)
|
||||
self.assertTrue(mock.called_once)
|
||||
|
||||
# Non-existant zone doesn't populate anything
|
||||
def test_nonexistent_zone(self):
|
||||
# Non-existent zone doesn't populate anything
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=422,
|
||||
json={'error': "Could not find domain 'unit.tests.'"})
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
self.assertTrue(mock.called_once)
|
||||
|
||||
# The rest of this is messy/complicated b/c it's dealing with mocking
|
||||
def test_multipage_populate(self):
|
||||
with requests_mock() as mock:
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
mock.get(re.compile('records'), status_code=200, text=RECORDS_PAGE_1)
|
||||
mock.get(re.compile('records.*offset=3'), status_code=200, text=RECORDS_PAGE_2)
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
self.provider.populate(zone)
|
||||
self.assertEquals(5, len(zone.records))
|
||||
|
||||
def _load_full_config(self):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
expected_n = len(expected.records) - 1
|
||||
self.assertEquals(14, expected_n)
|
||||
self.assertEquals(15, len(expected.records))
|
||||
return expected
|
||||
|
||||
def test_changes_are_formatted_correctly(self):
|
||||
expected = self._load_full_config()
|
||||
|
||||
# No diffs == no changes
|
||||
with requests_mock() as mock:
|
||||
@@ -90,27 +108,551 @@ class TestRackspaceSource(TestCase):
|
||||
mock.get(re.compile('records.*offset=3'), status_code=200, text=RECORDS_PAGE_2)
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(5, len(zone.records))
|
||||
changes = expected.changes(zone, provider)
|
||||
self.provider.populate(zone)
|
||||
changes = expected.changes(zone, self.provider)
|
||||
self.assertEquals(18, len(changes))
|
||||
|
||||
def test_plan_disappearing_ns_records(self):
|
||||
expected = Zone('unit.tests.', [])
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'values': ['8.8.8.8.', '9.9.9.9.']
|
||||
}))
|
||||
expected.add_record(Record.new(expected, 'sub', {
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'values': ['8.8.8.8.', '9.9.9.9.']
|
||||
}))
|
||||
with requests_mock() as mock:
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
mock.get(re.compile('records'), status_code=200, text=EMPTY_TEXT)
|
||||
|
||||
plan = self.provider.plan(expected)
|
||||
self.assertTrue(mock.called)
|
||||
|
||||
# OctoDNS does not propagate top-level NS records.
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
|
||||
def _test_apply_with_data(self, data):
|
||||
expected = Zone('unit.tests.', [])
|
||||
for record in data.OtherRecords:
|
||||
expected.add_record(Record.new(expected, record['subdomain'], record['data']))
|
||||
|
||||
with requests_mock() as list_mock:
|
||||
list_mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
list_mock.get(re.compile('records'), status_code=200, json=data.OwnRecords)
|
||||
plan = self.provider.plan(expected)
|
||||
self.assertTrue(list_mock.called)
|
||||
if not data.ExpectChanges:
|
||||
self.assertFalse(plan)
|
||||
return
|
||||
|
||||
with requests_mock() as mock:
|
||||
called = set()
|
||||
|
||||
def make_assert_sending_right_body(expected):
|
||||
def _assert_sending_right_body(request, _context):
|
||||
called.add(request.method)
|
||||
if request.method != 'DELETE':
|
||||
self.assertEqual(request.headers['content-type'], 'application/json')
|
||||
self.assertDictEqual(expected, json.loads(request.body))
|
||||
else:
|
||||
parts = urlparse(request.url)
|
||||
self.assertEqual(expected, parts.query)
|
||||
return ''
|
||||
return _assert_sending_right_body
|
||||
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
mock.post(re.compile('domains/.*/records$'), status_code=202,
|
||||
text=make_assert_sending_right_body(data.ExpectedAdditions))
|
||||
mock.delete(re.compile('domains/.*/records?.*'), status_code=202,
|
||||
text=make_assert_sending_right_body(data.ExpectedDeletions))
|
||||
mock.put(re.compile('domains/.*/records$'), status_code=202,
|
||||
text=make_assert_sending_right_body(data.ExpectedUpdates))
|
||||
|
||||
self.provider.apply(plan)
|
||||
self.assertTrue(data.ExpectedAdditions is None or "POST" in called)
|
||||
self.assertTrue(data.ExpectedDeletions is None or "DELETE" in called)
|
||||
self.assertTrue(data.ExpectedUpdates is None or "PUT" in called)
|
||||
|
||||
def test_apply_no_change_empty(self):
|
||||
class TestData(object):
|
||||
OtherRecords = []
|
||||
OwnRecords = {
|
||||
"totalEntries": 0,
|
||||
"records": []
|
||||
}
|
||||
ExpectChanges = False
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_no_change_a_records(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": '',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'values': ['1.2.3.4', '1.2.3.5', '1.2.3.6']
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 3,
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-222222",
|
||||
"type": "A",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-333333",
|
||||
"type": "A",
|
||||
"data": "1.2.3.6",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = False
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_no_change_a_records_cross_zone(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": 'foo',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
},
|
||||
{
|
||||
"subdomain": 'bar',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 3,
|
||||
"records": [{
|
||||
"name": "foo.unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "bar.unit.tests.",
|
||||
"id": "A-222222",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = False
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_one_addition(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": '',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 0,
|
||||
"records": []
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = {
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_multiple_additions_exploding(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": '',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'values': ['1.2.3.4', '1.2.3.5', '1.2.3.6']
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 0,
|
||||
"records": []
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = {
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"type": "A",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"type": "A",
|
||||
"data": "1.2.3.6",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_multiple_additions_namespaced(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [{
|
||||
"subdomain": 'foo',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
}, {
|
||||
"subdomain": 'bar',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
}]
|
||||
OwnRecords = {
|
||||
"totalEntries": 0,
|
||||
"records": []
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = {
|
||||
"records": [{
|
||||
"name": "bar.unit.tests.",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "foo.unit.tests.",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_single_deletion(self):
|
||||
class TestData(object):
|
||||
OtherRecords = []
|
||||
OwnRecords = {
|
||||
"totalEntries": 1,
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = "id=A-111111"
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_multiple_deletions(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": '',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.5'
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 3,
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-222222",
|
||||
"type": "A",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-333333",
|
||||
"type": "A",
|
||||
"data": "1.2.3.6",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = "id=A-111111&id=A-333333"
|
||||
ExpectedUpdates = {
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-222222",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_multiple_deletions_cross_zone(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": '',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 3,
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "foo.unit.tests.",
|
||||
"id": "A-222222",
|
||||
"type": "A",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "bar.unit.tests.",
|
||||
"id": "A-333333",
|
||||
"type": "A",
|
||||
"data": "1.2.3.6",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = "id=A-222222&id=A-333333"
|
||||
ExpectedUpdates = None
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_single_update(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": '',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 3600,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 1,
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = {
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 3600
|
||||
}]
|
||||
}
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_multiple_updates(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": '',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 3600,
|
||||
'values': ['1.2.3.4', '1.2.3.5', '1.2.3.6']
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 3,
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-222222",
|
||||
"type": "A",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-333333",
|
||||
"type": "A",
|
||||
"data": "1.2.3.6",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = {
|
||||
"records": [{
|
||||
"name": "unit.tests.",
|
||||
"id": "A-111111",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 3600
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-222222",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 3600
|
||||
}, {
|
||||
"name": "unit.tests.",
|
||||
"id": "A-333333",
|
||||
"data": "1.2.3.6",
|
||||
"ttl": 3600
|
||||
}]
|
||||
}
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_apply_multiple_updates_cross_zone(self):
|
||||
class TestData(object):
|
||||
OtherRecords = [
|
||||
{
|
||||
"subdomain": 'foo',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 3600,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
},
|
||||
{
|
||||
"subdomain": 'bar',
|
||||
"data": {
|
||||
'type': 'A',
|
||||
'ttl': 3600,
|
||||
'value': '1.2.3.4'
|
||||
}
|
||||
}
|
||||
]
|
||||
OwnRecords = {
|
||||
"totalEntries": 2,
|
||||
"records": [{
|
||||
"name": "foo.unit.tests.",
|
||||
"id": "A-111111",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}, {
|
||||
"name": "bar.unit.tests.",
|
||||
"id": "A-222222",
|
||||
"type": "A",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 60
|
||||
}]
|
||||
}
|
||||
ExpectChanges = True
|
||||
ExpectedAdditions = None
|
||||
ExpectedDeletions = None
|
||||
ExpectedUpdates = {
|
||||
"records": [{
|
||||
"name": "bar.unit.tests.",
|
||||
"id": "A-222222",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 3600
|
||||
}, {
|
||||
"name": "foo.unit.tests.",
|
||||
"id": "A-111111",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 3600
|
||||
}]
|
||||
}
|
||||
return self._test_apply_with_data(TestData)
|
||||
|
||||
def test_provider(self):
|
||||
expected = self._load_full_config()
|
||||
|
||||
# No existing records -> creates for every record in expected
|
||||
with requests_mock() as mock:
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
mock.get(re.compile('records'), status_code=200, text=EMPTY_TEXT)
|
||||
|
||||
plan = self.provider.plan(expected)
|
||||
self.assertTrue(mock.called)
|
||||
self.assertEquals(len(expected.records), len(plan.changes))
|
||||
|
||||
# Used in a minute
|
||||
def assert_rrsets_callback(request, context):
|
||||
data = loads(request.body)
|
||||
self.assertEquals(expected_n, len(data['rrsets']))
|
||||
return ''
|
||||
|
||||
# No existing records -> creates for every record in expected
|
||||
with requests_mock() as mock:
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
mock.get(re.compile('records'), status_code=200, text=EMPTY_TEXT)
|
||||
# post 201, is reponse to the create with data
|
||||
# post 201, is response to the create with data
|
||||
mock.patch(ANY, status_code=201, text=assert_rrsets_callback)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
self.assertEquals(expected_n, len(plan.changes))
|
||||
self.assertEquals(expected_n, provider.apply(plan))
|
||||
self.assertEquals(expected_n, self.provider.apply(plan))
|
||||
|
||||
# Non-existent zone -> creates for every record in expected
|
||||
# OMG this is fucking ugly, probably better to ditch requests_mocks and
|
||||
@@ -123,12 +665,12 @@ class TestRackspaceSource(TestCase):
|
||||
mock.get(ANY, status_code=422, text='')
|
||||
# patch 422's, unknown zone
|
||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
||||
# post 201, is reponse to the create with data
|
||||
# post 201, is response to the create with data
|
||||
mock.post(ANY, status_code=201, text=assert_rrsets_callback)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
plan = self.provider.plan(expected)
|
||||
self.assertEquals(expected_n, len(plan.changes))
|
||||
self.assertEquals(expected_n, provider.apply(plan))
|
||||
self.assertEquals(expected_n, self.provider.apply(plan))
|
||||
|
||||
with requests_mock() as mock:
|
||||
# get 422's, unknown zone
|
||||
@@ -138,8 +680,8 @@ class TestRackspaceSource(TestCase):
|
||||
mock.patch(ANY, status_code=422, text=dumps(data))
|
||||
|
||||
with self.assertRaises(HTTPError) as ctx:
|
||||
plan = provider.plan(expected)
|
||||
provider.apply(plan)
|
||||
plan = self.provider.plan(expected)
|
||||
self.provider.apply(plan)
|
||||
response = ctx.exception.response
|
||||
self.assertEquals(422, response.status_code)
|
||||
self.assertTrue('error' in response.json())
|
||||
@@ -151,8 +693,8 @@ class TestRackspaceSource(TestCase):
|
||||
mock.patch(ANY, status_code=500, text='')
|
||||
|
||||
with self.assertRaises(HTTPError):
|
||||
plan = provider.plan(expected)
|
||||
provider.apply(plan)
|
||||
plan = self.provider.plan(expected)
|
||||
self.provider.apply(plan)
|
||||
|
||||
with requests_mock() as mock:
|
||||
# get 422's, unknown zone
|
||||
@@ -163,133 +705,94 @@ class TestRackspaceSource(TestCase):
|
||||
mock.post(ANY, status_code=422, text='Hello Word!')
|
||||
|
||||
with self.assertRaises(HTTPError):
|
||||
plan = provider.plan(expected)
|
||||
provider.apply(plan)
|
||||
|
||||
def test_small_change(self):
|
||||
provider = load_provider()
|
||||
plan = self.provider.plan(expected)
|
||||
self.provider.apply(plan)
|
||||
|
||||
def test_plan_no_changes(self):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
self.assertEquals(15, len(expected.records))
|
||||
|
||||
# A small change to a single record
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=200, text=FULL_TEXT)
|
||||
|
||||
missing = Zone(expected.name, [])
|
||||
# Find and delete the SPF record
|
||||
for record in expected.records:
|
||||
if record._type != 'SPF':
|
||||
missing.add_record(record)
|
||||
|
||||
def assert_delete_callback(request, context):
|
||||
self.assertEquals({
|
||||
'rrsets': [{
|
||||
'records': [
|
||||
{'content': '"v=spf1 ip4:192.168.0.1/16-all"',
|
||||
'disabled': False}
|
||||
],
|
||||
'changetype': 'DELETE',
|
||||
'type': 'SPF',
|
||||
'name': 'spf.unit.tests.',
|
||||
'ttl': 600
|
||||
}]
|
||||
}, loads(request.body))
|
||||
return ''
|
||||
|
||||
mock.patch(ANY, status_code=201, text=assert_delete_callback)
|
||||
|
||||
plan = provider.plan(missing)
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
self.assertEquals(1, provider.apply(plan))
|
||||
|
||||
def test_existing_nameservers(self):
|
||||
ns_values = ['8.8.8.8.', '9.9.9.9.']
|
||||
provider = load_provider()
|
||||
|
||||
expected = Zone('unit.tests.', [])
|
||||
ns_record = Record.new(expected, '', {
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'values': ns_values
|
||||
})
|
||||
expected.add_record(ns_record)
|
||||
'values': ['8.8.8.8.', '9.9.9.9.']
|
||||
}))
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
}))
|
||||
|
||||
# no changes
|
||||
with requests_mock() as mock:
|
||||
data = {
|
||||
'rrsets': [{
|
||||
'comments': [],
|
||||
'name': 'unit.tests.',
|
||||
'records': [
|
||||
{
|
||||
'content': '8.8.8.8.',
|
||||
'disabled': False
|
||||
},
|
||||
{
|
||||
'content': '9.9.9.9.',
|
||||
'disabled': False
|
||||
}
|
||||
],
|
||||
'ttl': 600,
|
||||
'type': 'NS'
|
||||
}, {
|
||||
'comments': [],
|
||||
'name': 'unit.tests.',
|
||||
'records': [{
|
||||
'content': '1.2.3.4',
|
||||
'disabled': False,
|
||||
}],
|
||||
'ttl': 60,
|
||||
'type': 'A'
|
||||
}]
|
||||
}
|
||||
mock.get(ANY, status_code=200, json=data)
|
||||
mock.get(re.compile('domains/.*/records'), status_code=200, text=RECORDS_EXISTING_NAMESERVERS)
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
|
||||
unrelated_record = Record.new(expected, '', {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'value': '1.2.3.4'
|
||||
})
|
||||
expected.add_record(unrelated_record)
|
||||
plan = provider.plan(expected)
|
||||
plan = self.provider.plan(expected)
|
||||
|
||||
self.assertTrue(mock.called)
|
||||
self.assertFalse(plan)
|
||||
# remove it now that we don't need the unrelated change any longer
|
||||
expected.records.remove(unrelated_record)
|
||||
|
||||
# ttl diff
|
||||
def test_plan_remove_a_record(self):
|
||||
expected = Zone('unit.tests.', [])
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'values': ['8.8.8.8.', '9.9.9.9.']
|
||||
}))
|
||||
|
||||
with requests_mock() as mock:
|
||||
data = {
|
||||
'rrsets': [{
|
||||
'comments': [],
|
||||
'name': 'unit.tests.',
|
||||
'records': [
|
||||
{
|
||||
'content': '8.8.8.8.',
|
||||
'disabled': False
|
||||
},
|
||||
{
|
||||
'content': '9.9.9.9.',
|
||||
'disabled': False
|
||||
},
|
||||
],
|
||||
'ttl': 3600,
|
||||
'type': 'NS'
|
||||
}]
|
||||
}
|
||||
mock.get(ANY, status_code=200, json=data)
|
||||
mock.get(re.compile('domains/.*/records'), status_code=200, text=RECORDS_EXISTING_NAMESERVERS)
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
plan = self.provider.plan(expected)
|
||||
self.assertTrue(mock.called)
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
self.assertEqual(plan.changes[0].existing.ttl, 60)
|
||||
self.assertEqual(plan.changes[0].existing.values[0], '1.2.3.4')
|
||||
|
||||
def test_plan_create_a_record(self):
|
||||
expected = Zone('unit.tests.', [])
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'values': ['8.8.8.8.', '9.9.9.9.']
|
||||
}))
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'A',
|
||||
'ttl': 60,
|
||||
'values': ['1.2.3.4', '1.2.3.5']
|
||||
}))
|
||||
|
||||
# create
|
||||
with requests_mock() as mock:
|
||||
data = {
|
||||
'rrsets': []
|
||||
}
|
||||
mock.get(ANY, status_code=200, json=data)
|
||||
mock.get(re.compile('domains/.*/records'), status_code=200, text=RECORDS_EXISTING_NAMESERVERS)
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
plan = self.provider.plan(expected)
|
||||
self.assertTrue(mock.called)
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
self.assertEqual(plan.changes[0].new.ttl, 60)
|
||||
self.assertEqual(plan.changes[0].new.values[0], '1.2.3.4')
|
||||
self.assertEqual(plan.changes[0].new.values[1], '1.2.3.5')
|
||||
|
||||
def test_plan_change_ttl(self):
|
||||
expected = Zone('unit.tests.', [])
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'NS',
|
||||
'ttl': 600,
|
||||
'values': ['8.8.8.8.', '9.9.9.9.']
|
||||
}))
|
||||
expected.add_record(Record.new(expected, '', {
|
||||
'type': 'A',
|
||||
'ttl': 86400,
|
||||
'value': '1.2.3.4'
|
||||
}))
|
||||
|
||||
with requests_mock() as mock:
|
||||
mock.get(re.compile('domains/.*/records'), status_code=200, text=RECORDS_EXISTING_NAMESERVERS)
|
||||
mock.get(re.compile('domains$'), status_code=200, text=LIST_DOMAINS_RESPONSE)
|
||||
|
||||
plan = self.provider.plan(expected)
|
||||
|
||||
self.assertTrue(mock.called)
|
||||
self.assertEqual(1, len(plan.changes))
|
||||
self.assertEqual(plan.changes[0].existing.ttl, 60)
|
||||
self.assertEqual(plan.changes[0].new.ttl, 86400)
|
||||
self.assertEqual(plan.changes[0].new.values[0], '1.2.3.4')
|
||||
|
||||
Reference in New Issue
Block a user