Browse Source

Working push for A records.

pull/165/head
Terrence Cole 9 years ago
parent
commit
0579ff6f2d
3 changed files with 755 additions and 248 deletions
  1. +74
    -99
      octodns/provider/rackspace.py
  2. +29
    -0
      tests/fixtures/rackspace-sample-recordset-existing-nameservers.json
  3. +652
    -149
      tests/test_octodns_source_rackspace.py

+ 74
- 99
octodns/provider/rackspace.py View File

@ -1,13 +1,10 @@
# #
# #
# #
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from requests import HTTPError, Session, post from requests import HTTPError, Session, post
import json
from collections import defaultdict from collections import defaultdict
import logging import logging
@ -40,6 +37,10 @@ class RackspaceProvider(BaseProvider):
sess.headers.update({'X-Auth-Token': auth_token}) sess.headers.update({'X-Auth-Token': auth_token})
self._sess = sess 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): def _get_auth_token(self, username, api_key):
ret = post('https://identity.api.rackspacecloud.com/v2.0/tokens', ret = post('https://identity.api.rackspacecloud.com/v2.0/tokens',
json={"auth": {"RAX-KSKEY:apiKeyCredentials": {"username": username, "apiKey": api_key}}}, json={"auth": {"RAX-KSKEY:apiKeyCredentials": {"username": username, "apiKey": api_key}}},
@ -91,9 +92,15 @@ class RackspaceProvider(BaseProvider):
def _post(self, path, data=None): def _post(self, path, data=None):
return self._request('POST', path, data=data) 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): def _patch(self, path, data=None):
return self._request('PATCH', path, data=data) 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): def _data_for_multiple(self, rrset):
# TODO: geo not supported # TODO: geo not supported
return { return {
@ -204,7 +211,7 @@ class RackspaceProvider(BaseProvider):
resp_data = None resp_data = None
try: try:
domain_id = self._get_zone_id_for(zone) 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') self.log.debug('populate: loaded')
except HTTPError as e: except HTTPError as e:
if e.response.status_code == 401: if e.response.status_code == 401:
@ -224,9 +231,9 @@ class RackspaceProvider(BaseProvider):
if resp_data: if resp_data:
records = self._group_records(resp_data) records = self._group_records(resp_data)
for record_type, records_of_type in records.items(): 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(): 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)) data_for = getattr(self, '_data_for_{}'.format(record_type))
record_name = zone.hostname_from_fqdn(raw_record_name) record_name = zone.hostname_from_fqdn(raw_record_name)
record = Record.new(zone, record_name, data_for(record_set), record = Record.new(zone, record_name, data_for(record_set),
@ -239,6 +246,7 @@ class RackspaceProvider(BaseProvider):
def _group_records(self, all_records): def _group_records(self, all_records):
records = defaultdict(lambda: defaultdict(list)) records = defaultdict(lambda: defaultdict(list))
for record in all_records: for record in all_records:
self._id_map[(record['type'], record['name'], record['data'])] = record['id']
records[record['type']][record['name']].append(record) records[record['type']][record['name']].append(record)
return records return records
@ -294,61 +302,45 @@ class RackspaceProvider(BaseProvider):
} for v in record.values] } for v in record.values]
def _mod_Create(self, change): 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)
}
_mod_Update = _mod_Create
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
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): 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): def _apply(self, plan):
desired = plan.desired desired = plan.desired
@ -356,46 +348,29 @@ class RackspaceProvider(BaseProvider):
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
len(changes)) len(changes))
mods = []
domain_id = self._get_zone_id_for(desired)
creates = []
updates = []
deletes = []
for change in changes: 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')
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')
self.log.debug('_apply: complete')
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)
if creates:
data = {"records": sorted(creates, key=lambda v: v['name'])}
self._post('domains/{}/records'.format(domain_id), data=data)
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
- 0
tests/fixtures/rackspace-sample-recordset-existing-nameservers.json View 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" : []
}

+ 652
- 149
tests/test_octodns_source_rackspace.py View File

