1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

NS1, add Delete support, fix apply create, flush out tests to 100%

This commit is contained in:
Ross McFarland
2017-05-23 09:36:15 -07:00
parent 06e17d043b
commit bc1736bc39
2 changed files with 201 additions and 112 deletions

View File

@@ -22,6 +22,7 @@ class Ns1Provider(BaseProvider):
api_key: env/NS1_API_KEY api_key: env/NS1_API_KEY
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found'
def __init__(self, id, api_key, *args, **kwargs): def __init__(self, id, api_key, *args, **kwargs):
self.log = getLogger('Ns1Provider[{}]'.format(id)) self.log = getLogger('Ns1Provider[{}]'.format(id))
@@ -113,7 +114,7 @@ class Ns1Provider(BaseProvider):
nsone_zone = self._client.loadZone(zone.name[:-1]) nsone_zone = self._client.loadZone(zone.name[:-1])
records = nsone_zone.data['records'] records = nsone_zone.data['records']
except ResourceException as e: except ResourceException as e:
if e.message != 'server error: zone not found': if e.message != self.ZONE_NOT_FOUND_MESSAGE:
raise raise
records = [] records = []
@@ -174,6 +175,13 @@ class Ns1Provider(BaseProvider):
params = getattr(self, '_params_for_{}'.format(_type))(new) params = getattr(self, '_params_for_{}'.format(_type))(new)
record.update(**params) record.update(**params)
def _apply_Delete(self, nsone_zone, change):
existing = change.existing
name = self._get_name(existing)
_type = existing._type
record = nsone_zone.loadRecord(name, _type)
record.delete()
def _apply(self, plan): def _apply(self, plan):
desired = plan.desired desired = plan.desired
changes = plan.changes changes = plan.changes
@@ -183,7 +191,9 @@ class Ns1Provider(BaseProvider):
domain_name = desired.name[:-1] domain_name = desired.name[:-1]
try: try:
nsone_zone = self._client.loadZone(domain_name) nsone_zone = self._client.loadZone(domain_name)
except ResourceException: except ResourceException as e:
if e.message != self.ZONE_NOT_FOUND_MESSAGE:
raise
self.log.debug('_apply: no matching zone, creating') self.log.debug('_apply: no matching zone, creating')
nsone_zone = self._client.createZone(domain_name) nsone_zone = self._client.createZone(domain_name)

View File

