예제 #1
0
    def test_A_and_values_mixin(self):
        # doesn't blow up
        Record.new(self.zone, '', {
            'type': 'A',
            'ttl': 600,
            'value': '1.2.3.4',
        })
        Record.new(self.zone, '', {
            'type': 'A',
            'ttl': 600,
            'values': [
                '1.2.3.4',
                '1.2.3.5',
            ]
        })

        # missing value(s)
        with self.assertRaises(ValidationError) as ctx:
            Record.new(self.zone, '', {
                'type': 'A',
                'ttl': 600,
            })
        self.assertEquals(['missing value(s)'], ctx.exception.reasons)
        # missing value(s) & ttl
        with self.assertRaises(ValidationError) as ctx:
            Record.new(self.zone, '', {
                'type': 'A',
            })
        self.assertEquals(['missing ttl', 'missing value(s)'],
                          ctx.exception.reasons)

        # invalid ip address
        with self.assertRaises(ValidationError) as ctx:
            Record.new(self.zone, '', {
                'type': 'A',
                'ttl': 600,
                'value': 'hello'
            })
        self.assertEquals(['invalid ip address "hello"'],
                          ctx.exception.reasons)

        # invalid ip addresses
        with self.assertRaises(ValidationError) as ctx:
            Record.new(self.zone, '', {
                'type': 'A',
                'ttl': 600,
                'values': ['hello', 'goodbye']
            })
        self.assertEquals(
            ['invalid ip address "hello"', 'invalid ip address "goodbye"'],
            ctx.exception.reasons)

        # invalid & valid ip addresses, no ttl
        with self.assertRaises(ValidationError) as ctx:
            Record.new(self.zone, '', {
                'type': 'A',
                'values': ['1.2.3.4', 'hello', '5.6.7.8']
            })
        self.assertEquals([
            'missing ttl',
            'invalid ip address "hello"',
        ], ctx.exception.reasons)
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': [{
                    'preference': 10,
                    'exchange': 'mx1.unit.tests.',
                }, {
                    'preference': 20,
                    'exchange': 'mx2.unit.tests.',
                }]
            }))
    expected.add(
        Record.new(
            zone, 'naptr', {
                'ttl':
                36,
                'type':
                'NAPTR',
                'values': [{
                    'flags': 'U',
                    'order': 100,
                    'preference': 100,
                    'regexp': '!^.*$!sip:[email protected]!',
                    'replacement': '.',
                    'service': 'SIP+D2U',
                }, {
                    'flags': 'S',
                    'order': 10,
                    'preference': 100,
                    'regexp': '!^.*$!sip:[email protected]!',
                    '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.'],
            }))
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 40,
                'type': 'CAA',
                'value': {
                    'flags': 0,
                    'tag': 'issue',
                    'value': 'ca.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:[email protected]! .',
            '100 100 U SIP+D2U !^.*$!sip:[email protected]! .'
        ],
        '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.',
    }, {
        'type': 'CAA',
        'ttl': 40,
        'short_answers': ['0 issue ca.unit.tests'],
        'domain': '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.', [])
        for r in self.expected:
            desired.add_record(r)

        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')
        # ugh, need a mock zone with a mock prop since we're using getattr, we
        # can actually control side effects on `meth` with that.
        mock_zone = Mock()
        mock_zone.add_SRV = Mock()
        mock_zone.add_SRV.side_effect = [
            RateLimitException('boo', period=0),
            None,
        ]
        create_mock.side_effect = [mock_zone]
        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)
        # ugh, we need a mock record that can be returned from loadRecord for
        # the update and delete targets, we can add our side effects to that to
        # trigger rate limit handling
        mock_record = Mock()
        mock_record.update.side_effect = [
            RateLimitException('one', period=0),
            None,
        ]
        mock_record.delete.side_effect = [
            RateLimitException('two', period=0),
            None,
        ]
        nsone_zone.loadRecord.side_effect = [mock_record, mock_record]
        got_n = provider.apply(plan)
        self.assertEquals(2, got_n)
        nsone_zone.loadRecord.assert_has_calls([
            call('unit.tests', u'A'),
            call('delete-me', u'A'),
        ])
        mock_record.assert_has_calls(
            [call.update(answers=[u'1.2.3.4'], ttl=32),
             call.delete()])

    def test_escaping(self):
        provider = Ns1Provider('test', 'api-key')

        record = {'ttl': 31, 'short_answers': ['foo; bar baz; blip']}
        self.assertEquals(['foo\; bar baz\; blip'],
                          provider._data_for_SPF('SPF', record)['values'])

        record = {
            'ttl': 31,
            'short_answers': ['no', 'foo; bar baz; blip', 'yes']
        }
        self.assertEquals(['no', 'foo\; bar baz\; blip', 'yes'],
                          provider._data_for_TXT('TXT', record)['values'])

        zone = Zone('unit.tests.', [])
        record = Record.new(zone, 'spf', {
            'ttl': 34,
            'type': 'SPF',
            'value': 'foo\; bar baz\; blip'
        })
        self.assertEquals(['foo; bar baz; blip'],
                          provider._params_for_SPF(record)['answers'])

        record = Record.new(zone, 'txt', {
            'ttl': 35,
            'type': 'TXT',
            'value': 'foo\; bar baz\; blip'
        })
        self.assertEquals(['foo; bar baz; blip'],
                          provider._params_for_TXT(record)['answers'])
예제 #3
0
    def test_apply(self):
        # Create provider with sandbox enabled
        provider = DnsMadeEasyProvider('test', 'api', 'secret', True)

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        with open('tests/fixtures/dnsmadeeasy-domains.json') as fh:
            domains = json.load(fh)

        # non-existent domain, create everything
        resp.json.side_effect = [
            DnsMadeEasyClientNotFound,  # no zone in populate
            DnsMadeEasyClientNotFound,  # no domain during apply
            domains
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 10
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST', '/', data={'name': 'unit.tests'}),
            # get all domains to build the cache
            call('GET', '/'),
            # created at least some of the record with expected data
            call('POST', '/123123/records', data={
                'type': 'A',
                'name': '',
                'value': '1.2.3.4',
                'ttl': 300}),
            call('POST', '/123123/records', data={
                'type': 'A',
                'name': '',
                'value': '1.2.3.5',
                'ttl': 300}),
            call('POST', '/123123/records', data={
                'type': 'ANAME',
                'name': '',
                'value': 'aname.unit.tests.',
                'ttl': 1800}),
            call('POST', '/123123/records', data={
                'name': '',
                'value': 'ca.unit.tests',
                'issuerCritical': 0, 'caaType': 'issue',
                'ttl': 3600, 'type': 'CAA'}),
            call('POST', '/123123/records', data={
                'name': '_srv._tcp',
                'weight': 20,
                'value': 'foo-1.unit.tests.',
                'priority': 10,
                'ttl': 600,
                'type': 'SRV',
                'port': 30
            }),
        ])
        self.assertEquals(26, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.records = Mock(return_value=[
            {
                'id': 11189897,
                'name': 'www',
                'value': '1.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189898,
                'name': 'www',
                'value': '2.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189899,
                'name': 'ttl',
                'value': '3.2.3.4',
                'ttl': 600,
                'type': 'A',
            }
        ])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(Record.new(wanted, 'ttl', {
            'ttl': 300,
            'type': 'A',
            'value': '3.2.3.4'
        }))

        plan = provider.plan(wanted)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))

        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST', '/123123/records', data={
                'value': '3.2.3.4',
                'type': 'A',
                'name': 'ttl',
                'ttl': 300
            }),
            call('DELETE', '/123123/records/11189899'),
            call('DELETE', '/123123/records/11189897'),
            call('DELETE', '/123123/records/11189898')
        ], any_order=True)
예제 #4
0
    def test_apply(self):
        provider = HetznerProvider('test', 'token')

        resp = Mock()
        resp.json = Mock()
        provider._client._do = Mock(return_value=resp)

        domain_after_creation = {'zone': {
            'id': 'unit.tests',
            'name': 'unit.tests',
            'ttl': 3600,
        }}

        # non-existent domain, create everything
        resp.json.side_effect = [
            HetznerClientNotFound,  # no zone in populate
            HetznerClientNotFound,  # no zone during apply
            domain_after_creation,
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 9
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._client._do.assert_has_calls([
            # created the zone
            call('POST', '/zones', None, {
                'name': 'unit.tests',
                'ttl': None,
            }),
            # created all the records with their expected data
            call('POST', '/records', data={
                'name': '@',
                'ttl': 300,
                'type': 'A',
                'value': '1.2.3.4',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': '@',
                'ttl': 300,
                'type': 'A',
                'value': '1.2.3.5',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': '@',
                'ttl': 3600,
                'type': 'CAA',
                'value': '0 issue "ca.unit.tests"',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': '_imap._tcp',
                'ttl': 600,
                'type': 'SRV',
                'value': '0 0 0 .',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': '_pop3._tcp',
                'ttl': 600,
                'type': 'SRV',
                'value': '0 0 0 .',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': '_srv._tcp',
                'ttl': 600,
                'type': 'SRV',
                'value': '10 20 30 foo-1.unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': '_srv._tcp',
                'ttl': 600,
                'type': 'SRV',
                'value': '12 20 30 foo-2.unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'aaaa',
                'ttl': 600,
                'type': 'AAAA',
                'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'cname',
                'ttl': 300,
                'type': 'CNAME',
                'value': 'unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'included',
                'ttl': 3600,
                'type': 'CNAME',
                'value': 'unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'mx',
                'ttl': 300,
                'type': 'MX',
                'value': '10 smtp-4.unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'mx',
                'ttl': 300,
                'type': 'MX',
                'value': '20 smtp-2.unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'mx',
                'ttl': 300,
                'type': 'MX',
                'value': '30 smtp-3.unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'mx',
                'ttl': 300,
                'type': 'MX',
                'value': '40 smtp-1.unit.tests.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'sub',
                'ttl': 3600,
                'type': 'NS',
                'value': '6.2.3.4.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'sub',
                'ttl': 3600,
                'type': 'NS',
                'value': '7.2.3.4.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'txt',
                'ttl': 600,
                'type': 'TXT',
                'value': 'Bah bah black sheep',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'txt',
                'ttl': 600,
                'type': 'TXT',
                'value': 'have you any wool.',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'txt',
                'ttl': 600,
                'type': 'TXT',
                'value': 'v=DKIM1;k=rsa;s=email;h=sha256;'
                         'p=A/kinda+of/long/string+with+numb3rs',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'www',
                'ttl': 300,
                'type': 'A',
                'value': '2.2.3.6',
                'zone_id': 'unit.tests',
            }),
            call('POST', '/records', data={
                'name': 'www.sub',
                'ttl': 300,
                'type': 'A',
                'value': '2.2.3.6',
                'zone_id': 'unit.tests',
            }),
        ])
        self.assertEquals(24, provider._client._do.call_count)

        provider._client._do.reset_mock()

        # delete 1 and update 1
        provider._client.zone_get = Mock(return_value={
            'id': 'unit.tests',
            'name': 'unit.tests',
            'ttl': 3600,
        })
        provider._client.zone_records_get = Mock(return_value=[
            {
                'type': 'A',
                'id': 'one',
                'created': '0000-00-00T00:00:00Z',
                'modified': '0000-00-00T00:00:00Z',
                'zone_id': 'unit.tests',
                'name': 'www',
                'value': '1.2.3.4',
                'ttl': 300,
            },
            {
                'type': 'A',
                'id': 'two',
                'created': '0000-00-00T00:00:00Z',
                'modified': '0000-00-00T00:00:00Z',
                'zone_id': 'unit.tests',
                'name': 'www',
                'value': '2.2.3.4',
                'ttl': 300,
            },
            {
                'type': 'A',
                'id': 'three',
                'created': '0000-00-00T00:00:00Z',
                'modified': '0000-00-00T00:00:00Z',
                'zone_id': 'unit.tests',
                'name': 'ttl',
                'value': '3.2.3.4',
                'ttl': 600,
            },
        ])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(Record.new(wanted, 'ttl', {
            'ttl': 300,
            'type': 'A',
            'value': '3.2.3.4',
        }))

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._do.assert_has_calls([
            call('POST', '/records', data={
                'name': 'ttl',
                'ttl': 300,
                'type': 'A',
                'value': '3.2.3.4',
                'zone_id': 'unit.tests',
            }),
            call('DELETE', '/records/one'),
            call('DELETE', '/records/two'),
            call('DELETE', '/records/three'),
        ], any_order=True)
예제 #5
0
class TestDnsimpleProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

    # Our test suite differs a bit, add our NS and remove the simple one
    expected.add_record(Record.new(expected, 'under', {
        'ttl': 3600,
        'type': 'NS',
        'values': [
            'ns1.unit.tests.',
            'ns2.unit.tests.',
        ]
    }))
    for record in list(expected.records):
        if record.name == 'sub' and record._type == 'NS':
            expected._remove_record(record)
            break

    def test_populate(self):
        provider = DnsimpleProvider('test', 'token', 42)

        # Bad auth
        with requests_mock() as mock:
            mock.get(ANY, status_code=401,
                     text='{"message": "Authentication failed"}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('Unauthorized', ctx.exception.message)

        # General error
        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.assertEquals(502, ctx.exception.response.status_code)

        # Non-existant zone doesn't populate anything
        with requests_mock() as mock:
            mock.get(ANY, status_code=404,
                     text='{"message": "Domain `foo.bar` not found"}')

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        # No diffs == no changes
        with requests_mock() as mock:
            base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \
                'records?page='
            with open('tests/fixtures/dnsimple-page-1.json') as fh:
                mock.get('{}{}'.format(base, 1), text=fh.read())
            with open('tests/fixtures/dnsimple-page-2.json') as fh:
                mock.get('{}{}'.format(base, 2), text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(16, len(zone.records))
            changes = self.expected.changes(zone, provider)
            self.assertEquals(0, len(changes))

        # 2nd populate makes no network calls/all from cache
        again = Zone('unit.tests.', [])
        provider.populate(again)
        self.assertEquals(16, len(again.records))

        # bust the cache
        del provider._zone_records[zone.name]

        # test handling of invalid content
        with requests_mock() as mock:
            with open('tests/fixtures/dnsimple-invalid-content.json') as fh:
                mock.get(ANY, text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone, lenient=True)
            self.assertEquals(set([
                Record.new(zone, '', {
                    'ttl': 3600,
                    'type': 'SSHFP',
                    'values': []
                }, lenient=True),
                Record.new(zone, '_srv._tcp', {
                    'ttl': 600,
                    'type': 'SRV',
                    'values': []
                }, lenient=True),
                Record.new(zone, 'naptr', {
                    'ttl': 600,
                    'type': 'NAPTR',
                    'values': []
                }, lenient=True),
            ]), zone.records)

    def test_apply(self):
        provider = DnsimpleProvider('test', 'token', 42)

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        # non-existant domain, create everything
        resp.json.side_effect = [
            DnsimpleClientNotFound,  # no zone in populate
            DnsimpleClientNotFound,  # no domain during apply
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded
        n = len(self.expected.records) - 3
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST', '/domains', data={'name': 'unit.tests'}),
            # created at least one of the record with expected data
            call('POST', '/zones/unit.tests/records', data={
                'content': '20 30 foo-1.unit.tests.',
                'priority': 10,
                'type': 'SRV',
                'name': '_srv._tcp',
                'ttl': 600
            }),
        ])
        # expected number of total calls
        self.assertEquals(28, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.records = Mock(return_value=[
            {
                'id': 11189897,
                'name': 'www',
                'content': '1.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189898,
                'name': 'www',
                'content': '2.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189899,
                'name': 'ttl',
                'content': '3.2.3.4',
                'ttl': 600,
                'type': 'A',
            }
        ])
        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(Record.new(wanted, 'ttl', {
            'ttl': 300,
            'type': 'A',
            'value': '3.2.3.4'
        }))

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST', '/zones/unit.tests/records', data={
                'content': '3.2.3.4',
                'type': 'A',
                'name': 'ttl',
                'ttl': 300
            }),
            call('DELETE', '/zones/unit.tests/records/11189899'),
            call('DELETE', '/zones/unit.tests/records/11189897'),
            call('DELETE', '/zones/unit.tests/records/11189898')
        ], any_order=True)
    def test_cdn(self):
        provider = CloudflareProvider('test', 'email', 'token', True)

        # A CNAME for us to transform to ALIAS
        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "CNAME",
                "name": "cname.unit.tests",
                "content": "www.unit.tests",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "A",
                "name": "a.unit.tests",
                "content": "1.1.1.1",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "A",
                "name": "a.unit.tests",
                "content": "1.1.1.2",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "A",
                "name": "multi.unit.tests",
                "content": "1.1.1.3",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "AAAA",
                "name": "multi.unit.tests",
                "content": "::1",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        zone = Zone('unit.tests.', [])
        provider.populate(zone)

        # the two A records get merged into one CNAME record pointing to
        # the CDN.
        self.assertEquals(3, len(zone.records))

        record = list(zone.records)[0]
        self.assertEquals('multi', record.name)
        self.assertEquals('multi.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)

        record = list(zone.records)[1]
        self.assertEquals('cname', record.name)
        self.assertEquals('cname.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('cname.unit.tests.cdn.cloudflare.net.', record.value)

        record = list(zone.records)[2]
        self.assertEquals('a', record.name)
        self.assertEquals('a.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value)

        # CDN enabled records can't be updated, we don't know the real values
        # never point a Cloudflare record to itself.
        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(
                wanted, 'cname', {
                    'ttl': 300,
                    'type': 'CNAME',
                    'value': 'change.unit.tests.cdn.cloudflare.net.'
                }))
        wanted.add_record(
            Record.new(
                wanted, 'new', {
                    'ttl': 300,
                    'type': 'CNAME',
                    'value': 'new.unit.tests.cdn.cloudflare.net.'
                }))
        wanted.add_record(
            Record.new(wanted, 'created', {
                'ttl': 300,
                'type': 'CNAME',
                'value': 'www.unit.tests.'
            }))

        plan = provider.plan(wanted)
        self.assertEquals(1, len(plan.changes))
예제 #7
0
    def test_apply(self):
        provider = ConstellixProvider('test', 'api', 'secret')

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        # non-existent domain, create everything
        resp.json.side_effect = [
            [],  # no domains returned during populate
            [{
                'id': 123123,
                'name': 'unit.tests'
            }],  # domain created in apply
        ]

        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 6
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))

        provider._client._request.assert_has_calls([
            # get all domains to build the cache
            call('GET', ''),
            # created the domain
            call('POST', '/', data={'names': ['unit.tests']})
        ])
        # These two checks are broken up so that ordering doesn't break things.
        # Python3 doesn't make the calls in a consistent order so different
        # things follow the GET / on different runs
        provider._client._request.assert_has_calls([
            call('POST', '/123123/records/SRV', data={
                'roundRobin': [{
                    'priority': 10,
                    'weight': 20,
                    'value': 'foo-1.unit.tests.',
                    'port': 30
                }, {
                    'priority': 12,
                    'weight': 20,
                    'value': 'foo-2.unit.tests.',
                    'port': 30
                }],
                'name': '_srv._tcp',
                'ttl': 600,
            }),
        ])

        self.assertEquals(18, provider._client._request.call_count)

        provider._client._request.reset_mock()

        provider._client.records = Mock(return_value=[
            {
                'id': 11189897,
                'type': 'A',
                'name': 'www',
                'ttl': 300,
                'value': [
                    '1.2.3.4',
                    '2.2.3.4',
                ]
            }, {
                'id': 11189898,
                'type': 'A',
                'name': 'ttl',
                'ttl': 600,
                'value': [
                    '3.2.3.4'
                ]
            },  {
                'id': 11189899,
                'type': 'ALIAS',
                'name': 'alias',
                'ttl': 600,
                'value': [{
                    'value': 'aname.unit.tests.'
                }]
            }
        ])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(Record.new(wanted, 'ttl', {
            'ttl': 300,
            'type': 'A',
            'value': '3.2.3.4'
        }))

        plan = provider.plan(wanted)
        self.assertEquals(3, len(plan.changes))
        self.assertEquals(3, provider.apply(plan))

        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST', '/123123/records/A', data={
                'roundRobin': [{
                    'value': '3.2.3.4'
                }],
                'name': 'ttl',
                'ttl': 300
            }),
            call('DELETE', '/123123/records/A/11189897'),
            call('DELETE', '/123123/records/A/11189898'),
            call('DELETE', '/123123/records/ANAME/11189899')
        ], any_order=True)