@ -5,10 +5,11 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
import json
import re import re
from json import loads, dumps
from os.path import dirname, join from os.path import dirname, join
from unittest import TestCase from unittest import TestCase
from urlparse import urlparse
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock 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.record import Record
from octodns.zone import Zone from octodns.zone import Zone
from pprint import pprint
EMPTY_TEXT = ''' EMPTY_TEXT = '''
{ {
"totalEntries" : 6,
"totalEntries" : 0,
"records" : [] "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: with open('./tests/fixtures/rackspace-sample-recordset-page2.json') as fh:
RECORDS_PAGE_2 = fh.read() 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')
class TestRackspaceSource(TestCase):
with open('./tests/fixtures/rackspace-sample-recordset-existing-nameservers.json') as fh:
RECORDS_EXISTING_NAMESERVERS = fh.read()
def test_provider(self):
provider = load_provider()
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)
# Bad auth
def test_bad_auth(self):
with requests_mock() as mock: with requests_mock() as mock:
mock.get(ANY, status_code=401, text='Unauthorized') mock.get(ANY, status_code=401, text='Unauthorized')
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone)
self.provider.populate(zone)
self.assertTrue('unauthorized' in ctx.exception.message) self.assertTrue('unauthorized' in ctx.exception.message)
self.assertTrue(mock.called_once)
# General error
def test_server_error(self):
with requests_mock() as mock: with requests_mock() as mock:
mock.get(ANY, status_code=502, text='Things caught fire') mock.get(ANY, status_code=502, text='Things caught fire')
with self.assertRaises(HTTPError) as ctx: with self.assertRaises(HTTPError) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone)
self.provider.populate(zone)
self.assertEquals(502, ctx.exception.response.status_code) 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: with requests_mock() as mock:
mock.get(ANY, status_code=422, mock.get(ANY, status_code=422,
json={'error': "Could not find domain 'unit.tests.'"}) json={'error': "Could not find domain 'unit.tests.'"})
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone)
self.provider.populate(zone)
self.assertEquals(set(), zone.records) 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.', []) expected = Zone('unit.tests.', [])
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
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 # No diffs == no changes
with requests_mock() as mock: 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) mock.get(re.compile('records.*offset=3'), status_code=200, text=RECORDS_PAGE_2)
zone = Zone('unit.tests.', []) 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)) 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 # Used in a minute
def assert_rrsets_callback(request, context): def assert_rrsets_callback(request, context):
data = loads(request.body) data = loads(request.body)
self.assertEquals(expected_n, len(data['rrsets'])) self.assertEquals(expected_n, len(data['rrsets']))
return '' return ''
# No existing records -> creates for every record in expected
with requests_mock() as mock: 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) 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 # Non-existent zone -> creates for every record in expected
# OMG this is fucking ugly, probably better to ditch requests_mocks and # 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='') mock.get(ANY, status_code=422, text='')
# patch 422's, unknown zone # patch 422's, unknown zone
mock.patch(ANY, status_code=422, text=dumps(not_found)) 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) 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, len(plan.changes))
self.assertEquals(expected_n, provider.apply(plan))
self.assertEquals(expected_n, self.provider.apply(plan))
with requests_mock() as mock: with requests_mock() as mock:
# get 422's, unknown zone # get 422's, unknown zone
@ -138,8 +680,8 @@ class TestRackspaceSource(TestCase):
mock.patch(ANY, status_code=422, text=dumps(data)) mock.patch(ANY, status_code=422, text=dumps(data))
with self.assertRaises(HTTPError) as ctx: 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 response = ctx.exception.response
self.assertEquals(422, response.status_code) self.assertEquals(422, response.status_code)
self.assertTrue('error' in response.json()) self.assertTrue('error' in response.json())
@ -151,8 +693,8 @@ class TestRackspaceSource(TestCase):
mock.patch(ANY, status_code=500, text='') mock.patch(ANY, status_code=500, text='')
with self.assertRaises(HTTPError): 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: with requests_mock() as mock:
# get 422's, unknown zone # get 422's, unknown zone
@ -163,133 +705,94 @@ class TestRackspaceSource(TestCase):
mock.post(ANY, status_code=422, text='Hello Word!') mock.post(ANY, status_code=422, text='Hello Word!')
with self.assertRaises(HTTPError): 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.', []) expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
self.assertEquals(15, len(expected.records))
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,
'value': '1.2.3.4'
}))
# A small change to a single record
with requests_mock() as mock: 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))
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)
def test_existing_nameservers(self):
ns_values = ['8.8.8.8.', '9.9.9.9.']
provider = load_provider()
plan = self.provider.plan(expected)
self.assertTrue(mock.called)
self.assertFalse(plan)
def test_plan_remove_a_record(self):
expected = Zone('unit.tests.', []) expected = Zone('unit.tests.', [])
ns_record = Record.new(expected, '', {
expected.add_record(Record.new(expected, '', {
'type': 'NS', 'type': 'NS',
'ttl': 600, 'ttl': 600,
'values': ns_values
})
expected.add_record(ns_record)
'values': ['8.8.8.8.', '9.9.9.9.']
}))
# no changes
with requests_mock() as mock: 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)
unrelated_record = Record.new(expected, '', {
'type': 'A',
'ttl': 60,
'value': '1.2.3.4'
})
expected.add_record(unrelated_record)
plan = provider.plan(expected)
self.assertFalse(plan)
# remove it now that we don't need the unrelated change any longer
expected.records.remove(unrelated_record)
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.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']
}))
# ttl diff
with requests_mock() as mock: 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.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'
}))
# create
with requests_mock() as mock: 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)
self.assertEquals(1, len(plan.changes))
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')

Loading…
Cancel
Save