mirror of
				https://github.com/github/octodns.git
				synced 2024-05-11 05:55:00 +00:00 
			
		
		
		
	Merge pull request #33 from github/nsone-basic-support
First pass through NsOneProvider
This commit is contained in:
		
							
								
								
									
										202
									
								
								octodns/provider/ns1.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								octodns/provider/ns1.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,202 @@
 | 
			
		||||
#
 | 
			
		||||
#
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
from __future__ import absolute_import, division, print_function, \
 | 
			
		||||
    unicode_literals
 | 
			
		||||
 | 
			
		||||
from logging import getLogger
 | 
			
		||||
from nsone import NSONE
 | 
			
		||||
from nsone.rest.errors import ResourceException
 | 
			
		||||
 | 
			
		||||
from ..record import Record
 | 
			
		||||
from .base import BaseProvider
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Ns1Provider(BaseProvider):
 | 
			
		||||
    '''
 | 
			
		||||
    Ns1 provider
 | 
			
		||||
 | 
			
		||||
    nsone:
 | 
			
		||||
        class: octodns.provider.nsone.Ns1Provider
 | 
			
		||||
        api_key: env/NS1_API_KEY
 | 
			
		||||
    '''
 | 
			
		||||
    SUPPORTS_GEO = False
 | 
			
		||||
    ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, id, api_key, *args, **kwargs):
 | 
			
		||||
        self.log = getLogger('Ns1Provider[{}]'.format(id))
 | 
			
		||||
        self.log.debug('__init__: id=%s, api_key=***', id)
 | 
			
		||||
        super(Ns1Provider, self).__init__(id, *args, **kwargs)
 | 
			
		||||
        self._client = NSONE(apiKey=api_key)
 | 
			
		||||
 | 
			
		||||
    def _data_for_A(self, _type, record):
 | 
			
		||||
        return {
 | 
			
		||||
            'ttl': record['ttl'],
 | 
			
		||||
            'type': _type,
 | 
			
		||||
            'values': record['short_answers'],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    _data_for_AAAA = _data_for_A
 | 
			
		||||
    _data_for_SPF = _data_for_A
 | 
			
		||||
    _data_for_TXT = _data_for_A
 | 
			
		||||
 | 
			
		||||
    def _data_for_CNAME(self, _type, record):
 | 
			
		||||
        return {
 | 
			
		||||
            'ttl': record['ttl'],
 | 
			
		||||
            'type': _type,
 | 
			
		||||
            'value': record['short_answers'][0],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    _data_for_PTR = _data_for_CNAME
 | 
			
		||||
 | 
			
		||||
    def _data_for_MX(self, _type, record):
 | 
			
		||||
        values = []
 | 
			
		||||
        for answer in record['short_answers']:
 | 
			
		||||
            priority, value = answer.split(' ', 1)
 | 
			
		||||
            values.append({
 | 
			
		||||
                'priority': priority,
 | 
			
		||||
                'value': value,
 | 
			
		||||
            })
 | 
			
		||||
        return {
 | 
			
		||||
            'ttl': record['ttl'],
 | 
			
		||||
            'type': _type,
 | 
			
		||||
            'values': values,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def _data_for_NAPTR(self, _type, record):
 | 
			
		||||
        values = []
 | 
			
		||||
        for answer in record['short_answers']:
 | 
			
		||||
            order, preference, flags, service, regexp, replacement = \
 | 
			
		||||
                answer.split(' ', 5)
 | 
			
		||||
            values.append({
 | 
			
		||||
                'flags': flags,
 | 
			
		||||
                'order': order,
 | 
			
		||||
                'preference': preference,
 | 
			
		||||
                'regexp': regexp,
 | 
			
		||||
                'replacement': replacement,
 | 
			
		||||
                'service': service,
 | 
			
		||||
            })
 | 
			
		||||
        return {
 | 
			
		||||
            'ttl': record['ttl'],
 | 
			
		||||
            'type': _type,
 | 
			
		||||
            'values': values,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def _data_for_NS(self, _type, record):
 | 
			
		||||
        return {
 | 
			
		||||
            'ttl': record['ttl'],
 | 
			
		||||
            'type': _type,
 | 
			
		||||
            'values': [a if a.endswith('.') else '{}.'.format(a)
 | 
			
		||||
                       for a in record['short_answers']],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def _data_for_SRV(self, _type, record):
 | 
			
		||||
        values = []
 | 
			
		||||
        for answer in record['short_answers']:
 | 
			
		||||
            priority, weight, port, target = answer.split(' ', 3)
 | 
			
		||||
            values.append({
 | 
			
		||||
                'priority': priority,
 | 
			
		||||
                'weight': weight,
 | 
			
		||||
                'port': port,
 | 
			
		||||
                'target': target,
 | 
			
		||||
            })
 | 
			
		||||
        return {
 | 
			
		||||
            'ttl': record['ttl'],
 | 
			
		||||
            'type': _type,
 | 
			
		||||
            'values': values,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def populate(self, zone, target=False):
 | 
			
		||||
        self.log.debug('populate: name=%s', zone.name)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            nsone_zone = self._client.loadZone(zone.name[:-1])
 | 
			
		||||
            records = nsone_zone.data['records']
 | 
			
		||||
        except ResourceException as e:
 | 
			
		||||
            if e.message != self.ZONE_NOT_FOUND_MESSAGE:
 | 
			
		||||
                raise
 | 
			
		||||
            records = []
 | 
			
		||||
 | 
			
		||||
        before = len(zone.records)
 | 
			
		||||
        for record in records:
 | 
			
		||||
            _type = record['type']
 | 
			
		||||
            data_for = getattr(self, '_data_for_{}'.format(_type))
 | 
			
		||||
            name = zone.hostname_from_fqdn(record['domain'])
 | 
			
		||||
            record = Record.new(zone, name, data_for(_type, record))
 | 
			
		||||
            zone.add_record(record)
 | 
			
		||||
 | 
			
		||||
        self.log.info('populate:   found %s records',
 | 
			
		||||
                      len(zone.records) - before)
 | 
			
		||||
 | 
			
		||||
    def _params_for_A(self, record):
 | 
			
		||||
        return {'answers': record.values, 'ttl': record.ttl}
 | 
			
		||||
 | 
			
		||||
    _params_for_AAAA = _params_for_A
 | 
			
		||||
    _params_for_NS = _params_for_A
 | 
			
		||||
    _params_for_SPF = _params_for_A
 | 
			
		||||
    _params_for_TXT = _params_for_A
 | 
			
		||||
 | 
			
		||||
    def _params_for_CNAME(self, record):
 | 
			
		||||
        return {'answers': [record.value], 'ttl': record.ttl}
 | 
			
		||||
 | 
			
		||||
    _params_for_PTR = _params_for_CNAME
 | 
			
		||||
 | 
			
		||||
    def _params_for_MX(self, record):
 | 
			
		||||
        values = [(v.priority, v.value) for v in record.values]
 | 
			
		||||
        return {'answers': values, 'ttl': record.ttl}
 | 
			
		||||
 | 
			
		||||
    def _params_for_NAPTR(self, record):
 | 
			
		||||
        values = [(v.order, v.preference, v.flags, v.service, v.regexp,
 | 
			
		||||
                   v.replacement) for v in record.values]
 | 
			
		||||
        return {'answers': values, 'ttl': record.ttl}
 | 
			
		||||
 | 
			
		||||
    def _params_for_SRV(self, record):
 | 
			
		||||
        values = [(v.priority, v.weight, v.port, v.target)
 | 
			
		||||
                  for v in record.values]
 | 
			
		||||
        return {'answers': values, 'ttl': record.ttl}
 | 
			
		||||
 | 
			
		||||
    def _get_name(self, record):
 | 
			
		||||
        return record.fqdn[:-1] if record.name == '' else record.name
 | 
			
		||||
 | 
			
		||||
    def _apply_Create(self, nsone_zone, change):
 | 
			
		||||
        new = change.new
 | 
			
		||||
        name = self._get_name(new)
 | 
			
		||||
        _type = new._type
 | 
			
		||||
        params = getattr(self, '_params_for_{}'.format(_type))(new)
 | 
			
		||||
        getattr(nsone_zone, 'add_{}'.format(_type))(name, **params)
 | 
			
		||||
 | 
			
		||||
    def _apply_Update(self, nsone_zone, change):
 | 
			
		||||
        existing = change.existing
 | 
			
		||||
        name = self._get_name(existing)
 | 
			
		||||
        _type = existing._type
 | 
			
		||||
        record = nsone_zone.loadRecord(name, _type)
 | 
			
		||||
        new = change.new
 | 
			
		||||
        params = getattr(self, '_params_for_{}'.format(_type))(new)
 | 
			
		||||
        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):
 | 
			
		||||
        desired = plan.desired
 | 
			
		||||
        changes = plan.changes
 | 
			
		||||
        self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name,
 | 
			
		||||
                       len(changes))
 | 
			
		||||
 | 
			
		||||
        domain_name = desired.name[:-1]
 | 
			
		||||
        try:
 | 
			
		||||
            nsone_zone = self._client.loadZone(domain_name)
 | 
			
		||||
        except ResourceException as e:
 | 
			
		||||
            if e.message != self.ZONE_NOT_FOUND_MESSAGE:
 | 
			
		||||
                raise
 | 
			
		||||
            self.log.debug('_apply:   no matching zone, creating')
 | 
			
		||||
            nsone_zone = self._client.createZone(domain_name)
 | 
			
		||||
 | 
			
		||||
        for change in changes:
 | 
			
		||||
            class_name = change.__class__.__name__
 | 
			
		||||
            getattr(self, '_apply_{}'.format(class_name))(nsone_zone, change)
 | 
			
		||||
@@ -10,6 +10,7 @@ futures==3.0.5
 | 
			
		||||
incf.countryutils==1.0
 | 
			
		||||
ipaddress==1.0.18
 | 
			
		||||
jmespath==0.9.0
 | 
			
		||||
nsone==0.9.10
 | 
			
		||||
python-dateutil==2.6.0
 | 
			
		||||
requests==2.13.0
 | 
			
		||||
s3transfer==0.1.10
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										256
									
								
								tests/test_octodns_provider_ns1.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								tests/test_octodns_provider_ns1.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,256 @@
 | 
			
		||||
#
 | 
			
		||||
#
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
from __future__ import absolute_import, division, print_function, \
 | 
			
		||||
    unicode_literals
 | 
			
		||||
 | 
			
		||||
from mock import Mock, call, patch
 | 
			
		||||
from nsone.rest.errors import AuthException, ResourceException
 | 
			
		||||
from unittest import TestCase
 | 
			
		||||
 | 
			
		||||
from octodns.record import Delete, Record, Update
 | 
			
		||||
from octodns.provider.ns1 import Ns1Provider
 | 
			
		||||
from octodns.zone import Zone
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DummyZone(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, records):
 | 
			
		||||
        self.data = {
 | 
			
		||||
            'records': records
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestNs1Provider(TestCase):
 | 
			
		||||
    zone = Zone('unit.tests.', [])
 | 
			
		||||
    expected = set()
 | 
			
		||||
    expected.add(Record.new(zone, '', {
 | 
			
		||||
        'ttl': 32,
 | 
			
		||||
        'type': 'A',
 | 
			
		||||
        'value': '1.2.3.4',
 | 
			
		||||
    }))
 | 
			
		||||
    expected.add(Record.new(zone, 'foo', {
 | 
			
		||||
        'ttl': 33,
 | 
			
		||||
        'type': 'A',
 | 
			
		||||
        'values': ['1.2.3.4', '1.2.3.5'],
 | 
			
		||||
    }))
 | 
			
		||||
    expected.add(Record.new(zone, 'cname', {
 | 
			
		||||
        'ttl': 34,
 | 
			
		||||
        'type': 'CNAME',
 | 
			
		||||
        'value': 'foo.unit.tests.',
 | 
			
		||||
    }))
 | 
			
		||||
    expected.add(Record.new(zone, '', {
 | 
			
		||||
        'ttl': 35,
 | 
			
		||||
        'type': 'MX',
 | 
			
		||||
        'values': [{
 | 
			
		||||
            'priority': 10,
 | 
			
		||||
            'value': 'mx1.unit.tests.',
 | 
			
		||||
        }, {
 | 
			
		||||
            'priority': 20,
 | 
			
		||||
            'value': 'mx2.unit.tests.',
 | 
			
		||||
        }]
 | 
			
		||||
    }))
 | 
			
		||||
    expected.add(Record.new(zone, 'naptr', {
 | 
			
		||||
        'ttl': 36,
 | 
			
		||||
        'type': 'NAPTR',
 | 
			
		||||
        'values': [{
 | 
			
		||||
            'flags': 'U',
 | 
			
		||||
            'order': 100,
 | 
			
		||||
            'preference': 100,
 | 
			
		||||
            'regexp': '!^.*$!sip:info@bar.example.com!',
 | 
			
		||||
            'replacement': '.',
 | 
			
		||||
            'service': 'SIP+D2U',
 | 
			
		||||
        }, {
 | 
			
		||||
            'flags': 'S',
 | 
			
		||||
            'order': 10,
 | 
			
		||||
            'preference': 100,
 | 
			
		||||
            'regexp': '!^.*$!sip:info@bar.example.com!',
 | 
			
		||||
            'replacement': '.',
 | 
			
		||||
            'service': 'SIP+D2U',
 | 
			
		||||
        }]
 | 
			
		||||
    }))
 | 
			
		||||
    expected.add(Record.new(zone, '', {
 | 
			
		||||
        'ttl': 37,
 | 
			
		||||
        'type': 'NS',
 | 
			
		||||
        'values': ['ns1.unit.tests.', 'ns2.unit.tests.'],
 | 
			
		||||
    }))
 | 
			
		||||
    expected.add(Record.new(zone, '_srv._tcp', {
 | 
			
		||||
        'ttl': 38,
 | 
			
		||||
        'type': 'SRV',
 | 
			
		||||
        'values': [{
 | 
			
		||||
            'priority': 10,
 | 
			
		||||
            'weight': 20,
 | 
			
		||||
            'port': 30,
 | 
			
		||||
            'target': 'foo-1.unit.tests.',
 | 
			
		||||
        }, {
 | 
			
		||||
            'priority': 12,
 | 
			
		||||
            'weight': 30,
 | 
			
		||||
            'port': 30,
 | 
			
		||||
            'target': 'foo-2.unit.tests.',
 | 
			
		||||
        }]
 | 
			
		||||
    }))
 | 
			
		||||
    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])
 | 
			
		||||
 | 
			
		||||
        # 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()
 | 
			
		||||
        ])
 | 
			
		||||
		Reference in New Issue
	
	Block a user