예제 #8
0
class TestDynProviderAlias(TestCase):
    expected = Zone('unit.tests.', [])
    for name, data in (('', {
            'type': 'ALIAS',
            'ttl': 300,
            'value': 'www.unit.tests.'
    }), ('www', {
            'type': 'A',
            'ttl': 300,
            'values': ['1.2.3.4']
    })):
        expected.add_record(Record.new(expected, name, data))

    def setUp(self):
        # Flush our zone to ensure we start fresh
        _CachingDynZone.flush_zone(self.expected.name[:-1])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # get Zone
            {
                'data': {}
            },
            # get_all_records
            {
                'data': {
                    'a_records': [{
                        'fqdn': 'www.unit.tests',
                        'rdata': {
                            'address': '1.2.3.4'
                        },
                        'record_id': 1,
                        'record_type': 'A',
                        'ttl': 300,
                        'zone': 'unit.tests',
                    }],
                    'alias_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'alias': 'www.unit.tests.'
                        },
                        'record_id': 2,
                        'record_type': 'ALIAS',
                        'ttl': 300,
                        'zone': 'unit.tests',
                    }],
                }
            }
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        changes = self.expected.changes(got, SimpleProvider())
        self.assertEquals([], changes)

    @patch('dyn.core.SessionEngine.execute')
    def test_sync(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # No such zone, during populate
            DynectGetError('foo'),
            # No such zone, during sync
            DynectGetError('foo'),
            # get empty Zone
            {
                'data': {}
            },
            # get zone we can modify & delete with
            {
                'data': {
                    # A top-level to delete
                    'a_records': [{
                        'fqdn': 'www.unit.tests',
                        'rdata': {
                            'address': '1.2.3.4'
                        },
                        'record_id': 1,
                        'record_type': 'A',
                        'ttl': 300,
                        'zone': 'unit.tests',
                    }],
                    # A node to delete
                    'alias_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'alias': 'www.unit.tests.'
                        },
                        'record_id': 2,
                        'record_type': 'ALIAS',
                        'ttl': 300,
                        'zone': 'unit.tests',
                    }],
                }
            }
        ]

        # No existing records, create all
        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.zones.Zone._update') as update_mock:
                plan = provider.plan(self.expected)
                update_mock.assert_not_called()
                provider.apply(plan)
                update_mock.assert_called()
            add_mock.assert_called()
            # Once for each dyn record
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/Zone/unit.tests/', 'GET', {})
        ])
        self.assertEquals(2, len(plan.changes))
예제 #9
0
class TestDynProvider(TestCase):
    expected = Zone('unit.tests.', [])
    for name, data in (('', {
            'type': 'A',
            'ttl': 300,
            'values': ['1.2.3.4']
    }), ('cname', {
            'type': 'CNAME',
            'ttl': 301,
            'value': 'unit.tests.'
    }), ('', {
            'type':
            'MX',
            'ttl':
            302,
            'values': [{
                'preference': 10,
                'exchange': 'smtp-1.unit.tests.'
            }, {
                'preference': 20,
                'exchange': 'smtp-2.unit.tests.'
            }]
    }), ('naptr', {
            'type':
            'NAPTR',
            'ttl':
            303,
            'values': [{
                'order': 100,
                'preference': 101,
                'flags': 'U',
                'service': 'SIP+D2U',
                'regexp': '!^.*$!sip:[email protected]!',
                'replacement': '.',
            }, {
                'order': 200,
                'preference': 201,
                'flags': 'U',
                'service': 'SIP+D2U',
                'regexp': '!^.*$!sip:[email protected]!',
                'replacement': '.',
            }]
    }), ('sub', {
            'type': 'NS',
            'ttl': 3600,
            'values': ['ns3.p10.dynect.net.', 'ns3.p10.dynect.net.'],
    }), ('ptr', {
            'type': 'PTR',
            'ttl': 304,
            'value': 'xx.unit.tests.'
    }), ('spf', {
            'type': 'SPF',
            'ttl': 305,
            'values': ['v=spf1 ip4:192.168.0.1/16-all', 'v=spf1 -all'],
    }), ('', {
            'type': 'SSHFP',
            'ttl': 306,
            'value': {
                'algorithm': 1,
                'fingerprint_type': 1,
                'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73',
            }
    }), ('_srv._tcp', {
            'type':
            'SRV',
            'ttl':
            307,
            'values': [{
                'priority': 11,
                'weight': 12,
                'port': 10,
                'target': 'foo-1.unit.tests.'
            }, {
                'priority': 21,
                'weight': 22,
                'port': 20,
                'target': 'foo-2.unit.tests.'
            }]
    })):
        expected.add_record(Record.new(expected, name, data))

    @classmethod
    def setUpClass(self):
        # Get the DynectSession creation out of the way so that tests can
        # ignore it
        with patch('dyn.core.SessionEngine.execute',
                   return_value={'status': 'success'}):
            provider = DynProvider('test', 'cust', 'user', 'pass')
            provider._check_dyn_sess()

    def setUp(self):
        # Flush our zone to ensure we start fresh
        _CachingDynZone.flush_zone(self.expected.name[:-1])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_non_existent(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            DynectGetError('foo'),
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
        ])
        self.assertEquals(set(), got.records)

    @patch('dyn.core.SessionEngine.execute')
    def test_populate(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # get Zone
            {
                'data': {}
            },
            # get_all_records
            {
                'data': {
                    'a_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'address': '1.2.3.4'
                        },
                        'record_id': 1,
                        'record_type': 'A',
                        'ttl': 300,
                        'zone': 'unit.tests',
                    }],
                    'cname_records': [{
                        'fqdn': 'cname.unit.tests',
                        'rdata': {
                            'cname': 'unit.tests.'
                        },
                        'record_id': 2,
                        'record_type': 'CNAME',
                        'ttl': 301,
                        'zone': 'unit.tests',
                    }],
                    'ns_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'nsdname': 'ns1.p10.dynect.net.'
                        },
                        'record_id': 254597562,
                        'record_type': 'NS',
                        'service_class': '',
                        'ttl': 3600,
                        'zone': 'unit.tests'
                    }, {
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'nsdname': 'ns2.p10.dynect.net.'
                        },
                        'record_id': 254597563,
                        'record_type': 'NS',
                        'service_class': '',
                        'ttl': 3600,
                        'zone': 'unit.tests'
                    }, {
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'nsdname': 'ns3.p10.dynect.net.'
                        },
                        'record_id': 254597564,
                        'record_type': 'NS',
                        'service_class': '',
                        'ttl': 3600,
                        'zone': 'unit.tests'
                    }, {
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'nsdname': 'ns4.p10.dynect.net.'
                        },
                        'record_id': 254597565,
                        'record_type': 'NS',
                        'service_class': '',
                        'ttl': 3600,
                        'zone': 'unit.tests'
                    }, {
                        'fqdn': 'sub.unit.tests',
                        'rdata': {
                            'nsdname': 'ns3.p10.dynect.net.'
                        },
                        'record_id': 254597564,
                        'record_type': 'NS',
                        'service_class': '',
                        'ttl': 3600,
                        'zone': 'unit.tests'
                    }, {
                        'fqdn': 'sub.unit.tests',
                        'rdata': {
                            'nsdname': 'ns3.p10.dynect.net.'
                        },
                        'record_id': 254597564,
                        'record_type': 'NS',
                        'service_class': '',
                        'ttl': 3600,
                        'zone': 'unit.tests'
                    }],
                    'mx_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'exchange': 'smtp-1.unit.tests.',
                            'preference': 10
                        },
                        'record_id': 3,
                        'record_type': 'MX',
                        'ttl': 302,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'exchange': 'smtp-2.unit.tests.',
                            'preference': 20
                        },
                        'record_id': 4,
                        'record_type': 'MX',
                        'ttl': 302,
                        'zone': 'unit.tests',
                    }],
                    'naptr_records': [{
                        'fqdn': 'naptr.unit.tests',
                        'rdata': {
                            'flags': 'U',
                            'order': 100,
                            'preference': 101,
                            'regexp': '!^.*$!sip:[email protected]!',
                            'replacement': '.',
                            'services': 'SIP+D2U'
                        },
                        'record_id': 5,
                        'record_type': 'MX',
                        'ttl': 303,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': 'naptr.unit.tests',
                        'rdata': {
                            'flags': 'U',
                            'order': 200,
                            'preference': 201,
                            'regexp': '!^.*$!sip:[email protected]!',
                            'replacement': '.',
                            'services': 'SIP+D2U'
                        },
                        'record_id': 6,
                        'record_type': 'MX',
                        'ttl': 303,
                        'zone': 'unit.tests',
                    }],
                    'ptr_records': [{
                        'fqdn': 'ptr.unit.tests',
                        'rdata': {
                            'ptrdname': 'xx.unit.tests.'
                        },
                        'record_id': 7,
                        'record_type': 'PTR',
                        'ttl': 304,
                        'zone': 'unit.tests',
                    }],
                    'soa_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'txtdata':
                            'ns1.p16.dynect.net. '
                            'hostmaster.unit.tests. 4 3600 600 604800 1800'
                        },
                        'record_id': 99,
                        'record_type': 'SOA',
                        'ttl': 299,
                        'zone': 'unit.tests',
                    }],
                    'spf_records': [{
                        'fqdn': 'spf.unit.tests',
                        'rdata': {
                            'txtdata': 'v=spf1 ip4:192.168.0.1/16-all'
                        },
                        'record_id': 8,
                        'record_type': 'SPF',
                        'ttl': 305,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': 'spf.unit.tests',
                        'rdata': {
                            'txtdata': 'v=spf1 -all'
                        },
                        'record_id': 8,
                        'record_type': 'SPF',
                        'ttl': 305,
                        'zone': 'unit.tests',
                    }],
                    'sshfp_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'algorithm': 1,
                            'fingerprint':
                            'bf6b6825d2977c511a475bbefb88aad54a92ac73',
                            'fptype': 1
                        },
                        'record_id': 9,
                        'record_type': 'SSHFP',
                        'ttl': 306,
                        'zone': 'unit.tests',
                    }],
                    'srv_records': [{
                        'fqdn': '_srv._tcp.unit.tests',
                        'rdata': {
                            'port': 10,
                            'priority': 11,
                            'target': 'foo-1.unit.tests.',
                            'weight': 12
                        },
                        'record_id': 10,
                        'record_type': 'SRV',
                        'ttl': 307,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': '_srv._tcp.unit.tests',
                        'rdata': {
                            'port': 20,
                            'priority': 21,
                            'target': 'foo-2.unit.tests.',
                            'weight': 22
                        },
                        'record_id': 11,
                        'record_type': 'SRV',
                        'ttl': 307,
                        'zone': 'unit.tests',
                    }],
                }
            }
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        changes = self.expected.changes(got, SimpleProvider())
        self.assertEquals([], changes)

    @patch('dyn.core.SessionEngine.execute')
    def test_sync(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # No such zone, during populate
            DynectGetError('foo'),
            # No such zone, during sync
            DynectGetError('foo'),
            # get empty Zone
            {
                'data': {}
            },
            # get zone we can modify & delete with
            {
                'data': {
                    # A top-level to delete
                    'a_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'address': '1.2.3.4'
                        },
                        'record_id': 1,
                        'record_type': 'A',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': 'a.unit.tests',
                        'rdata': {
                            'address': '2.3.4.5'
                        },
                        'record_id': 2,
                        'record_type': 'A',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                    # A node to delete
                    'cname_records': [{
                        'fqdn': 'cname.unit.tests',
                        'rdata': {
                            'cname': 'unit.tests.'
                        },
                        'record_id': 3,
                        'record_type': 'CNAME',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                    # A record to leave alone
                    'ptr_records': [{
                        'fqdn': 'ptr.unit.tests',
                        'rdata': {
                            'ptrdname': 'xx.unit.tests.'
                        },
                        'record_id': 4,
                        'record_type': 'PTR',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                    # A record to modify
                    'srv_records': [{
                        'fqdn': '_srv._tcp.unit.tests',
                        'rdata': {
                            'port': 10,
                            'priority': 11,
                            'target': 'foo-1.unit.tests.',
                            'weight': 12
                        },
                        'record_id': 5,
                        'record_type': 'SRV',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': '_srv._tcp.unit.tests',
                        'rdata': {
                            'port': 20,
                            'priority': 21,
                            'target': 'foo-2.unit.tests.',
                            'weight': 22
                        },
                        'record_id': 6,
                        'record_type': 'SRV',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                }
            }
        ]

        # No existing records, create all
        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.zones.Zone._update') as update_mock:
                plan = provider.plan(self.expected)
                update_mock.assert_not_called()
                provider.apply(plan)
                update_mock.assert_called()
            add_mock.assert_called()
            # Once for each dyn record (8 Records, 2 of which have dual values)
            self.assertEquals(14, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/Zone/unit.tests/', 'GET', {})
        ])
        self.assertEquals(9, len(plan.changes))

        execute_mock.reset_mock()

        # Delete one and modify another
        new = Zone('unit.tests.', [])
        for name, data in (('a', {
                'type': 'A',
                'ttl': 30,
                'value': '2.3.4.5'
        }), ('ptr', {
                'type': 'PTR',
                'ttl': 30,
                'value': 'xx.unit.tests.'
        }), ('_srv._tcp', {
                'type':
                'SRV',
                'ttl':
                30,
                'values': [{
                    'priority': 31,
                    'weight': 12,
                    'port': 10,
                    'target': 'foo-1.unit.tests.'
                }, {
                    'priority': 21,
                    'weight': 22,
                    'port': 20,
                    'target': 'foo-2.unit.tests.'
                }]
        })):
            new.add_record(Record.new(new, name, data))

        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.records.DNSRecord.delete') as delete_mock:
                with patch('dyn.tm.zones.Zone._update') as update_mock:
                    plan = provider.plan(new)
                    provider.apply(plan)
                    update_mock.assert_called()
                # we expect 4 deletes, 2 from actual deletes and 2 from
                # updates which delete and recreate
                self.assertEquals(4, len(delete_mock.call_args_list))
            # the 2 (re)creates
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        self.assertEquals(3, len(plan.changes))
예제 #10
0
    def test_existing_nameservers(self):
        ns_values = ['8.8.8.8.', '9.9.9.9.']
        provider = PowerDnsProvider('test',
                                    'non.existent',
                                    'api-key',
                                    nameserver_values=ns_values)

        expected = Zone('unit.tests.', [])
        ns_record = Record.new(expected, '', {
            'type': 'NS',
            'ttl': 600,
            'values': ns_values
        })
        expected.add_record(ns_record)

        # 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)

            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._remove_record(unrelated_record)

        # ttl diff
        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)

            plan = provider.plan(expected)
            self.assertEquals(1, len(plan.changes))

        # create
        with requests_mock() as mock:
            data = {'rrsets': []}
            mock.get(ANY, status_code=200, json=data)

            plan = provider.plan(expected)
            self.assertEquals(1, len(plan.changes))
예제 #11
0
from octodns.provider.base import Plan

from azure.mgmt.dns.models import ARecord, AaaaRecord, CaaRecord, \
    CnameRecord, MxRecord, SrvRecord, NsRecord, PtrRecord, TxtRecord, \
    RecordSet, SoaRecord, Zone as AzureZone
from msrestazure.azure_exceptions import CloudError

from unittest import TestCase
from mock import Mock, patch

zone = Zone(name='unit.tests.', sub_zones=[])
octo_records = []
octo_records.append(
    Record.new(zone, '', {
        'ttl': 0,
        'type': 'A',
        'values': ['1.2.3.4', '10.10.10.10']
    }))
octo_records.append(
    Record.new(zone, 'a', {
        'ttl': 1,
        'type': 'A',
        'values': ['1.2.3.4', '1.1.1.1']
    }))
octo_records.append(
    Record.new(zone, 'aa', {
        'ttl': 9001,
        'type': 'A',
        'values': ['1.2.4.3']
    }))
octo_records.append(
예제 #12
0
class TestDigitalOceanProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

    # Our test suite differs a bit, add our NS and remove the simple one
    expected.add_record(
        Record.new(
            expected, 'under', {
                'ttl': 3600,
                'type': 'NS',
                'values': [
                    'ns1.unit.tests.',
                    'ns2.unit.tests.',
                ]
            }))
    for record in list(expected.records):
        if record.name == 'sub' and record._type == 'NS':
            expected._remove_record(record)
            break

    def test_populate(self):
        provider = DigitalOceanProvider('test', 'token')

        # Bad auth
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=401,
                     text='{"id":"unauthorized",'
                     '"message":"Unable to authenticate you."}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('Unauthorized', text_type(ctx.exception))

        # General error
        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.assertEquals(502, ctx.exception.response.status_code)

        # Non-existent zone doesn't populate anything
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=404,
                     text='{"id":"not_found","message":"The resource you '
                     'were accessing could not be found."}')

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        # No diffs == no changes
        with requests_mock() as mock:
            base = 'https://api.digitalocean.com/v2/domains/unit.tests/' \
                'records?page='
            with open('tests/fixtures/digitalocean-page-1.json') as fh:
                mock.get('{}{}'.format(base, 1), text=fh.read())
            with open('tests/fixtures/digitalocean-page-2.json') as fh:
                mock.get('{}{}'.format(base, 2), text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(12, len(zone.records))
            changes = self.expected.changes(zone, provider)
            self.assertEquals(0, len(changes))

        # 2nd populate makes no network calls/all from cache
        again = Zone('unit.tests.', [])
        provider.populate(again)
        self.assertEquals(12, len(again.records))

        # bust the cache
        del provider._zone_records[zone.name]

    def test_apply(self):
        provider = DigitalOceanProvider('test', 'token')

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        domain_after_creation = {
            "domain_records": [{
                "id": 11189874,
                "type": "NS",
                "name": "@",
                "data": "ns1.digitalocean.com",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }, {
                "id": 11189875,
                "type": "NS",
                "name": "@",
                "data": "ns2.digitalocean.com",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }, {
                "id": 11189876,
                "type": "NS",
                "name": "@",
                "data": "ns3.digitalocean.com",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }, {
                "id": 11189877,
                "type": "A",
                "name": "@",
                "data": "192.0.2.1",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }],
            "links": {},
            "meta": {
                "total": 4
            }
        }

        # non-existent domain, create everything
        resp.json.side_effect = [
            DigitalOceanClientNotFound,  # no zone in populate
            DigitalOceanClientNotFound,  # no domain during apply
            domain_after_creation
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 8
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST',
                 '/domains',
                 data={
                     'ip_address': '192.0.2.1',
                     'name': 'unit.tests'
                 }),
            # get all records in newly created zone
            call('GET', '/domains/unit.tests/records', {'page': 1}),
            # delete the initial A record
            call('DELETE', '/domains/unit.tests/records/11189877'),
            # created at least some of the record with expected data
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.4',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.5',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': 'ca.unit.tests.',
                     'flags': 0,
                     'name': '@',
                     'tag': 'issue',
                     'ttl': 3600,
                     'type': 'CAA'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'name': '_srv._tcp',
                     'weight': 20,
                     'data': 'foo-1.unit.tests.',
                     'priority': 10,
                     'ttl': 600,
                     'type': 'SRV',
                     'port': 30
                 }),
        ])
        self.assertEquals(24, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.records = Mock(return_value=[{
            'id': 11189897,
            'name': 'www',
            'data': '1.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189898,
            'name': 'www',
            'data': '2.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189899,
            'name': 'ttl',
            'data': '3.2.3.4',
            'ttl': 600,
            'type': 'A',
        }])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(wanted, 'ttl', {
                'ttl': 300,
                'type': 'A',
                'value': '3.2.3.4'
            }))

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/domains/unit.tests/records/11189899'),
            call('DELETE', '/domains/unit.tests/records/11189897'),
            call('DELETE', '/domains/unit.tests/records/11189898')
        ],
                                                   any_order=True)