@@ -5,11 +5,11 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from mock import patch from mock import Mock, call, patch
from nsone.rest.errors import AuthException, ResourceException from nsone.rest.errors import AuthException, ResourceException
from unittest import TestCase from unittest import TestCase
from octodns.record import Record from octodns.record import Delete, Record, Update
from octodns.provider.ns1 import Ns1Provider from octodns.provider.ns1 import Ns1Provider
from octodns.zone import Zone from octodns.zone import Zone
@@ -23,90 +23,7 @@ class DummyZone(object):
class TestNs1Provider(TestCase): class TestNs1Provider(TestCase):
@patch('nsone.NSONE.loadZone')
def test_provider(self, load_mock):
provider = Ns1Provider('test', 'api-key')
# Bad auth
load_mock.side_effect = AuthException('unauthorized')
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
with self.assertRaises(AuthException) as ctx:
provider.populate(zone)
self.assertEquals(load_mock.side_effect, ctx.exception)
# General error
load_mock.reset_mock()
load_mock.side_effect = ResourceException('boom')
zone = Zone('unit.tests.', [])
with self.assertRaises(ResourceException) as ctx:
provider.populate(zone)
self.assertEquals(load_mock.side_effect, ctx.exception)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Non-existant zone doesn't populate anything
load_mock.reset_mock()
load_mock.side_effect = \
ResourceException('server error: zone not found')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Existing zone w/o records
load_mock.reset_mock()
nsone_zone = DummyZone([])
load_mock.side_effect = [nsone_zone]
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Existing zone w/records
load_mock.reset_mock()
nsone_zone = DummyZone([{
'type': 'A',
'ttl': 32,
'short_answers': ['1.2.3.4'],
'domain': 'unit.tests.',
}, {
'type': 'A',
'ttl': 33,
'short_answers': ['1.2.3.4', '1.2.3.5'],
'domain': 'foo.unit.tests.',
}, {
'type': 'CNAME',
'ttl': 34,
'short_answers': ['foo.unit.tests.'],
'domain': 'cname.unit.tests.',
}, {
'type': 'MX',
'ttl': 35,
'short_answers': ['10 mx1.unit.tests.', '20 mx2.unit.tests.'],
'domain': 'unit.tests.',
}, {
'type': 'NAPTR',
'ttl': 36,
'short_answers': [
'10 100 S SIP+D2U !^.*$!sip:info@bar.example.com! .',
'100 100 U SIP+D2U !^.*$!sip:info@bar.example.com! .'
],
'domain': 'naptr.unit.tests.',
}, {
'type': 'NS',
'ttl': 37,
'short_answers': ['ns1.unit.tests.', 'ns2.unit.tests.'],
'domain': 'unit.tests.',
}, {
'type': 'SRV',
'ttl': 38,
'short_answers': ['12 20 30 foo-2.unit.tests.',
'10 20 30 foo-2.unit.tests.'],
'domain': '_srv._tcp.unit.tests.',
}])
load_mock.side_effect = [nsone_zone]
zone = Zone('unit.tests.', [])
provider.populate(zone)
expected = set() expected = set()
expected.add(Record.new(zone, '', { expected.add(Record.new(zone, '', {
'ttl': 32, 'ttl': 32,
@@ -114,12 +31,12 @@ class TestNs1Provider(TestCase):
'value': '1.2.3.4', 'value': '1.2.3.4',
})) }))
expected.add(Record.new(zone, 'foo', { expected.add(Record.new(zone, 'foo', {
'ttl': 32, 'ttl': 33,
'type': 'A', 'type': 'A',
'values': ['1.2.3.4', '1.2.3.5'], 'values': ['1.2.3.4', '1.2.3.5'],
})) }))
expected.add(Record.new(zone, 'cname', { expected.add(Record.new(zone, 'cname', {
'ttl': 33, 'ttl': 34,
'type': 'CNAME', 'type': 'CNAME',
'value': 'foo.unit.tests.', 'value': 'foo.unit.tests.',
})) }))
@@ -173,5 +90,167 @@ class TestNs1Provider(TestCase):
'target': 'foo-2.unit.tests.', 'target': 'foo-2.unit.tests.',
}] }]
})) }))
self.assertEquals(expected, zone.records) expected.add(Record.new(zone, 'sub', {
'ttl': 39,
'type': 'NS',
'values': ['ns3.unit.tests.', 'ns4.unit.tests.'],
}))
nsone_records = [{
'type': 'A',
'ttl': 32,
'short_answers': ['1.2.3.4'],
'domain': 'unit.tests.',
}, {
'type': 'A',
'ttl': 33,
'short_answers': ['1.2.3.4', '1.2.3.5'],
'domain': 'foo.unit.tests.',
}, {
'type': 'CNAME',
'ttl': 34,
'short_answers': ['foo.unit.tests.'],
'domain': 'cname.unit.tests.',
}, {
'type': 'MX',
'ttl': 35,
'short_answers': ['10 mx1.unit.tests.', '20 mx2.unit.tests.'],
'domain': 'unit.tests.',
}, {
'type': 'NAPTR',
'ttl': 36,
'short_answers': [
'10 100 S SIP+D2U !^.*$!sip:info@bar.example.com! .',
'100 100 U SIP+D2U !^.*$!sip:info@bar.example.com! .'
],
'domain': 'naptr.unit.tests.',
}, {
'type': 'NS',
'ttl': 37,
'short_answers': ['ns1.unit.tests.', 'ns2.unit.tests.'],
'domain': 'unit.tests.',
}, {
'type': 'SRV',
'ttl': 38,
'short_answers': ['12 30 30 foo-2.unit.tests.',
'10 20 30 foo-1.unit.tests.'],
'domain': '_srv._tcp.unit.tests.',
}, {
'type': 'NS',
'ttl': 39,
'short_answers': ['ns3.unit.tests.', 'ns4.unit.tests.'],
'domain': 'sub.unit.tests.',
}]
@patch('nsone.NSONE.loadZone')
def test_populate(self, load_mock):
provider = Ns1Provider('test', 'api-key')
# Bad auth
load_mock.side_effect = AuthException('unauthorized')
zone = Zone('unit.tests.', [])
with self.assertRaises(AuthException) as ctx:
provider.populate(zone)
self.assertEquals(load_mock.side_effect, ctx.exception)
# General error
load_mock.reset_mock()
load_mock.side_effect = ResourceException('boom')
zone = Zone('unit.tests.', [])
with self.assertRaises(ResourceException) as ctx:
provider.populate(zone)
self.assertEquals(load_mock.side_effect, ctx.exception)
self.assertEquals(('unit.tests',), load_mock.call_args[0]) self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Non-existant zone doesn't populate anything
load_mock.reset_mock()
load_mock.side_effect = \
ResourceException('server error: zone not found')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Existing zone w/o records
load_mock.reset_mock()
nsone_zone = DummyZone([])
load_mock.side_effect = [nsone_zone]
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
# Existing zone w/records
load_mock.reset_mock()
nsone_zone = DummyZone(self.nsone_records)
load_mock.side_effect = [nsone_zone]
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(self.expected, zone.records)
self.assertEquals(('unit.tests',), load_mock.call_args[0])
@patch('nsone.NSONE.createZone')
@patch('nsone.NSONE.loadZone')
def test_sync(self, load_mock, create_mock):
provider = Ns1Provider('test', 'api-key')
desired = Zone('unit.tests.', [])
desired.records.update(self.expected)
plan = provider.plan(desired)
# everything except the root NS
expected_n = len(self.expected) - 1
self.assertEquals(expected_n, len(plan.changes))
# Fails, general error
load_mock.reset_mock()
create_mock.reset_mock()
load_mock.side_effect = ResourceException('boom')
with self.assertRaises(ResourceException) as ctx:
provider.apply(plan)
self.assertEquals(load_mock.side_effect, ctx.exception)
# Fails, bad auth
load_mock.reset_mock()
create_mock.reset_mock()
load_mock.side_effect = \
ResourceException('server error: zone not found')
create_mock.side_effect = AuthException('unauthorized')
with self.assertRaises(AuthException) as ctx:
provider.apply(plan)
self.assertEquals(create_mock.side_effect, ctx.exception)
# non-existant zone, create
load_mock.reset_mock()
create_mock.reset_mock()
load_mock.side_effect = \
ResourceException('server error: zone not found')
create_mock.side_effect = None
got_n = provider.apply(plan)
self.assertEquals(expected_n, got_n)
# Update & delete
load_mock.reset_mock()
create_mock.reset_mock()
nsone_zone = DummyZone(self.nsone_records + [{
'type': 'A',
'ttl': 42,
'short_answers': ['9.9.9.9'],
'domain': 'delete-me.unit.tests.',
}])
nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2'
nsone_zone.loadRecord = Mock()
load_mock.side_effect = [nsone_zone, nsone_zone]
plan = provider.plan(desired)
self.assertEquals(2, len(plan.changes))
self.assertIsInstance(plan.changes[0], Update)
self.assertIsInstance(plan.changes[1], Delete)
got_n = provider.apply(plan)
self.assertEquals(2, got_n)
nsone_zone.loadRecord.assert_has_calls([
call('unit.tests', u'A'),
call().update(answers=[u'1.2.3.4'], ttl=32),
call('delete-me', u'A'),
call().delete()
])