예제 #13
0
    def test_apply(self):
        provider = DigitalOceanProvider('test', 'token')

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        domain_after_creation = {
            "domain_records": [{
                "id": 11189874,
                "type": "NS",
                "name": "@",
                "data": "ns1.digitalocean.com",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }, {
                "id": 11189875,
                "type": "NS",
                "name": "@",
                "data": "ns2.digitalocean.com",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }, {
                "id": 11189876,
                "type": "NS",
                "name": "@",
                "data": "ns3.digitalocean.com",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }, {
                "id": 11189877,
                "type": "A",
                "name": "@",
                "data": "192.0.2.1",
                "priority": None,
                "port": None,
                "ttl": 3600,
                "weight": None,
                "flags": None,
                "tag": None
            }],
            "links": {},
            "meta": {
                "total": 4
            }
        }

        # non-existent domain, create everything
        resp.json.side_effect = [
            DigitalOceanClientNotFound,  # no zone in populate
            DigitalOceanClientNotFound,  # no domain during apply
            domain_after_creation
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 8
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST',
                 '/domains',
                 data={
                     'ip_address': '192.0.2.1',
                     'name': 'unit.tests'
                 }),
            # get all records in newly created zone
            call('GET', '/domains/unit.tests/records', {'page': 1}),
            # delete the initial A record
            call('DELETE', '/domains/unit.tests/records/11189877'),
            # created at least some of the record with expected data
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.4',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.5',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': 'ca.unit.tests.',
                     'flags': 0,
                     'name': '@',
                     'tag': 'issue',
                     'ttl': 3600,
                     'type': 'CAA'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'name': '_srv._tcp',
                     'weight': 20,
                     'data': 'foo-1.unit.tests.',
                     'priority': 10,
                     'ttl': 600,
                     'type': 'SRV',
                     'port': 30
                 }),
        ])
        self.assertEquals(24, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.records = Mock(return_value=[{
            'id': 11189897,
            'name': 'www',
            'data': '1.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189898,
            'name': 'www',
            'data': '2.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189899,
            'name': 'ttl',
            'data': '3.2.3.4',
            'ttl': 600,
            'type': 'A',
        }])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(wanted, 'ttl', {
                'ttl': 300,
                'type': 'A',
                'value': '3.2.3.4'
            }))

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/domains/unit.tests/records/11189899'),
            call('DELETE', '/domains/unit.tests/records/11189897'),
            call('DELETE', '/domains/unit.tests/records/11189898')
        ],
                                                   any_order=True)
    def test_apply(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        domain_after_creation = {
            "tm":
            1000000000,
            "data": [{
                "id": "12341001",
                "domain": "unit.tests",
                "host": "@",
                "ttl": "0",
                "prio": "0",
                "type": "SOA",
                "rdata": "dns1.easydns.com. zone.easydns.com. 2020010101"
                " 3600 600 604800 0",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }, {
                "id": "12341002",
                "domain": "unit.tests",
                "host": "@",
                "ttl": "0",
                "prio": "0",
                "type": "NS",
                "rdata": "LOCAL.",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }, {
                "id": "12341003",
                "domain": "unit.tests",
                "host": "@",
                "ttl": "0",
                "prio": "0",
                "type": "MX",
                "rdata": "LOCAL.",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }],
            "count":
            3,
            "total":
            3,
            "start":
            0,
            "max":
            1000,
            "status":
            200
        }

        # non-existent domain, create everything
        resp.json.side_effect = [
            EasyDNSClientNotFound,  # no zone in populate
            domain_after_creation
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 9
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))
        self.assertFalse(plan.exists)

        self.assertEquals(25, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.records = Mock(
            return_value=[{
                "id": "12342001",
                "domain": "unit.tests",
                "host": "www",
                "ttl": "300",
                "prio": "0",
                "type": "A",
                "rdata": "2.2.3.9",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }, {
                "id": "12342002",
                "domain": "unit.tests",
                "host": "www",
                "ttl": "300",
                "prio": "0",
                "type": "A",
                "rdata": "2.2.3.8",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }, {
                "id": "12342003",
                "domain": "unit.tests",
                "host": "test1",
                "ttl": "3600",
                "prio": "0",
                "type": "A",
                "rdata": "1.2.3.4",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(wanted, 'test1', {
                "name": "test1",
                "ttl": 300,
                "type": "A",
                "value": "1.2.3.4",
            }))

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('PUT',
                 '/zones/records/add/unit.tests/A',
                 data={
                     'rdata': '1.2.3.4',
                     'name': 'test1',
                     'ttl': 300,
                     'type': 'A',
                     'host': 'test1',
                 }),
            call('DELETE', '/zones/records/unit.tests/12342001'),
            call('DELETE', '/zones/records/unit.tests/12342002'),
            call('DELETE', '/zones/records/unit.tests/12342003')
        ],
                                                   any_order=True)
    def test_update_delete(self):
        # We need another run so that we can delete, we can't both add and
        # delete in one go b/c of swaps
        provider = CloudflareProvider('test', 'email', 'token')

        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997653",
                "type": "NS",
                "name": "unit.tests",
                "content": "ns1.foo.bar",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997654",
                "type": "NS",
                "name": "unit.tests",
                "content": "ns2.foo.bar",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        provider._request = Mock()
        provider._request.side_effect = [
            self.empty,  # no zones
            {
                'result': {
                    'id': 42,
                }
            },  # zone create
            None,
            None,
        ]

        # Add something and delete something
        zone = Zone('unit.tests.', [])
        existing = Record.new(
            zone,
            '',
            {
                'ttl': 300,
                'type': 'NS',
                # This matches the zone data above, one to delete, one to leave
                'values': ['ns1.foo.bar.', 'ns2.foo.bar.'],
            })
        new = Record.new(
            zone,
            '',
            {
                'ttl': 300,
                'type': 'NS',
                # This leaves one and deletes one
                'value': 'ns2.foo.bar.',
            })
        change = Update(existing, new)
        plan = Plan(zone, zone, [change], True)
        provider._apply(plan)

        # Get zones, create zone, create a record, delete a record
        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997654',
                 data={
                     'content': 'ns2.foo.bar.',
                     'type': 'NS',
                     'name': 'unit.tests',
                     'ttl': 300
                 }),
            call('DELETE', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997653')
        ])
예제 #16
0
    def test_sync(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # No such zone, during populate
            DynectGetError('foo'),
            # No such zone, during sync
            DynectGetError('foo'),
            # get empty Zone
            {
                'data': {}
            },
            # get zone we can modify & delete with
            {
                'data': {
                    # A top-level to delete
                    'a_records': [{
                        'fqdn': 'unit.tests',
                        'rdata': {
                            'address': '1.2.3.4'
                        },
                        'record_id': 1,
                        'record_type': 'A',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': 'a.unit.tests',
                        'rdata': {
                            'address': '2.3.4.5'
                        },
                        'record_id': 2,
                        'record_type': 'A',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                    # A node to delete
                    'cname_records': [{
                        'fqdn': 'cname.unit.tests',
                        'rdata': {
                            'cname': 'unit.tests.'
                        },
                        'record_id': 3,
                        'record_type': 'CNAME',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                    # A record to leave alone
                    'ptr_records': [{
                        'fqdn': 'ptr.unit.tests',
                        'rdata': {
                            'ptrdname': 'xx.unit.tests.'
                        },
                        'record_id': 4,
                        'record_type': 'PTR',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                    # A record to modify
                    'srv_records': [{
                        'fqdn': '_srv._tcp.unit.tests',
                        'rdata': {
                            'port': 10,
                            'priority': 11,
                            'target': 'foo-1.unit.tests.',
                            'weight': 12
                        },
                        'record_id': 5,
                        'record_type': 'SRV',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }, {
                        'fqdn': '_srv._tcp.unit.tests',
                        'rdata': {
                            'port': 20,
                            'priority': 21,
                            'target': 'foo-2.unit.tests.',
                            'weight': 22
                        },
                        'record_id': 6,
                        'record_type': 'SRV',
                        'ttl': 30,
                        'zone': 'unit.tests',
                    }],
                }
            }
        ]

        # No existing records, create all
        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.zones.Zone._update') as update_mock:
                plan = provider.plan(self.expected)
                update_mock.assert_not_called()
                provider.apply(plan)
                update_mock.assert_called()
            add_mock.assert_called()
            # Once for each dyn record (8 Records, 2 of which have dual values)
            self.assertEquals(14, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/Zone/unit.tests/', 'GET', {})
        ])
        self.assertEquals(9, len(plan.changes))

        execute_mock.reset_mock()

        # Delete one and modify another
        new = Zone('unit.tests.', [])
        for name, data in (('a', {
                'type': 'A',
                'ttl': 30,
                'value': '2.3.4.5'
        }), ('ptr', {
                'type': 'PTR',
                'ttl': 30,
                'value': 'xx.unit.tests.'
        }), ('_srv._tcp', {
                'type':
                'SRV',
                'ttl':
                30,
                'values': [{
                    'priority': 31,
                    'weight': 12,
                    'port': 10,
                    'target': 'foo-1.unit.tests.'
                }, {
                    'priority': 21,
                    'weight': 22,
                    'port': 20,
                    'target': 'foo-2.unit.tests.'
                }]
        })):
            new.add_record(Record.new(new, name, data))

        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.records.DNSRecord.delete') as delete_mock:
                with patch('dyn.tm.zones.Zone._update') as update_mock:
                    plan = provider.plan(new)
                    provider.apply(plan)
                    update_mock.assert_called()
                # we expect 4 deletes, 2 from actual deletes and 2 from
                # updates which delete and recreate
                self.assertEquals(4, len(delete_mock.call_args_list))
            # the 2 (re)creates
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        self.assertEquals(3, len(plan.changes))
    def test_srv(self):
        provider = CloudflareProvider('test', 'email', 'token')

        zone = Zone('unit.tests.', [])
        # SRV record not under a sub-domain
        srv_record = Record.new(
            zone, '_example._tcp', {
                'ttl': 300,
                'type': 'SRV',
                'value': {
                    'port': 1234,
                    'priority': 0,
                    'target': 'nc.unit.tests.',
                    'weight': 5
                }
            })
        # SRV record under a sub-domain
        srv_record_with_sub = Record.new(
            zone, '_example._tcp.sub', {
                'ttl': 300,
                'type': 'SRV',
                'value': {
                    'port': 1234,
                    'priority': 0,
                    'target': 'nc.unit.tests.',
                    'weight': 5
                }
            })

        srv_record_contents = provider._gen_data(srv_record)
        srv_record_with_sub_contents = provider._gen_data(srv_record_with_sub)
        self.assertEquals(
            {
                'name': '_example._tcp.unit.tests',
                'ttl': 300,
                'type': 'SRV',
                'data': {
                    'service': '_example',
                    'proto': '_tcp',
                    'name': 'unit.tests.',
                    'priority': 0,
                    'weight': 5,
                    'port': 1234,
                    'target': 'nc.unit.tests'
                }
            },
            list(srv_record_contents)[0])
        self.assertEquals(
            {
                'name': '_example._tcp.sub.unit.tests',
                'ttl': 300,
                'type': 'SRV',
                'data': {
                    'service': '_example',
                    'proto': '_tcp',
                    'name': 'sub',
                    'priority': 0,
                    'weight': 5,
                    'port': 1234,
                    'target': 'nc.unit.tests'
                }
            },
            list(srv_record_with_sub_contents)[0])
예제 #18
0
class TestDynProviderGeo(TestCase):

    with open('./tests/fixtures/dyn-traffic-director-get.json') as fh:
        traffic_director_response = loads(fh.read())

    @property
    def traffic_directors_reponse(self):
        return {
            'data': [{
                'active': 'Y',
                'label': 'unit.tests.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'some.other.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'other format',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }]
        }

    # Doing this as a property so that we get a fresh copy each time, dyn's
    # client lib messes with the return value and prevents it from working on
    # subsequent uses otherwise
    @property
    def records_response(self):
        return {
            'data': {
                'a_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {
                        'address': '1.2.3.4'
                    },
                    'record_id': 1,
                    'record_type': 'A',
                    'ttl': 301,
                    'zone': 'unit.tests',
                }],
            }
        }

    monitor_id = '42a'
    monitors_response = {
        'data': [{
            'active': 'Y',
            'dsf_monitor_id': monitor_id,
            'endpoints': [],
            'label': 'unit.tests.',
            'notifier': '',
            'options': {
                'expected': '',
                'header': 'User-Agent: Dyn Monitor',
                'host': 'unit.tests',
                'path': '/_dns',
                'port': '443',
                'timeout': '10'
            },
            'probe_interval': '60',
            'protocol': 'HTTPS',
            'response_count': '2',
            'retries': '2'
        }],
        'job_id':
        3376281406,
        'msgs': [{
            'ERR_CD': None,
            'INFO': 'DSFMonitor_get: Here are your monitors',
            'LVL': 'INFO',
            'SOURCE': 'BLL'
        }],
        'status':
        'success'
    }

    expected_geo = Zone('unit.tests.', [])
    geo_record = Record.new(
        expected_geo, '', {
            'geo': {
                'AF': ['2.2.3.4', '2.2.3.5'],
                'AS-JP': ['3.2.3.4', '3.2.3.5'],
                'NA-US': ['4.2.3.4', '4.2.3.5'],
                'NA-US-CA': ['5.2.3.4', '5.2.3.5']
            },
            'ttl': 300,
            'type': 'A',
            'values': ['1.2.3.4', '1.2.3.5'],
        })
    expected_geo.add_record(geo_record)
    expected_regular = Zone('unit.tests.', [])
    regular_record = Record.new(expected_regular, '', {
        'ttl': 301,
        'type': 'A',
        'value': '1.2.3.4',
    })
    expected_regular.add_record(regular_record)

    def setUp(self):
        # Flush our zone to ensure we start fresh
        _CachingDynZone.flush_zone('unit.tests')

    @patch('dyn.core.SessionEngine.execute')
    def test_traffic_directors(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass', True)
        # short-circuit session checking
        provider._dyn_sess = True

        # no tds
        mock.side_effect = [{'data': []}]
        self.assertEquals({}, provider.traffic_directors)

        # a supported td and an ingored one
        response = {
            'data': [{
                'active': 'Y',
                'label': 'unit.tests.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'geo.unit.tests.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'something else',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }],
            'job_id':
            3376164583,
            'status':
            'success'
        }
        mock.side_effect = [response]
        # first make sure that we get the empty version from cache
        self.assertEquals({}, provider.traffic_directors)
        # reach in and bust the cache
        provider._traffic_directors = None
        tds = provider.traffic_directors
        self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']),
                          set(tds.keys()))
        self.assertEquals(['A'], tds['unit.tests.'].keys())
        self.assertEquals(['A'], tds['geo.unit.tests.'].keys())

    @patch('dyn.core.SessionEngine.execute')
    def test_traffic_director_monitor(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass', True)
        # short-circuit session checking
        provider._dyn_sess = True

        # no monitors, will try and create
        geo_monitor_id = '42x'
        mock.side_effect = [
            self.monitors_response, {
                'data': {
                    'active': 'Y',
                    'dsf_monitor_id': geo_monitor_id,
                    'endpoints': [],
                    'label': 'geo.unit.tests.',
                    'notifier': '',
                    'options': {
                        'expected': '',
                        'header': 'User-Agent: Dyn Monitor',
                        'host': 'geo.unit.tests.',
                        'path': '/_dns',
                        'port': '443',
                        'timeout': '10'
                    },
                    'probe_interval': '60',
                    'protocol': 'HTTPS',
                    'response_count': '2',
                    'retries': '2'
                },
                'job_id':
                3376259461,
                'msgs': [{
                    'ERR_CD': None,
                    'INFO': 'add: Here is the new monitor',
                    'LVL': 'INFO',
                    'SOURCE': 'BLL'
                }],
                'status':
                'success'
            }
        ]

        # ask for a monitor that doesn't exist
        monitor = provider._traffic_director_monitor('geo.unit.tests.')
        self.assertEquals(geo_monitor_id, monitor.dsf_monitor_id)
        # should see a request for the list and a create
        mock.assert_has_calls([
            call('/DSFMonitor/', 'GET', {'detail': 'Y'}),
            call(
                '/DSFMonitor/', 'POST', {
                    'retries': 2,
                    'protocol': 'HTTPS',
                    'response_count': 2,
                    'label': 'geo.unit.tests.',
                    'probe_interval': 60,
                    'active': 'Y',
                    'options': {
                        'path': '/_dns',
                        'host': 'geo.unit.tests',
                        'header': 'User-Agent: Dyn Monitor',
                        'port': 443,
                        'timeout': 10
                    }
                })
        ])
        # created monitor is now cached
        self.assertTrue(
            'geo.unit.tests.' in provider._traffic_director_monitors)
        # pre-existing one is there too
        self.assertTrue('unit.tests.' in provider._traffic_director_monitors)

        # now ask for a monitor that does exist
        mock.reset_mock()
        monitor = provider._traffic_director_monitor('unit.tests.')
        self.assertEquals(self.monitor_id, monitor.dsf_monitor_id)
        # should have resulted in no calls b/c exists & we've cached the list
        mock.assert_not_called()

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_empty(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # empty all around
        mock.side_effect = [
            # get traffic directors
            {
                'data': []
            },
            # get zone
            {
                'data': {}
            },
            # get records
            {
                'data': {}
            },
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(0, len(got.records))
        mock.assert_has_calls([
            call('/DSF/', 'GET', {'detail': 'Y'}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_td(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # only traffic director
        mock.side_effect = [
            # get traffic directors
            self.traffic_directors_reponse,
            # get traffic director
            self.traffic_director_response,
            # get zone
            {
                'data': {}
            },
            # get records
            {
                'data': {}
            },
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        self.assertFalse(self.expected_geo.changes(got, provider))
        mock.assert_has_calls([
            call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
                 {'pending_changes': 'Y'}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_regular(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # only regular
        mock.side_effect = [
            # get traffic directors
            {
                'data': []
            },
            # get zone
            {
                'data': {}
            },
            # get records
            self.records_response
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        self.assertFalse(self.expected_regular.changes(got, provider))
        mock.assert_has_calls([
            call('/DSF/', 'GET', {'detail': 'Y'}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_both(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # both traffic director and regular, regular is ignored
        mock.side_effect = [
            # get traffic directors
            self.traffic_directors_reponse,
            # get traffic director
            self.traffic_director_response,
            # get zone
            {
                'data': {}
            },
            # get records
            self.records_response
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        self.assertFalse(self.expected_geo.changes(got, provider))
        mock.assert_has_calls([
            call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
                 {'pending_changes': 'Y'}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_director_busted(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        busted_traffic_director_response = {
            "status":
            "success",
            "data": {
                "notifiers": [],
                "rulesets": [],
                "ttl": "300",
                "active": "Y",
                "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI",
                "nodes": [{
                    "fqdn": "unit.tests",
                    "zone": "unit.tests"
                }],
                "pending_change": "",
                "label": "unit.tests.:A"
            },
            "job_id":
            3376642606,
            "msgs": [{
                "INFO": "detail: Here is your service",
                "LVL": "INFO",
                "ERR_CD": None,
                "SOURCE": "BLL"
            }]
        }
        # busted traffic director
        mock.side_effect = [
            # get traffic directors
            self.traffic_directors_reponse,
            # get traffic director
            busted_traffic_director_response,
            # get zone
            {
                'data': {}
            },
            # get records
            {
                'data': {}
            },
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        # we expect a change here for the record, the values aren't important,
        # so just compare set contents (which does name and type)
        self.assertEquals(self.expected_geo.records, got.records)
        mock.assert_has_calls([
            call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
                 {'pending_changes': 'Y'}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_apply_traffic_director(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # stubbing these out to avoid a lot of messy mocking, they'll be tested
        # individually, we'll check for expected calls
        provider._mod_geo_Create = MagicMock()
        provider._mod_geo_Update = MagicMock()
        provider._mod_geo_Delete = MagicMock()
        provider._mod_Create = MagicMock()
        provider._mod_Update = MagicMock()
        provider._mod_Delete = MagicMock()

        # busted traffic director
        mock.side_effect = [
            # get zone
            {
                'data': {}
            },
            # accept publish
            {
                'data': {}
            },
        ]
        desired = Zone('unit.tests.', [])
        geo = self.geo_record
        regular = self.regular_record

        changes = [
            Create(geo),
            Create(regular),
            Update(geo, geo),
            Update(regular, regular),
            Delete(geo),
            Delete(regular),
        ]
        plan = Plan(None, desired, changes)
        provider._apply(plan)
        mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/Zone/unit.tests/', 'PUT', {'publish': True})
        ])
        # should have seen 1 call to each
        provider._mod_geo_Create.assert_called_once()
        provider._mod_geo_Update.assert_called_once()
        provider._mod_geo_Delete.assert_called_once()
        provider._mod_Create.assert_called_once()
        provider._mod_Update.assert_called_once()
        provider._mod_Delete.assert_called_once()

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_create(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # will be tested seperately
        provider._mod_rulesets = MagicMock()

        mock.side_effect = [
            # create traffic director
            self.traffic_director_response,
            # get traffic directors
            self.traffic_directors_reponse
        ]
        provider._mod_geo_Create(None, Create(self.geo_record))
        # td now lives in cache
        self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
        # should have seen 1 gen call
        provider._mod_rulesets.assert_called_once()

    def test_mod_geo_update_geo_geo(self):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # update of an existing td

        # pre-populate the cache with our mock td
        provider._traffic_directors = {
            'unit.tests.': {
                'A': 42,
            }
        }
        # mock _mod_rulesets
        provider._mod_rulesets = MagicMock()

        geo = self.geo_record
        change = Update(geo, geo)
        provider._mod_geo_Update(None, change)
        # still in cache
        self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
        # should have seen 1 gen call
        provider._mod_rulesets.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_update_geo_regular(self, _):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # convert a td to a regular record

        provider._mod_Create = MagicMock()
        provider._mod_geo_Delete = MagicMock()

        change = Update(self.geo_record, self.regular_record)
        provider._mod_geo_Update(42, change)
        # should have seen a call to create the new regular record
        provider._mod_Create.assert_called_once_with(42, change)
        # should have seen a call to delete the old td record
        provider._mod_geo_Delete.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_update_regular_geo(self, _):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        # convert a regular record to a td

        provider._mod_geo_Create = MagicMock()
        provider._mod_Delete = MagicMock()

        change = Update(self.regular_record, self.geo_record)
        provider._mod_geo_Update(42, change)
        # should have seen a call to create the new geo record
        provider._mod_geo_Create.assert_called_once_with(42, change)
        # should have seen a call to delete the old regular record
        provider._mod_Delete.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_delete(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        td_mock = MagicMock()
        provider._traffic_directors = {
            'unit.tests.': {
                'A': td_mock,
            }
        }
        provider._mod_geo_Delete(None, Delete(self.geo_record))
        # delete called
        td_mock.delete.assert_called_once()
        # removed from cache
        self.assertFalse('A' in provider.traffic_directors['unit.tests.'])

    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_find_or_create_pool(self, mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        td = 42

        # no candidates cache miss, so create
        values = ['1.2.3.4', '1.2.3.5']
        pool = provider._find_or_create_pool(td, [], 'default', 'A', values)
        self.assertIsInstance(pool, DSFResponsePool)
        self.assertEquals(1, len(pool.rs_chains))
        records = pool.rs_chains[0].record_sets[0].records
        self.assertEquals(values, [r.address for r in records])
        mock.assert_called_once_with(td)

        # cache hit, use the one we just created
        mock.reset_mock()
        pools = [pool]
        cached = provider._find_or_create_pool(td, pools, 'default', 'A',
                                               values)
        self.assertEquals(pool, cached)
        mock.assert_not_called()

        # cache miss, non-matching label
        mock.reset_mock()
        miss = provider._find_or_create_pool(td, pools, 'NA-US-CA', 'A',
                                             values)
        self.assertNotEquals(pool, miss)
        self.assertEquals('NA-US-CA', miss.label)
        mock.assert_called_once_with(td)

        # cache miss, non-matching label
        mock.reset_mock()
        values = ['2.2.3.4.', '2.2.3.5']
        miss = provider._find_or_create_pool(td, pools, 'default', 'A', values)
        self.assertNotEquals(pool, miss)
        mock.assert_called_once_with(td)

    @patch('dyn.tm.services.DSFRuleset.add_response_pool')
    @patch('dyn.tm.services.DSFRuleset.create')
    # just lets us ignore the pool.create calls
    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_mod_rulesets_create(self, _, ruleset_create_mock,
                                 add_response_pool_mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        td_mock = MagicMock()
        td_mock._rulesets = []
        provider._traffic_director_monitor = MagicMock()
        provider._find_or_create_pool = MagicMock()

        td_mock.all_response_pools = []

        provider._find_or_create_pool.side_effect = [
            _DummyPool('default'),
            _DummyPool(1),
            _DummyPool(2),
            _DummyPool(3),
            _DummyPool(4),
        ]

        change = Create(self.geo_record)
        provider._mod_rulesets(td_mock, change)
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
        ))
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first geo and it's fallback
            call(1),
            call('default', index=999),
            # 2nd geo and it's fallback
            call(2),
            call('default', index=999),
            # 3nd geo and it's fallback
            call(3),
            call('default', index=999),
            # 4th geo and it's 2 levels of fallback
            call(4),
            call(3, index=999),
            call('default', index=999),
        ))

    # have to patch the place it's imported into, not where it lives
    @patch('octodns.provider.dyn.get_response_pool')
    @patch('dyn.tm.services.DSFRuleset.add_response_pool')
    @patch('dyn.tm.services.DSFRuleset.create')
    # just lets us ignore the pool.create calls
    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_mod_rulesets_existing(self, _, ruleset_create_mock,
                                   add_response_pool_mock,
                                   get_response_pool_mock):
        provider = DynProvider('test',
                               'cust',
                               'user',
                               'pass',
                               traffic_directors_enabled=True)

        ruleset_mock = MagicMock()
        ruleset_mock.response_pools = [_DummyPool(3)]

        td_mock = MagicMock()
        td_mock._rulesets = [
            ruleset_mock,
        ]
        provider._traffic_director_monitor = MagicMock()
        provider._find_or_create_pool = MagicMock()

        unused_pool = _DummyPool('unused')
        td_mock.all_response_pools = \
            ruleset_mock.response_pools + [unused_pool]
        get_response_pool_mock.return_value = unused_pool

        provider._find_or_create_pool.side_effect = [
            _DummyPool('default'),
            _DummyPool(1),
            _DummyPool(2),
            ruleset_mock.response_pools[0],
            _DummyPool(4),
        ]

        change = Create(self.geo_record)
        provider._mod_rulesets(td_mock, change)
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
        ))
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first geo and it's fallback
            call(1),
            call('default', index=999),
            # 2nd geo and it's fallback
            call(2),
            call('default', index=999),
            # 3nd geo, from existing, and it's fallback
            call(3),
            call('default', index=999),
            # 4th geo and it's 2 levels of fallback
            call(4),
            call(3, index=999),
            call('default', index=999),
        ))
        # unused poll should have been deleted
        self.assertTrue(unused_pool.deleted)
        # old ruleset ruleset should be deleted, it's pool will have been
        # reused
        ruleset_mock.delete.assert_called_once()
예제 #19
0
    def test_populate_normal(self):
        got = Zone('example.com.', [])
        self.source.populate(got)
        self.assertEquals(11, len(got.records))

        expected = Zone('example.com.', [])
        for name, data in (
            ('', {
                'type': 'A',
                'ttl': 30,
                'values': ['10.2.3.4', '10.2.3.5'],
            }),
            ('sub', {
                'type': 'NS',
                'ttl': 30,
                'values': ['ns1.ns.com.', 'ns2.ns.com.'],
            }),
            ('www', {
                'type': 'A',
                'ttl': 3600,
                'value': '10.2.3.6',
            }),
            ('cname', {
                'type': 'CNAME',
                'ttl': 3600,
                'value': 'www.example.com.',
            }),
            ('some-host-abc123', {
                'type': 'A',
                'ttl': 1800,
                'value': '10.2.3.7',
            }),
            ('has-dup-def123', {
                'type': 'A',
                'ttl': 3600,
                'value': '10.2.3.8',
            }),
            ('www.sub', {
                'type': 'A',
                'ttl': 3600,
                'value': '1.2.3.4',
            }),
            ('has-dup-def456', {
                'type': 'A',
                'ttl': 3600,
                'value': '10.2.3.8',
            }),
            ('', {
                'type':
                'MX',
                'ttl':
                3600,
                'values': [{
                    'priority': 10,
                    'value': 'smtp-1-host.example.com.',
                }, {
                    'priority': 20,
                    'value': 'smtp-2-host.example.com.',
                }]
            }),
            ('smtp', {
                'type':
                'MX',
                'ttl':
                1800,
                'values': [{
                    'priority': 30,
                    'value': 'smtp-1-host.example.com.',
                }, {
                    'priority': 40,
                    'value': 'smtp-2-host.example.com.',
                }]
            }),
        ):
            record = Record.new(expected, name, data)
            expected.add_record(record)

        changes = expected.changes(got, SimpleProvider())
        self.assertEquals([], changes)
예제 #20
0
    def test_base_provider(self):
        with self.assertRaises(NotImplementedError) as ctx:
            BaseProvider('base')
        self.assertEquals('Abstract base class, log property missing',
                          text_type(ctx.exception))

        class HasLog(BaseProvider):
            log = getLogger('HasLog')

        with self.assertRaises(NotImplementedError) as ctx:
            HasLog('haslog')
        self.assertEquals('Abstract base class, SUPPORTS_GEO property missing',
                          text_type(ctx.exception))

        class HasSupportsGeo(HasLog):
            SUPPORTS_GEO = False

        zone = Zone('unit.tests.', ['sub'])
        with self.assertRaises(NotImplementedError) as ctx:
            HasSupportsGeo('hassupportsgeo').populate(zone)
        self.assertEquals('Abstract base class, SUPPORTS property missing',
                          text_type(ctx.exception))

        class HasSupports(HasSupportsGeo):
            SUPPORTS = set(('A', ))

        with self.assertRaises(NotImplementedError) as ctx:
            HasSupports('hassupports').populate(zone)
        self.assertEquals('Abstract base class, populate method missing',
                          text_type(ctx.exception))

        # SUPPORTS_DYNAMIC has a default/fallback
        self.assertFalse(HasSupports('hassupports').SUPPORTS_DYNAMIC)

        # But can be overridden
        class HasSupportsDyanmic(HasSupports):
            SUPPORTS_DYNAMIC = True

        self.assertTrue(
            HasSupportsDyanmic('hassupportsdynamic').SUPPORTS_DYNAMIC)

        class HasPopulate(HasSupports):
            def populate(self, zone, target=False, lenient=False):
                zone.add_record(Record.new(zone, '', {
                    'ttl': 60,
                    'type': 'A',
                    'value': '2.3.4.5'
                }),
                                lenient=lenient)
                zone.add_record(Record.new(zone, 'going', {
                    'ttl': 60,
                    'type': 'A',
                    'value': '3.4.5.6'
                }),
                                lenient=lenient)
                zone.add_record(Record.new(zone, 'foo.sub', {
                    'ttl': 61,
                    'type': 'A',
                    'value': '4.5.6.7'
                }),
                                lenient=lenient)

        zone.add_record(
            Record.new(zone, '', {
                'ttl': 60,
                'type': 'A',
                'value': '1.2.3.4'
            }))

        self.assertTrue(
            HasSupports('hassupportsgeo').supports(list(zone.records)[0]))

        plan = HasPopulate('haspopulate').plan(zone)
        self.assertEquals(3, len(plan.changes))

        with self.assertRaises(NotImplementedError) as ctx:
            HasPopulate('haspopulate').apply(plan)
        self.assertEquals('Abstract base class, _apply method missing',
                          text_type(ctx.exception))
예제 #21
0
class TestConstellixProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

    # Our test suite differs a bit, add our NS and remove the simple one
    expected.add_record(Record.new(expected, 'under', {
        'ttl': 3600,
        'type': 'NS',
        'values': [
            'ns1.unit.tests.',
            'ns2.unit.tests.',
        ]
    }))

    # Add some ALIAS records
    expected.add_record(Record.new(expected, '', {
        'ttl': 1800,
        'type': 'ALIAS',
        'value': 'aname.unit.tests.'
    }))

    expected.add_record(Record.new(expected, 'sub', {
        'ttl': 1800,
        'type': 'ALIAS',
        'value': 'aname.unit.tests.'
    }))

    for record in list(expected.records):
        if record.name == 'sub' and record._type == 'NS':
            expected._remove_record(record)
            break

    def test_populate(self):
        provider = ConstellixProvider('test', 'api', 'secret')

        # Bad auth
        with requests_mock() as mock:
            mock.get(ANY, status_code=401,
                     text='{"errors": ["Unable to authenticate token"]}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('Unauthorized', text_type(ctx.exception))

        # Bad request
        with requests_mock() as mock:
            mock.get(ANY, status_code=400,
                     text='{"errors": ["\\"unittests\\" is not '
                          'a valid domain name"]}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('\n  - "unittests" is not a valid domain name',
                              text_type(ctx.exception))

        # General error
        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.assertEquals(502, ctx.exception.response.status_code)

        # Non-existent zone doesn't populate anything
        with requests_mock() as mock:
            mock.get(ANY, status_code=404,
                     text='<html><head></head><body></body></html>')

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        # No diffs == no changes
        with requests_mock() as mock:
            base = 'https://api.dns.constellix.com/v1/domains'
            with open('tests/fixtures/constellix-domains.json') as fh:
                mock.get('{}{}'.format(base, ''), text=fh.read())
            with open('tests/fixtures/constellix-records.json') as fh:
                mock.get('{}{}'.format(base, '/123123/records'),
                         text=fh.read())

                zone = Zone('unit.tests.', [])
                provider.populate(zone)
                self.assertEquals(15, len(zone.records))
                changes = self.expected.changes(zone, provider)
                self.assertEquals(0, len(changes))

        # 2nd populate makes no network calls/all from cache
        again = Zone('unit.tests.', [])
        provider.populate(again)
        self.assertEquals(15, len(again.records))

        # bust the cache
        del provider._zone_records[zone.name]

    def test_apply(self):
        provider = ConstellixProvider('test', 'api', 'secret')

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        # non-existent domain, create everything
        resp.json.side_effect = [
            [],  # no domains returned during populate
            [{
                'id': 123123,
                'name': 'unit.tests'
            }],  # domain created in apply
        ]

        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 6
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))

        provider._client._request.assert_has_calls([
            # get all domains to build the cache
            call('GET', ''),
            # created the domain
            call('POST', '/', data={'names': ['unit.tests']})
        ])
        # These two checks are broken up so that ordering doesn't break things.
        # Python3 doesn't make the calls in a consistent order so different
        # things follow the GET / on different runs
        provider._client._request.assert_has_calls([
            call('POST', '/123123/records/SRV', data={
                'roundRobin': [{
                    'priority': 10,
                    'weight': 20,
                    'value': 'foo-1.unit.tests.',
                    'port': 30
                }, {
                    'priority': 12,
                    'weight': 20,
                    'value': 'foo-2.unit.tests.',
                    'port': 30
                }],
                'name': '_srv._tcp',
                'ttl': 600,
            }),
        ])

        self.assertEquals(18, provider._client._request.call_count)

        provider._client._request.reset_mock()

        provider._client.records = Mock(return_value=[
            {
                'id': 11189897,
                'type': 'A',
                'name': 'www',
                'ttl': 300,
                'value': [
                    '1.2.3.4',
                    '2.2.3.4',
                ]
            }, {
                'id': 11189898,
                'type': 'A',
                'name': 'ttl',
                'ttl': 600,
                'value': [
                    '3.2.3.4'
                ]
            },  {
                'id': 11189899,
                'type': 'ALIAS',
                'name': 'alias',
                'ttl': 600,
                'value': [{
                    'value': 'aname.unit.tests.'
                }]
            }
        ])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(Record.new(wanted, 'ttl', {
            'ttl': 300,
            'type': 'A',
            'value': '3.2.3.4'
        }))

        plan = provider.plan(wanted)
        self.assertEquals(3, len(plan.changes))
        self.assertEquals(3, provider.apply(plan))

        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST', '/123123/records/A', data={
                'roundRobin': [{
                    'value': '3.2.3.4'
                }],
                'name': 'ttl',
                'ttl': 300
            }),
            call('DELETE', '/123123/records/A/11189897'),
            call('DELETE', '/123123/records/A/11189898'),
            call('DELETE', '/123123/records/ANAME/11189899')
        ], any_order=True)
예제 #22
0
class TestSelectelProvider(TestCase):
    API_URL = 'https://api.selectel.ru/domains/v1'

    api_record = []

    zone = Zone('unit.tests.', [])
    expected = set()

    domain = [{"name": "unit.tests", "id": 100000}]

    # A, subdomain=''
    api_record.append({
        'type': 'A',
        'ttl': 100,
        'content': '1.2.3.4',
        'name': 'unit.tests',
        'id': 1
    })
    expected.add(
        Record.new(zone, '', {
            'ttl': 100,
            'type': 'A',
            'value': '1.2.3.4',
        }))

    # A, subdomain='sub'
    api_record.append({
        'type': 'A',
        'ttl': 200,
        'content': '1.2.3.4',
        'name': 'sub.unit.tests',
        'id': 2
    })
    expected.add(
        Record.new(zone, 'sub', {
            'ttl': 200,
            'type': 'A',
            'value': '1.2.3.4',
        }))

    # CNAME
    api_record.append({
        'type': 'CNAME',
        'ttl': 300,
        'content': 'unit.tests',
        'name': 'www2.unit.tests',
        'id': 3
    })
    expected.add(
        Record.new(zone, 'www2', {
            'ttl': 300,
            'type': 'CNAME',
            'value': 'unit.tests.',
        }))

    # MX
    api_record.append({
        'type': 'MX',
        'ttl': 400,
        'content': 'mx1.unit.tests',
        'priority': 10,
        'name': 'unit.tests',
        'id': 4
    })
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 400,
                'type': 'MX',
                'values': [{
                    'preference': 10,
                    'exchange': 'mx1.unit.tests.',
                }]
            }))

    # NS
    api_record.append({
        'type': 'NS',
        'ttl': 600,
        'content': 'ns1.unit.tests',
        'name': 'unit.tests.',
        'id': 6
    })
    api_record.append({
        'type': 'NS',
        'ttl': 600,
        'content': 'ns2.unit.tests',
        'name': 'unit.tests',
        'id': 7
    })
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 600,
                'type': 'NS',
                'values': ['ns1.unit.tests.', 'ns2.unit.tests.'],
            }))

    # NS with sub
    api_record.append({
        'type': 'NS',
        'ttl': 700,
        'content': 'ns3.unit.tests',
        'name': 'www3.unit.tests',
        'id': 8
    })
    api_record.append({
        'type': 'NS',
        'ttl': 700,
        'content': 'ns4.unit.tests',
        'name': 'www3.unit.tests',
        'id': 9
    })
    expected.add(
        Record.new(
            zone, 'www3', {
                'ttl': 700,
                'type': 'NS',
                'values': ['ns3.unit.tests.', 'ns4.unit.tests.'],
            }))

    # SRV
    api_record.append({
        'type': 'SRV',
        'ttl': 800,
        'target': 'foo-1.unit.tests',
        'weight': 20,
        'priority': 10,
        'port': 30,
        'id': 10,
        'name': '_srv._tcp.unit.tests'
    })
    api_record.append({
        'type': 'SRV',
        'ttl': 800,
        'target': 'foo-2.unit.tests',
        'name': '_srv._tcp.unit.tests',
        'weight': 50,
        'priority': 40,
        'port': 60,
        'id': 11
    })
    expected.add(
        Record.new(
            zone, '_srv._tcp', {
                'ttl':
                800,
                'type':
                'SRV',
                'values': [{
                    'priority': 10,
                    'weight': 20,
                    'port': 30,
                    'target': 'foo-1.unit.tests.',
                }, {
                    'priority': 40,
                    'weight': 50,
                    'port': 60,
                    'target': 'foo-2.unit.tests.',
                }]
            }))

    # AAAA
    aaaa_record = {
        'type': 'AAAA',
        'ttl': 200,
        'content': '1:1ec:1::1',
        'name': 'unit.tests',
        'id': 15
    }
    api_record.append(aaaa_record)
    expected.add(
        Record.new(zone, '', {
            'ttl': 200,
            'type': 'AAAA',
            'value': '1:1ec:1::1',
        }))

    # TXT
    api_record.append({
        'type': 'TXT',
        'ttl': 300,
        'content': 'little text',
        'name': 'text.unit.tests',
        'id': 16
    })
    expected.add(
        Record.new(zone, 'text', {
            'ttl': 200,
            'type': 'TXT',
            'value': 'little text',
        }))

    @requests_mock.Mocker()
    def test_populate(self, fake_http):
        zone = Zone('unit.tests.', [])
        fake_http.get(f'{self.API_URL}/unit.tests/records/',
                      json=self.api_record)
        fake_http.get(f'{self.API_URL}/', json=self.domain)
        fake_http.head(f'{self.API_URL}/unit.tests/records/',
                       headers={'X-Total-Count': str(len(self.api_record))})
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})

        provider = SelectelProvider(123, 'secret_token')
        provider.populate(zone)

        self.assertEquals(self.expected, zone.records)

    @requests_mock.Mocker()
    def test_populate_invalid_record(self, fake_http):
        more_record = self.api_record
        more_record.append({
            "name": "unit.tests",
            "id": 100001,
            "content": "support.unit.tests.",
            "ttl": 300,
            "ns": "ns1.unit.tests",
            "type": "SOA",
            "email": "*****@*****.**"
        })

        zone = Zone('unit.tests.', [])
        fake_http.get(f'{self.API_URL}/unit.tests/records/', json=more_record)
        fake_http.get(f'{self.API_URL}/', json=self.domain)
        fake_http.head(f'{self.API_URL}/unit.tests/records/',
                       headers={'X-Total-Count': str(len(self.api_record))})
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})

        zone.add_record(
            Record.new(
                self.zone, 'unsup', {
                    'ttl': 200,
                    'type': 'NAPTR',
                    'value': {
                        'order': 40,
                        'preference': 70,
                        'flags': 'U',
                        'service': 'SIP+D2U',
                        'regexp': '!^.*$!sip:[email protected]!',
                        'replacement': '.',
                    }
                }))

        provider = SelectelProvider(123, 'secret_token')
        provider.populate(zone)

        self.assertNotEqual(self.expected, zone.records)

    @requests_mock.Mocker()
    def test_apply(self, fake_http):

        fake_http.get(f'{self.API_URL}/unit.tests/records/', json=list())
        fake_http.get(f'{self.API_URL}/', json=self.domain)
        fake_http.head(f'{self.API_URL}/unit.tests/records/',
                       headers={'X-Total-Count': '0'})
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})
        fake_http.post(f'{self.API_URL}/100000/records/', json=list())

        provider = SelectelProvider(123, 'test_token')

        zone = Zone('unit.tests.', [])

        for record in self.expected:
            zone.add_record(record)

        plan = provider.plan(zone)
        self.assertEquals(8, len(plan.changes))
        self.assertEquals(8, provider.apply(plan))

    @requests_mock.Mocker()
    def test_domain_list(self, fake_http):
        fake_http.get(f'{self.API_URL}/', json=self.domain)
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})

        expected = {'unit.tests': self.domain[0]}
        provider = SelectelProvider(123, 'test_token')

        result = provider.domain_list()
        self.assertEquals(result, expected)

    @requests_mock.Mocker()
    def test_authentication_fail(self, fake_http):
        fake_http.get(f'{self.API_URL}/', status_code=401)
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})

        with self.assertRaises(Exception) as ctx:
            SelectelProvider(123, 'fail_token')
        self.assertEquals(text_type(ctx.exception),
                          'Authorization failed. Invalid or empty token.')

    @requests_mock.Mocker()
    def test_not_exist_domain(self, fake_http):
        fake_http.get(f'{self.API_URL}/', status_code=404, json='')
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})

        fake_http.post(f'{self.API_URL}/',
                       json={
                           "name": "unit.tests",
                           "create_date": 1507154178,
                           "id": 100000
                       })
        fake_http.get(f'{self.API_URL}/unit.tests/records/', json=list())
        fake_http.head(f'{self.API_URL}/unit.tests/records/',
                       headers={'X-Total-Count': str(len(self.api_record))})
        fake_http.post(f'{self.API_URL}/100000/records/', json=list())

        provider = SelectelProvider(123, 'test_token')

        zone = Zone('unit.tests.', [])

        for record in self.expected:
            zone.add_record(record)

        plan = provider.plan(zone)
        self.assertEquals(8, len(plan.changes))
        self.assertEquals(8, provider.apply(plan))

    @requests_mock.Mocker()
    def test_delete_no_exist_record(self, fake_http):
        fake_http.get(f'{self.API_URL}/', json=self.domain)
        fake_http.get(f'{self.API_URL}/100000/records/', json=list())
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})
        fake_http.head(f'{self.API_URL}/unit.tests/records/',
                       headers={'X-Total-Count': '0'})

        provider = SelectelProvider(123, 'test_token')

        zone = Zone('unit.tests.', [])

        provider.delete_record('unit.tests', 'NS', zone)

    @requests_mock.Mocker()
    def test_change_record(self, fake_http):
        exist_record = [
            self.aaaa_record, {
                "content": "6.6.5.7",
                "ttl": 100,
                "type": "A",
                "id": 100001,
                "name": "delete.unit.tests"
            }, {
                "content": "9.8.2.1",
                "ttl": 100,
                "type": "A",
                "id": 100002,
                "name": "unit.tests"
            }
        ]  # exist
        fake_http.get(f'{self.API_URL}/unit.tests/records/', json=exist_record)
        fake_http.get(f'{self.API_URL}/', json=self.domain)
        fake_http.get(f'{self.API_URL}/100000/records/', json=exist_record)
        fake_http.head(f'{self.API_URL}/unit.tests/records/',
                       headers={'X-Total-Count': str(len(exist_record))})
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})
        fake_http.head(f'{self.API_URL}/100000/records/',
                       headers={'X-Total-Count': str(len(exist_record))})
        fake_http.post(f'{self.API_URL}/100000/records/', json=list())
        fake_http.delete(f'{self.API_URL}/100000/records/100001', text="")
        fake_http.delete(f'{self.API_URL}/100000/records/100002', text="")

        provider = SelectelProvider(123, 'test_token')

        zone = Zone('unit.tests.', [])

        for record in self.expected:
            zone.add_record(record)

        plan = provider.plan(zone)
        self.assertEquals(8, len(plan.changes))
        self.assertEquals(8, provider.apply(plan))

    @requests_mock.Mocker()
    def test_include_change_returns_false(self, fake_http):
        fake_http.get(f'{self.API_URL}/', json=self.domain)
        fake_http.head(f'{self.API_URL}/',
                       headers={'X-Total-Count': str(len(self.domain))})
        provider = SelectelProvider(123, 'test_token')
        zone = Zone('unit.tests.', [])

        exist_record = Record.new(zone, '', {
            'ttl': 60,
            'type': 'A',
            'values': ['1.1.1.1', '2.2.2.2']
        })
        new = Record.new(zone, '', {
            'ttl': 10,
            'type': 'A',
            'values': ['1.1.1.1', '2.2.2.2']
        })
        change = Update(exist_record, new)

        include_change = provider._include_change(change)

        self.assertFalse(include_change)
예제 #23
0
    def test_apply(self):
        provider = DnsimpleProvider('test', 'token', 42)

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        # non-existant domain, create everything
        resp.json.side_effect = [
            DnsimpleClientNotFound,  # no zone in populate
            DnsimpleClientNotFound,  # no domain during apply
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded
        n = len(self.expected.records) - 3
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST', '/domains', data={'name': 'unit.tests'}),
            # created at least one of the record with expected data
            call('POST', '/zones/unit.tests/records', data={
                'content': '20 30 foo-1.unit.tests.',
                'priority': 10,
                'type': 'SRV',
                'name': '_srv._tcp',
                'ttl': 600
            }),
        ])
        # expected number of total calls
        self.assertEquals(28, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.records = Mock(return_value=[
            {
                'id': 11189897,
                'name': 'www',
                'content': '1.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189898,
                'name': 'www',
                'content': '2.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189899,
                'name': 'ttl',
                'content': '3.2.3.4',
                'ttl': 600,
                'type': 'A',
            }
        ])
        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(Record.new(wanted, 'ttl', {
            'ttl': 300,
            'type': 'A',
            'value': '3.2.3.4'
        }))

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST', '/zones/unit.tests/records', data={
                'content': '3.2.3.4',
                'type': 'A',
                'name': 'ttl',
                'ttl': 300
            }),
            call('DELETE', '/zones/unit.tests/records/11189899'),
            call('DELETE', '/zones/unit.tests/records/11189897'),
            call('DELETE', '/zones/unit.tests/records/11189898')
        ], any_order=True)
    def test_apply(self):
        provider = CloudflareProvider('test', 'email', 'token')

        provider._request = Mock()

        provider._request.side_effect = [
            self.empty,  # no zones
            {
                'result': {
                    'id': 42,
                }
            },  # zone create
        ] + [None] * 20  # individual record creates

        # non-existent zone, create everything
        plan = provider.plan(self.expected)
        self.assertEquals(12, len(plan.changes))
        self.assertEquals(12, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._request.assert_has_calls(
            [
                # created the domain
                call('POST',
                     '/zones',
                     data={
                         'jump_start': False,
                         'name': 'unit.tests'
                     }),
                # created at least one of the record with expected data
                call('POST',
                     '/zones/42/dns_records',
                     data={
                         'content': 'ns1.unit.tests.',
                         'type': 'NS',
                         'name': 'under.unit.tests',
                         'ttl': 3600
                     }),
                # make sure semicolons are not escaped when sending data
                call('POST',
                     '/zones/42/dns_records',
                     data={
                         'content': 'v=DKIM1;k=rsa;s=email;h=sha256;'
                         'p=A/kinda+of/long/string+with+numb3rs',
                         'type': 'TXT',
                         'name': 'txt.unit.tests',
                         'ttl': 600
                     }),
            ],
            True)
        # expected number of total calls
        self.assertEquals(22, provider._request.call_count)

        provider._request.reset_mock()

        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997653",
                "type": "A",
                "name": "www.unit.tests",
                "content": "1.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997654",
                "type": "A",
                "name": "www.unit.tests",
                "content": "2.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:44.030044Z",
                "created_on": "2017-03-11T18:01:44.030044Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997655",
                "type": "A",
                "name": "nc.unit.tests",
                "content": "3.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 120,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:44.030044Z",
                "created_on": "2017-03-11T18:01:44.030044Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997655",
                "type": "A",
                "name": "ttl.unit.tests",
                "content": "4.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 600,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:44.030044Z",
                "created_on": "2017-03-11T18:01:44.030044Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        # we don't care about the POST/create return values
        provider._request.return_value = {}
        provider._request.side_effect = None

        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(
                wanted,
                'nc',
                {
                    'ttl': 60,  # TTL is below their min
                    'type': 'A',
                    'value': '3.2.3.4'
                }))
        wanted.add_record(
            Record.new(
                wanted,
                'ttl',
                {
                    'ttl': 300,  # TTL change
                    'type': 'A',
                    'value': '3.2.3.4'
                }))

        plan = provider.plan(wanted)
        # only see the delete & ttl update, below min-ttl is filtered out
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        self.assertTrue(plan.exists)
        # creates a the new value and then deletes all the old
        provider._request.assert_has_calls([
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997655',
                 data={
                     'content': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997653'),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997654')
        ])
예제 #25
0
    def test_populate(self):
        provider = DnsimpleProvider('test', 'token', 42)

        # Bad auth
        with requests_mock() as mock:
            mock.get(ANY, status_code=401,
                     text='{"message": "Authentication failed"}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('Unauthorized', ctx.exception.message)

        # General error
        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.assertEquals(502, ctx.exception.response.status_code)

        # Non-existant zone doesn't populate anything
        with requests_mock() as mock:
            mock.get(ANY, status_code=404,
                     text='{"message": "Domain `foo.bar` not found"}')

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        # No diffs == no changes
        with requests_mock() as mock:
            base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \
                'records?page='
            with open('tests/fixtures/dnsimple-page-1.json') as fh:
                mock.get('{}{}'.format(base, 1), text=fh.read())
            with open('tests/fixtures/dnsimple-page-2.json') as fh:
                mock.get('{}{}'.format(base, 2), text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(16, len(zone.records))
            changes = self.expected.changes(zone, provider)
            self.assertEquals(0, len(changes))

        # 2nd populate makes no network calls/all from cache
        again = Zone('unit.tests.', [])
        provider.populate(again)
        self.assertEquals(16, len(again.records))

        # bust the cache
        del provider._zone_records[zone.name]

        # test handling of invalid content
        with requests_mock() as mock:
            with open('tests/fixtures/dnsimple-invalid-content.json') as fh:
                mock.get(ANY, text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone, lenient=True)
            self.assertEquals(set([
                Record.new(zone, '', {
                    'ttl': 3600,
                    'type': 'SSHFP',
                    'values': []
                }, lenient=True),
                Record.new(zone, '_srv._tcp', {
                    'ttl': 600,
                    'type': 'SRV',
                    'values': []
                }, lenient=True),
                Record.new(zone, 'naptr', {
                    'ttl': 600,
                    'type': 'NAPTR',
                    'values': []
                }, lenient=True),
            ]), zone.records)
class TestCloudflareProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

    # Our test suite differs a bit, add our NS and remove the simple one
    expected.add_record(
        Record.new(
            expected, 'under', {
                'ttl': 3600,
                'type': 'NS',
                'values': [
                    'ns1.unit.tests.',
                    'ns2.unit.tests.',
                ]
            }))
    for record in list(expected.records):
        if record.name == 'sub' and record._type == 'NS':
            expected._remove_record(record)
            break

    empty = {'result': [], 'result_info': {'count': 0, 'per_page': 0}}

    def test_populate(self):
        provider = CloudflareProvider('test', 'email', 'token')

        # Bad requests
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=400,
                     text='{"success":false,"errors":[{"code":1101,'
                     '"message":"request was invalid"}],'
                     '"messages":[],"result":null}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)

            self.assertEquals('CloudflareError', type(ctx.exception).__name__)
            self.assertEquals('request was invalid', ctx.exception.message)

        # Bad auth
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=403,
                     text='{"success":false,"errors":[{"code":9103,'
                     '"message":"Unknown X-Auth-Key or X-Auth-Email"}],'
                     '"messages":[],"result":null}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('CloudflareAuthenticationError',
                              type(ctx.exception).__name__)
            self.assertEquals('Unknown X-Auth-Key or X-Auth-Email',
                              ctx.exception.message)

        # Bad auth, unknown resp
        with requests_mock() as mock:
            mock.get(ANY, status_code=403, text='{}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('CloudflareAuthenticationError',
                              type(ctx.exception).__name__)
            self.assertEquals('Cloudflare error', ctx.exception.message)

        # General error
        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.assertEquals(502, ctx.exception.response.status_code)

        # Non-existent zone doesn't populate anything
        with requests_mock() as mock:
            mock.get(ANY, status_code=200, json=self.empty)

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        # re-populating the same non-existent zone uses cache and makes no
        # calls
        again = Zone('unit.tests.', [])
        provider.populate(again)
        self.assertEquals(set(), again.records)

        # bust zone cache
        provider._zones = None

        # existing zone with data
        with requests_mock() as mock:
            base = 'https://api.cloudflare.com/client/v4/zones'

            # zones
            with open('tests/fixtures/cloudflare-zones-page-1.json') as fh:
                mock.get('{}?page=1'.format(base),
                         status_code=200,
                         text=fh.read())
            with open('tests/fixtures/cloudflare-zones-page-2.json') as fh:
                mock.get('{}?page=2'.format(base),
                         status_code=200,
                         text=fh.read())
            mock.get('{}?page=3'.format(base),
                     status_code=200,
                     json={
                         'result': [],
                         'result_info': {
                             'count': 0,
                             'per_page': 0
                         }
                     })

            # records
            base = '{}/234234243423aaabb334342aaa343435/dns_records' \
                .format(base)
            with open('tests/fixtures/cloudflare-dns_records-'
                      'page-1.json') as fh:
                mock.get('{}?page=1'.format(base),
                         status_code=200,
                         text=fh.read())
            with open('tests/fixtures/cloudflare-dns_records-'
                      'page-2.json') as fh:
                mock.get('{}?page=2'.format(base),
                         status_code=200,
                         text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(12, len(zone.records))

            changes = self.expected.changes(zone, provider)

            self.assertEquals(0, len(changes))

        # re-populating the same zone/records comes out of cache, no calls
        again = Zone('unit.tests.', [])
        provider.populate(again)
        self.assertEquals(12, len(again.records))

    def test_apply(self):
        provider = CloudflareProvider('test', 'email', 'token')

        provider._request = Mock()

        provider._request.side_effect = [
            self.empty,  # no zones
            {
                'result': {
                    'id': 42,
                }
            },  # zone create
        ] + [None] * 20  # individual record creates

        # non-existent zone, create everything
        plan = provider.plan(self.expected)
        self.assertEquals(12, len(plan.changes))
        self.assertEquals(12, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._request.assert_has_calls(
            [
                # created the domain
                call('POST',
                     '/zones',
                     data={
                         'jump_start': False,
                         'name': 'unit.tests'
                     }),
                # created at least one of the record with expected data
                call('POST',
                     '/zones/42/dns_records',
                     data={
                         'content': 'ns1.unit.tests.',
                         'type': 'NS',
                         'name': 'under.unit.tests',
                         'ttl': 3600
                     }),
                # make sure semicolons are not escaped when sending data
                call('POST',
                     '/zones/42/dns_records',
                     data={
                         'content': 'v=DKIM1;k=rsa;s=email;h=sha256;'
                         'p=A/kinda+of/long/string+with+numb3rs',
                         'type': 'TXT',
                         'name': 'txt.unit.tests',
                         'ttl': 600
                     }),
            ],
            True)
        # expected number of total calls
        self.assertEquals(22, provider._request.call_count)

        provider._request.reset_mock()

        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997653",
                "type": "A",
                "name": "www.unit.tests",
                "content": "1.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997654",
                "type": "A",
                "name": "www.unit.tests",
                "content": "2.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:44.030044Z",
                "created_on": "2017-03-11T18:01:44.030044Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997655",
                "type": "A",
                "name": "nc.unit.tests",
                "content": "3.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 120,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:44.030044Z",
                "created_on": "2017-03-11T18:01:44.030044Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997655",
                "type": "A",
                "name": "ttl.unit.tests",
                "content": "4.2.3.4",
                "proxiable": True,
                "proxied": False,
                "ttl": 600,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:44.030044Z",
                "created_on": "2017-03-11T18:01:44.030044Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        # we don't care about the POST/create return values
        provider._request.return_value = {}
        provider._request.side_effect = None

        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(
                wanted,
                'nc',
                {
                    'ttl': 60,  # TTL is below their min
                    'type': 'A',
                    'value': '3.2.3.4'
                }))
        wanted.add_record(
            Record.new(
                wanted,
                'ttl',
                {
                    'ttl': 300,  # TTL change
                    'type': 'A',
                    'value': '3.2.3.4'
                }))

        plan = provider.plan(wanted)
        # only see the delete & ttl update, below min-ttl is filtered out
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        self.assertTrue(plan.exists)
        # creates a the new value and then deletes all the old
        provider._request.assert_has_calls([
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997655',
                 data={
                     'content': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997653'),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997654')
        ])

    def test_update_add_swap(self):
        provider = CloudflareProvider('test', 'email', 'token')

        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997653",
                "type": "A",
                "name": "a.unit.tests",
                "content": "1.1.1.1",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997654",
                "type": "A",
                "name": "a.unit.tests",
                "content": "2.2.2.2",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        provider._request = Mock()
        provider._request.side_effect = [
            self.empty,  # no zones
            {
                'result': {
                    'id': 42,
                }
            },  # zone create
            None,
            None,
            None,
            None,
        ]

        # Add something and delete something
        zone = Zone('unit.tests.', [])
        existing = Record.new(
            zone,
            'a',
            {
                'ttl': 300,
                'type': 'A',
                # This matches the zone data above, one to swap, one to leave
                'values': ['1.1.1.1', '2.2.2.2'],
            })
        new = Record.new(
            zone,
            'a',
            {
                'ttl': 300,
                'type': 'A',
                # This leaves one, swaps ones, and adds one
                'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'],
            })
        change = Update(existing, new)
        plan = Plan(zone, zone, [change], True)
        provider._apply(plan)

        # get the list of zones, create a zone, add some records, update
        # something, and delete something
        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call('POST',
                 '/zones/42/dns_records',
                 data={
                     'content': '4.4.4.4',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997654',
                 data={
                     'content': '2.2.2.2',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997653',
                 data={
                     'content': '3.3.3.3',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
        ])

    def test_update_delete(self):
        # We need another run so that we can delete, we can't both add and
        # delete in one go b/c of swaps
        provider = CloudflareProvider('test', 'email', 'token')

        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997653",
                "type": "NS",
                "name": "unit.tests",
                "content": "ns1.foo.bar",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997654",
                "type": "NS",
                "name": "unit.tests",
                "content": "ns2.foo.bar",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        provider._request = Mock()
        provider._request.side_effect = [
            self.empty,  # no zones
            {
                'result': {
                    'id': 42,
                }
            },  # zone create
            None,
            None,
        ]

        # Add something and delete something
        zone = Zone('unit.tests.', [])
        existing = Record.new(
            zone,
            '',
            {
                'ttl': 300,
                'type': 'NS',
                # This matches the zone data above, one to delete, one to leave
                'values': ['ns1.foo.bar.', 'ns2.foo.bar.'],
            })
        new = Record.new(
            zone,
            '',
            {
                'ttl': 300,
                'type': 'NS',
                # This leaves one and deletes one
                'value': 'ns2.foo.bar.',
            })
        change = Update(existing, new)
        plan = Plan(zone, zone, [change], True)
        provider._apply(plan)

        # Get zones, create zone, create a record, delete a record
        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997654',
                 data={
                     'content': 'ns2.foo.bar.',
                     'type': 'NS',
                     'name': 'unit.tests',
                     'ttl': 300
                 }),
            call('DELETE', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997653')
        ])

    def test_srv(self):
        provider = CloudflareProvider('test', 'email', 'token')

        zone = Zone('unit.tests.', [])
        # SRV record not under a sub-domain
        srv_record = Record.new(
            zone, '_example._tcp', {
                'ttl': 300,
                'type': 'SRV',
                'value': {
                    'port': 1234,
                    'priority': 0,
                    'target': 'nc.unit.tests.',
                    'weight': 5
                }
            })
        # SRV record under a sub-domain
        srv_record_with_sub = Record.new(
            zone, '_example._tcp.sub', {
                'ttl': 300,
                'type': 'SRV',
                'value': {
                    'port': 1234,
                    'priority': 0,
                    'target': 'nc.unit.tests.',
                    'weight': 5
                }
            })

        srv_record_contents = provider._gen_data(srv_record)
        srv_record_with_sub_contents = provider._gen_data(srv_record_with_sub)
        self.assertEquals(
            {
                'name': '_example._tcp.unit.tests',
                'ttl': 300,
                'type': 'SRV',
                'data': {
                    'service': '_example',
                    'proto': '_tcp',
                    'name': 'unit.tests.',
                    'priority': 0,
                    'weight': 5,
                    'port': 1234,
                    'target': 'nc.unit.tests'
                }
            },
            list(srv_record_contents)[0])
        self.assertEquals(
            {
                'name': '_example._tcp.sub.unit.tests',
                'ttl': 300,
                'type': 'SRV',
                'data': {
                    'service': '_example',
                    'proto': '_tcp',
                    'name': 'sub',
                    'priority': 0,
                    'weight': 5,
                    'port': 1234,
                    'target': 'nc.unit.tests'
                }
            },
            list(srv_record_with_sub_contents)[0])

    def test_alias(self):
        provider = CloudflareProvider('test', 'email', 'token')

        # A CNAME for us to transform to ALIAS
        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "CNAME",
                "name": "unit.tests",
                "content": "www.unit.tests",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(1, len(zone.records))
        record = list(zone.records)[0]
        self.assertEquals('', record.name)
        self.assertEquals('unit.tests.', record.fqdn)
        self.assertEquals('ALIAS', record._type)
        self.assertEquals('www.unit.tests.', record.value)

        # Make sure we transform back to CNAME going the other way
        contents = provider._gen_data(record)
        self.assertEquals(
            {
                'content': 'www.unit.tests.',
                'name': 'unit.tests',
                'proxied': False,
                'ttl': 300,
                'type': 'CNAME'
            },
            list(contents)[0])

    def test_gen_key(self):
        provider = CloudflareProvider('test', 'email', 'token')

        for expected, data in (
            ('foo.bar.com.', {
                'content': 'foo.bar.com.',
                'type': 'CNAME',
            }),
            ('10 foo.bar.com.', {
                'content': 'foo.bar.com.',
                'priority': 10,
                'type': 'MX',
            }),
            ('0 tag some-value', {
                'data': {
                    'flags': 0,
                    'tag': 'tag',
                    'value': 'some-value',
                },
                'type': 'CAA',
            }),
            ('42 100 thing-were-pointed.at 101', {
                'data': {
                    'port': 42,
                    'priority': 100,
                    'target': 'thing-were-pointed.at',
                    'weight': 101,
                },
                'type': 'SRV',
            }),
        ):
            self.assertEqual(expected, provider._gen_key(data))

    def test_cdn(self):
        provider = CloudflareProvider('test', 'email', 'token', True)

        # A CNAME for us to transform to ALIAS
        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "CNAME",
                "name": "cname.unit.tests",
                "content": "www.unit.tests",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "A",
                "name": "a.unit.tests",
                "content": "1.1.1.1",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "A",
                "name": "a.unit.tests",
                "content": "1.1.1.2",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "A",
                "name": "multi.unit.tests",
                "content": "1.1.1.3",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "AAAA",
                "name": "multi.unit.tests",
                "content": "::1",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        zone = Zone('unit.tests.', [])
        provider.populate(zone)

        # the two A records get merged into one CNAME record pointing to
        # the CDN.
        self.assertEquals(3, len(zone.records))

        record = list(zone.records)[0]
        self.assertEquals('multi', record.name)
        self.assertEquals('multi.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)

        record = list(zone.records)[1]
        self.assertEquals('cname', record.name)
        self.assertEquals('cname.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('cname.unit.tests.cdn.cloudflare.net.', record.value)

        record = list(zone.records)[2]
        self.assertEquals('a', record.name)
        self.assertEquals('a.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value)

        # CDN enabled records can't be updated, we don't know the real values
        # never point a Cloudflare record to itself.
        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(
                wanted, 'cname', {
                    'ttl': 300,
                    'type': 'CNAME',
                    'value': 'change.unit.tests.cdn.cloudflare.net.'
                }))
        wanted.add_record(
            Record.new(
                wanted, 'new', {
                    'ttl': 300,
                    'type': 'CNAME',
                    'value': 'new.unit.tests.cdn.cloudflare.net.'
                }))
        wanted.add_record(
            Record.new(wanted, 'created', {
                'ttl': 300,
                'type': 'CNAME',
                'value': 'www.unit.tests.'
            }))

        plan = provider.plan(wanted)
        self.assertEquals(1, len(plan.changes))

    def test_cdn_alias(self):
        provider = CloudflareProvider('test', 'email', 'token', True)

        # A CNAME for us to transform to ALIAS
        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997642",
                "type": "CNAME",
                "name": "unit.tests",
                "content": "www.unit.tests",
                "proxiable": True,
                "proxied": True,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(1, len(zone.records))
        record = list(zone.records)[0]
        self.assertEquals('', record.name)
        self.assertEquals('unit.tests.', record.fqdn)
        self.assertEquals('ALIAS', record._type)
        self.assertEquals('unit.tests.cdn.cloudflare.net.', record.value)

        # CDN enabled records can't be updated, we don't know the real values
        # never point a Cloudflare record to itself.
        wanted = Zone('unit.tests.', [])
        wanted.add_record(
            Record.new(
                wanted, '', {
                    'ttl': 300,
                    'type': 'ALIAS',
                    'value': 'change.unit.tests.cdn.cloudflare.net.'
                }))

        plan = provider.plan(wanted)
        self.assertEquals(False, hasattr(plan, 'changes'))

    def test_unproxiabletype_recordfor_returnsrecordwithnocloudflare(self):
        provider = CloudflareProvider('test', 'email', 'token')
        name = "unit.tests"
        _type = "NS"
        zone_records = [{
            "id": "fc12ab34cd5611334422ab3322997654",
            "type": _type,
            "name": name,
            "content": "ns2.foo.bar",
            "proxiable": True,
            "proxied": False,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }]
        provider.zone_records = Mock(return_value=zone_records)
        zone = Zone('unit.tests.', [])
        provider.populate(zone)

        record = provider._record_for(zone, name, _type, zone_records, False)

        self.assertFalse('cloudflare' in record._octodns)

    def test_proxiabletype_recordfor_retrecordwithcloudflareunproxied(self):
        provider = CloudflareProvider('test', 'email', 'token')
        name = "multi.unit.tests"
        _type = "AAAA"
        zone_records = [{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": _type,
            "name": name,
            "content": "::1",
            "proxiable": True,
            "proxied": False,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }]
        provider.zone_records = Mock(return_value=zone_records)
        zone = Zone('unit.tests.', [])
        provider.populate(zone)

        record = provider._record_for(zone, name, _type, zone_records, False)

        self.assertFalse(record._octodns['cloudflare']['proxied'])

    def test_proxiabletype_recordfor_returnsrecordwithcloudflareproxied(self):
        provider = CloudflareProvider('test', 'email', 'token')
        name = "multi.unit.tests"
        _type = "AAAA"
        zone_records = [{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": _type,
            "name": name,
            "content": "::1",
            "proxiable": True,
            "proxied": True,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }]
        provider.zone_records = Mock(return_value=zone_records)
        zone = Zone('unit.tests.', [])
        provider.populate(zone)

        record = provider._record_for(zone, name, _type, zone_records, False)

        self.assertTrue(record._octodns['cloudflare']['proxied'])

    def test_proxiedrecordandnewttl_includechange_returnsfalse(self):
        provider = CloudflareProvider('test', 'email', 'token')
        zone = Zone('unit.tests.', [])
        existing = set_record_proxied_flag(
            Record.new(zone, 'a', {
                'ttl': 1,
                'type': 'A',
                'values': ['1.1.1.1', '2.2.2.2']
            }), True)
        new = Record.new(zone, 'a', {
            'ttl': 300,
            'type': 'A',
            'values': ['1.1.1.1', '2.2.2.2']
        })
        change = Update(existing, new)

        include_change = provider._include_change(change)

        self.assertFalse(include_change)

    def test_unproxiabletype_gendata_returnsnoproxied(self):
        provider = CloudflareProvider('test', 'email', 'token')
        zone = Zone('unit.tests.', [])
        record = Record.new(zone, 'a', {
            'ttl': 3600,
            'type': 'NS',
            'value': 'ns1.unit.tests.'
        })

        data = provider._gen_data(record).next()

        self.assertFalse('proxied' in data)

    def test_proxiabletype_gendata_returnsunproxied(self):
        provider = CloudflareProvider('test', 'email', 'token')
        zone = Zone('unit.tests.', [])
        record = set_record_proxied_flag(
            Record.new(zone, 'a', {
                'ttl': 300,
                'type': 'A',
                'value': '1.2.3.4'
            }), False)

        data = provider._gen_data(record).next()

        self.assertFalse(data['proxied'])

    def test_proxiabletype_gendata_returnsproxied(self):
        provider = CloudflareProvider('test', 'email', 'token')
        zone = Zone('unit.tests.', [])
        record = set_record_proxied_flag(
            Record.new(zone, 'a', {
                'ttl': 300,
                'type': 'A',
                'value': '1.2.3.4'
            }), True)

        data = provider._gen_data(record).next()

        self.assertTrue(data['proxied'])

    def test_createrecord_extrachanges_returnsemptylist(self):
        provider = CloudflareProvider('test', 'email', 'token')
        provider.zone_records = Mock(return_value=[])
        existing = Zone('unit.tests.', [])
        provider.populate(existing)
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": True,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        desired = Zone('unit.tests.', [])
        provider.populate(desired)
        changes = existing.changes(desired, provider)

        extra_changes = provider._extra_changes(existing, desired, changes)

        self.assertFalse(extra_changes)

    def test_updaterecord_extrachanges_returnsemptylist(self):
        provider = CloudflareProvider('test', 'email', 'token')
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": True,
            "ttl": 120,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        existing = Zone('unit.tests.', [])
        provider.populate(existing)
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": True,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        desired = Zone('unit.tests.', [])
        provider.populate(desired)
        changes = existing.changes(desired, provider)

        extra_changes = provider._extra_changes(existing, desired, changes)

        self.assertFalse(extra_changes)

    def test_deleterecord_extrachanges_returnsemptylist(self):
        provider = CloudflareProvider('test', 'email', 'token')
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": True,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        existing = Zone('unit.tests.', [])
        provider.populate(existing)
        provider.zone_records = Mock(return_value=[])
        desired = Zone('unit.tests.', [])
        provider.populate(desired)
        changes = existing.changes(desired, provider)

        extra_changes = provider._extra_changes(existing, desired, changes)

        self.assertFalse(extra_changes)

    def test_proxify_extrachanges_returnsupdatelist(self):
        provider = CloudflareProvider('test', 'email', 'token')
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": False,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        existing = Zone('unit.tests.', [])
        provider.populate(existing)
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": True,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        desired = Zone('unit.tests.', [])
        provider.populate(desired)
        changes = existing.changes(desired, provider)

        extra_changes = provider._extra_changes(existing, desired, changes)

        self.assertEquals(1, len(extra_changes))
        self.assertFalse(
            extra_changes[0].existing._octodns['cloudflare']['proxied'])
        self.assertTrue(extra_changes[0].new._octodns['cloudflare']['proxied'])

    def test_unproxify_extrachanges_returnsupdatelist(self):
        provider = CloudflareProvider('test', 'email', 'token')
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": True,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        existing = Zone('unit.tests.', [])
        provider.populate(existing)
        provider.zone_records = Mock(return_value=[{
            "id": "fc12ab34cd5611334422ab3322997642",
            "type": "CNAME",
            "name": "a.unit.tests",
            "content": "www.unit.tests",
            "proxiable": True,
            "proxied": False,
            "ttl": 300,
            "locked": False,
            "zone_id": "ff12ab34cd5611334422ab3322997650",
            "zone_name": "unit.tests",
            "modified_on": "2017-03-11T18:01:43.420689Z",
            "created_on": "2017-03-11T18:01:43.420689Z",
            "meta": {
                "auto_added": False
            }
        }])
        desired = Zone('unit.tests.', [])
        provider.populate(desired)
        changes = existing.changes(desired, provider)

        extra_changes = provider._extra_changes(existing, desired, changes)

        self.assertEquals(1, len(extra_changes))
        self.assertTrue(
            extra_changes[0].existing._octodns['cloudflare']['proxied'])
        self.assertFalse(
            extra_changes[0].new._octodns['cloudflare']['proxied'])
    def test_populate_normal(self):
        got = Zone('example.com.', [])
        self.source.populate(got)
        self.assertEquals(17, len(got.records))

        expected = Zone('example.com.', [])
        for name, data in (
            ('', {
                'type': 'A',
                'ttl': 30,
                'values': ['10.2.3.4', '10.2.3.5'],
            }),
            ('', {
                'type': 'NS',
                'ttl': 3600,
                'values': ['ns1.ns.com.', 'ns2.ns.com.'],
            }),
            ('sub', {
                'type': 'NS',
                'ttl': 30,
                'values': ['ns1.ns.com.', 'ns2.ns.com.'],
            }),
            ('www', {
                'type': 'A',
                'ttl': 3600,
                'value': '10.2.3.6',
            }),
            ('cname', {
                'type': 'CNAME',
                'ttl': 3600,
                'value': 'www.example.com.',
            }),
            ('some-host-abc123', {
                'type': 'A',
                'ttl': 1800,
                'value': '10.2.3.7',
            }),
            ('has-dup-def123', {
                'type': 'A',
                'ttl': 3600,
                'value': '10.2.3.8',
            }),
            ('www.sub', {
                'type': 'A',
                'ttl': 3600,
                'value': '1.2.3.4',
            }),
            ('has-dup-def456', {
                'type': 'A',
                'ttl': 3600,
                'value': '10.2.3.8',
            }),
            ('', {
                'type':
                'MX',
                'ttl':
                3600,
                'values': [{
                    'preference': 10,
                    'exchange': 'smtp-1-host.example.com.',
                }, {
                    'preference': 20,
                    'exchange': 'smtp-2-host.example.com.',
                }]
            }),
            ('smtp', {
                'type':
                'MX',
                'ttl':
                1800,
                'values': [{
                    'preference': 30,
                    'exchange': 'smtp-1-host.example.com.',
                }, {
                    'preference': 40,
                    'exchange': 'smtp-2-host.example.com.',
                }]
            }),
            ('', {
                'type': 'TXT',
                'ttl': 300,
                'value': 'test TXT',
            }),
            ('colon', {
                'type': 'TXT',
                'ttl': 300,
                'value': 'test : TXT',
            }),
            ('nottl', {
                'type': 'TXT',
                'ttl': 3600,
                'value': 'nottl test TXT',
            }),
            ('ipv6-3', {
                'type': 'AAAA',
                'ttl': 300,
                'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5742',
            }),
            ('ipv6-6', {
                'type': 'AAAA',
                'ttl': 3600,
                'value': '2a02:1348:017c:d5d0:0024:19ff:fef3:5743',
            }),
            ('semicolon', {
                'type': 'TXT',
                'ttl': 300,
                'value': 'v=DKIM1\\; k=rsa\\; p=blah',
            }),
        ):
            record = Record.new(expected, name, data)
            expected.add_record(record)

        changes = expected.changes(got, SimpleProvider())
        self.assertEquals([], changes)
    def test_update_add_swap(self):
        provider = CloudflareProvider('test', 'email', 'token')

        provider.zone_records = Mock(return_value=[
            {
                "id": "fc12ab34cd5611334422ab3322997653",
                "type": "A",
                "name": "a.unit.tests",
                "content": "1.1.1.1",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
            {
                "id": "fc12ab34cd5611334422ab3322997654",
                "type": "A",
                "name": "a.unit.tests",
                "content": "2.2.2.2",
                "proxiable": True,
                "proxied": False,
                "ttl": 300,
                "locked": False,
                "zone_id": "ff12ab34cd5611334422ab3322997650",
                "zone_name": "unit.tests",
                "modified_on": "2017-03-11T18:01:43.420689Z",
                "created_on": "2017-03-11T18:01:43.420689Z",
                "meta": {
                    "auto_added": False
                }
            },
        ])

        provider._request = Mock()
        provider._request.side_effect = [
            self.empty,  # no zones
            {
                'result': {
                    'id': 42,
                }
            },  # zone create
            None,
            None,
            None,
            None,
        ]

        # Add something and delete something
        zone = Zone('unit.tests.', [])
        existing = Record.new(
            zone,
            'a',
            {
                'ttl': 300,
                'type': 'A',
                # This matches the zone data above, one to swap, one to leave
                'values': ['1.1.1.1', '2.2.2.2'],
            })
        new = Record.new(
            zone,
            'a',
            {
                'ttl': 300,
                'type': 'A',
                # This leaves one, swaps ones, and adds one
                'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'],
            })
        change = Update(existing, new)
        plan = Plan(zone, zone, [change], True)
        provider._apply(plan)

        # get the list of zones, create a zone, add some records, update
        # something, and delete something
        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call('POST',
                 '/zones/42/dns_records',
                 data={
                     'content': '4.4.4.4',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997654',
                 data={
                     'content': '2.2.2.2',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997653',
                 data={
                     'content': '3.3.3.3',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
        ])
예제 #29
0
class TestDnsMadeEasyProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

    # Our test suite differs a bit, add our NS and remove the simple one
    expected.add_record(Record.new(expected, 'under', {
        'ttl': 3600,
        'type': 'NS',
        'values': [
            'ns1.unit.tests.',
            'ns2.unit.tests.',
        ]
    }))

    # Add some ALIAS records
    expected.add_record(Record.new(expected, '', {
        'ttl': 1800,
        'type': 'ALIAS',
        'value': 'aname.unit.tests.'
    }))

    for record in list(expected.records):
        if record.name == 'sub' and record._type == 'NS':
            expected._remove_record(record)
            break

    def test_populate(self):
        provider = DnsMadeEasyProvider('test', 'api', 'secret')

        # Bad auth
        with requests_mock() as mock:
            mock.get(ANY, status_code=401,
                     text='{"error": ["API key not found"]}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('Unauthorized', text_type(ctx.exception))

        # Bad request
        with requests_mock() as mock:
            mock.get(ANY, status_code=400,
                     text='{"error": ["Rate limit exceeded"]}')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('\n  - Rate limit exceeded',
                              text_type(ctx.exception))

        # General error
        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.assertEquals(502, ctx.exception.response.status_code)

        # Non-existent zone doesn't populate anything
        with requests_mock() as mock:
            mock.get(ANY, status_code=404,
                     text='<html><head></head><body></body></html>')

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        # No diffs == no changes
        with requests_mock() as mock:
            base = 'https://api.dnsmadeeasy.com/V2.0/dns/managed'
            with open('tests/fixtures/dnsmadeeasy-domains.json') as fh:
                mock.get('{}{}'.format(base, '/'), text=fh.read())
            with open('tests/fixtures/dnsmadeeasy-records.json') as fh:
                mock.get('{}{}'.format(base, '/123123/records'),
                         text=fh.read())

                zone = Zone('unit.tests.', [])
                provider.populate(zone)
                self.assertEquals(14, len(zone.records))
                changes = self.expected.changes(zone, provider)
                self.assertEquals(0, len(changes))

        # 2nd populate makes no network calls/all from cache
        again = Zone('unit.tests.', [])
        provider.populate(again)
        self.assertEquals(14, len(again.records))

        # bust the cache
        del provider._zone_records[zone.name]

    def test_apply(self):
        # Create provider with sandbox enabled
        provider = DnsMadeEasyProvider('test', 'api', 'secret', True)

        resp = Mock()
        resp.json = Mock()
        provider._client._request = Mock(return_value=resp)

        with open('tests/fixtures/dnsmadeeasy-domains.json') as fh:
            domains = json.load(fh)

        # non-existent domain, create everything
        resp.json.side_effect = [
            DnsMadeEasyClientNotFound,  # no zone in populate
            DnsMadeEasyClientNotFound,  # no domain during apply
            domains
        ]
        plan = provider.plan(self.expected)

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 10
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST', '/', data={'name': 'unit.tests'}),
            # get all domains to build the cache
            call('GET', '/'),
            # created at least some of the record with expected data
            call('POST', '/123123/records', data={
                'type': 'A',
                'name': '',
                'value': '1.2.3.4',
                'ttl': 300}),
            call('POST', '/123123/records', data={
                'type': 'A',
                'name': '',
                'value': '1.2.3.5',
                'ttl': 300}),
            call('POST', '/123123/records', data={
                'type': 'ANAME',
                'name': '',
                'value': 'aname.unit.tests.',
                'ttl': 1800}),
            call('POST', '/123123/records', data={
                'name': '',
                'value': 'ca.unit.tests',
                'issuerCritical': 0, 'caaType': 'issue',
                'ttl': 3600, 'type': 'CAA'}),
            call('POST', '/123123/records', data={
                'name': '_srv._tcp',
                'weight': 20,
                'value': 'foo-1.unit.tests.',
                'priority': 10,
                'ttl': 600,
                'type': 'SRV',
                'port': 30
            }),
        ])
        self.assertEquals(26, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.records = Mock(return_value=[
            {
                'id': 11189897,
                'name': 'www',
                'value': '1.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189898,
                'name': 'www',
                'value': '2.2.3.4',
                'ttl': 300,
                'type': 'A',
            },
            {
                'id': 11189899,
                'name': 'ttl',
                'value': '3.2.3.4',
                'ttl': 600,
                'type': 'A',
            }
        ])

        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

        wanted = Zone('unit.tests.', [])
        wanted.add_record(Record.new(wanted, 'ttl', {
            'ttl': 300,
            'type': 'A',
            'value': '3.2.3.4'
        }))

        plan = provider.plan(wanted)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))

        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST', '/123123/records', data={
                'value': '3.2.3.4',
                'type': 'A',
                'name': 'ttl',
                'ttl': 300
            }),
            call('DELETE', '/123123/records/11189899'),
            call('DELETE', '/123123/records/11189897'),
            call('DELETE', '/123123/records/11189898')
        ], any_order=True)
예제 #30
0
    def test_SRV(self):
        # doesn't blow up
        Record.new(
            self.zone, '_srv._tcp', {
                'type': 'SRV',
                'ttl': 600,
                'value': {
                    'priority': 1,
                    'weight': 2,
                    'port': 3,
                    'target': 'foo.bar.baz.'
                }
            })

        # invalid name
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, 'neup', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 2,
                        'port': 3,
                        'target': 'foo.bar.baz.'
                    }
                })
        self.assertEquals(['invalid name'], ctx.exception.reasons)

        # missing priority
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'weight': 2,
                        'port': 3,
                        'target': 'foo.bar.baz.'
                    }
                })
        self.assertEquals(['missing priority'], ctx.exception.reasons)

        # invalid priority
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 'foo',
                        'weight': 2,
                        'port': 3,
                        'target': 'foo.bar.baz.'
                    }
                })
        self.assertEquals(['invalid priority "foo"'], ctx.exception.reasons)

        # missing weight
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'port': 3,
                        'target': 'foo.bar.baz.'
                    }
                })
        self.assertEquals(['missing weight'], ctx.exception.reasons)
        # invalid weight
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 'foo',
                        'port': 3,
                        'target': 'foo.bar.baz.'
                    }
                })
        self.assertEquals(['invalid weight "foo"'], ctx.exception.reasons)

        # missing port
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 2,
                        'target': 'foo.bar.baz.'
                    }
                })
        self.assertEquals(['missing port'], ctx.exception.reasons)
        # invalid port
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 2,
                        'port': 'foo',
                        'target': 'foo.bar.baz.'
                    }
                })
        self.assertEquals(['invalid port "foo"'], ctx.exception.reasons)

        # missing target
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 2,
                        'port': 3,
                    }
                })
        self.assertEquals(['missing target'], ctx.exception.reasons)
        # invalid target
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 2,
                        'port': 3,
                        'target': 'foo.bar.baz'
                    }
                })
        self.assertEquals(['missing trailing .'], ctx.exception.reasons)