Beispiel #1
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)
    def test_process_plan(self):
        ownership = OwnershipProcessor('ownership')
        provider = PlannableProvider('helper')

        # No plan, is a quick noop
        self.assertFalse(ownership.process_plan(None))

        # Nothing exists create both records and ownership
        ownership_added = ownership.process_source_zone(zone.copy())
        plan = provider.plan(ownership_added)
        self.assertTrue(plan)
        # Double the number of records
        self.assertEquals(len(records) * 2, len(plan.changes))
        # Now process the plan, shouldn't make any changes, we're creating
        # everything
        got = ownership.process_plan(plan)
        self.assertTrue(got)
        self.assertEquals(len(records) * 2, len(got.changes))

        # Something extra exists and doesn't have ownership TXT, leave it
        # alone, we don't own it.
        extra_a = Record.new(zone, 'extra-a', {
            'ttl': 30,
            'type': 'A',
            'value': '4.4.4.4',
        })
        plan.existing.add_record(extra_a)
        # If we'd done a "real" plan we'd have a delete for the extra thing.
        plan.changes.append(Delete(extra_a))
        # Process the plan, shouldn't make any changes since the extra bit is
        # something we don't own
        got = ownership.process_plan(plan)
        self.assertTrue(got)
        self.assertEquals(len(records) * 2, len(got.changes))

        # Something extra exists and does have an ownership record so we will
        # delete it...
        copy = Zone('unit.tests.', [])
        for record in records.values():
            if record.name != 'the-a':
                copy.add_record(record)
        # New ownership, without the `the-a`
        ownership_added = ownership.process_source_zone(copy)
        self.assertEquals(len(records) * 2 - 2, len(ownership_added.records))
        plan = provider.plan(ownership_added)
        # Fake the extra existing by adding the record, its ownership, and the
        # two delete changes.
        the_a = records['the-a']
        plan.existing.add_record(the_a)
        name = f'{ownership.txt_name}.a.the-a'
        the_a_ownership = Record.new(zone, name, {
            'ttl': 30,
            'type': 'TXT',
            'value': ownership.txt_value,
        })
        plan.existing.add_record(the_a_ownership)
        plan.changes.append(Delete(the_a))
        plan.changes.append(Delete(the_a_ownership))
        # Finally process the plan, should be a noop and we should get the same
        # plan out, meaning the planned deletes were allowed to happen.
        got = ownership.process_plan(plan)
        self.assertTrue(got)
        self.assertEquals(plan, got)
        self.assertEquals(len(plan.changes), len(got.changes))
Beispiel #3
0
    def test_populate(self):
        _expected = self.make_expected()

        # Unhappy Plan - Not authenticated
        # Live test against API, will fail in an unauthorized error
        with self.assertRaises(WebFault) as ctx:
            provider = TransipProvider('test', 'unittest', self.bogus_key)
            zone = Zone('unit.tests.', [])
            provider.populate(zone, True)

        self.assertEquals(str('WebFault'),
                          str(ctx.exception.__class__.__name__))

        self.assertEquals(str('200'), ctx.exception.fault.faultcode)

        # Unhappy Plan - Zone does not exists
        # Will trigger an exception if provider is used as a target for a
        # non-existing zone
        with self.assertRaises(Exception) as ctx:
            provider = TransipProvider('test', 'unittest', self.bogus_key)
            provider._client = MockDomainService('unittest', self.bogus_key)
            zone = Zone('notfound.unit.tests.', [])
            provider.populate(zone, True)

        self.assertEquals(str('TransipNewZoneException'),
                          str(ctx.exception.__class__.__name__))

        self.assertEquals(
            'populate: (102) Transip used as target' +
            ' for non-existing zone: notfound.unit.tests.',
            text_type(ctx.exception))

        # Happy Plan - Zone does not exists
        # Won't trigger an exception if provider is NOT used as a target for a
        # non-existing zone.
        provider = TransipProvider('test', 'unittest', self.bogus_key)
        provider._client = MockDomainService('unittest', self.bogus_key)
        zone = Zone('notfound.unit.tests.', [])
        provider.populate(zone, False)

        # Happy Plan - Populate with mockup records
        provider = TransipProvider('test', 'unittest', self.bogus_key)
        provider._client = MockDomainService('unittest', self.bogus_key)
        provider._client.mockup(_expected.records)
        zone = Zone('unit.tests.', [])
        provider.populate(zone, False)

        # Transip allows relative values for types like cname, mx.
        # Test is these are correctly appended with the domain
        provider._currentZone = zone
        self.assertEquals("www.unit.tests.", provider._parse_to_fqdn("www"))
        self.assertEquals("www.unit.tests.",
                          provider._parse_to_fqdn("www.unit.tests."))
        self.assertEquals("www.sub.sub.sub.unit.tests.",
                          provider._parse_to_fqdn("www.sub.sub.sub"))
        self.assertEquals("unit.tests.", provider._parse_to_fqdn("@"))

        # Happy Plan - Even if the zone has no records the zone should exist
        provider = TransipProvider('test', 'unittest', self.bogus_key)
        provider._client = MockDomainService('unittest', self.bogus_key)
        zone = Zone('unit.tests.', [])
        exists = provider.populate(zone, True)
        self.assertTrue(exists, 'populate should return true')

        return
Beispiel #4
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', 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='{"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(1, 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)
Beispiel #5
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)
            mock.get('http://non.existent:8081/api/v1/servers/localhost',
                     status_code=200,
                     json={'version': '4.1.0'})

            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)
            mock.get('http://non.existent:8081/api/v1/servers/localhost',
                     status_code=200,
                     json={'version': '4.1.0'})

            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))
    def test__apply(self, *_):
        class DummyDesired:
            def __init__(self, name, changes):
                self.name = name
                self.changes = changes

        apply_z = Zone("unit.tests.", [])
        create_r = Record.new(apply_z, '', {
            'ttl': 0,
            'type': 'A',
            'values': ['1.2.3.4', '10.10.10.10']})
        delete_r = Record.new(apply_z, 'a', {
            'ttl': 1,
            'type': 'A',
            'values': ['1.2.3.4', '1.1.1.1']})
        update_existing_r = Record.new(apply_z, 'aa', {
            'ttl': 9001,
            'type': 'A',
            'values': ['1.2.4.3']})
        update_new_r = Record.new(apply_z, 'aa', {
            'ttl': 666,
            'type': 'A',
            'values': ['1.4.3.2']})

        gcloud_zone_mock = DummyGoogleCloudZone("unit.tests.", "unit-tests")
        status_mock = Mock()
        return_values_for_status = iter(
            ["pending"] * 11 + ['done', 'done'])
        type(status_mock).status = PropertyMock(
            side_effect=lambda: next(return_values_for_status))
        gcloud_zone_mock.changes = Mock(return_value=status_mock)

        provider = self._get_provider()
        provider.gcloud_client = Mock()
        provider._gcloud_zones = {"unit.tests.": gcloud_zone_mock}
        desired = Mock()
        desired.name = "unit.tests."
        changes = []
        changes.append(Create(create_r))
        changes.append(Delete(delete_r))
        changes.append(Update(existing=update_existing_r, new=update_new_r))

        provider.apply(Plan(
            existing=[update_existing_r, delete_r],
            desired=desired,
            changes=changes,
            exists=True
        ))

        calls_mock = gcloud_zone_mock.changes.return_value
        mocked_calls = []
        for mock_call in calls_mock.add_record_set.mock_calls:
            mocked_calls.append(mock_call[1][0])

        self.assertEqual(mocked_calls, [
            DummyResourceRecordSet(
                'unit.tests.', 'A', 0, ['1.2.3.4', '10.10.10.10']),
            DummyResourceRecordSet(
                'aa.unit.tests.', 'A', 666, ['1.4.3.2'])
        ])

        mocked_calls2 = []
        for mock_call in calls_mock.delete_record_set.mock_calls:
            mocked_calls2.append(mock_call[1][0])

        self.assertEqual(mocked_calls2, [
            DummyResourceRecordSet(
                'a.unit.tests.', 'A', 1, ['1.2.3.4', '1.1.1.1']),
            DummyResourceRecordSet(
                'aa.unit.tests.', 'A', 9001, ['1.2.4.3'])
        ])

        type(status_mock).status = "pending"

        with self.assertRaises(RuntimeError):
            provider.apply(Plan(
                existing=[update_existing_r, delete_r],
                desired=desired,
                changes=changes,
                exists=True
            ))

        unsupported_change = Mock()
        unsupported_change.__len__ = Mock(return_value=1)
        type_mock = Mock()
        type_mock._type = "A"
        unsupported_change.record = type_mock

        mock_plan = Mock()
        type(mock_plan).desired = PropertyMock(return_value=DummyDesired(
            "dummy name", []))
        type(mock_plan).changes = [unsupported_change]

        with self.assertRaises(RuntimeError):
            provider.apply(mock_plan)
Beispiel #7
0
    def test_apply(self):
        provider = DnsimpleProvider('test', 'token', 42)

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

        # non-existent 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 some of the record with expected data
            call('POST', '/zones/unit.tests/records', data={
                'content': '1.2.3.4',
                'type': 'A',
                'name': '',
                'ttl': 300}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '1.2.3.5',
                'type': 'A',
                'name': '',
                'ttl': 300}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '0 issue "ca.unit.tests"',
                'type': 'CAA',
                'name': '',
                'ttl': 3600}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49',
                'type': 'SSHFP',
                'name': '',
                'ttl': 3600}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73',
                'type': 'SSHFP',
                'name': '',
                'ttl': 3600}),
            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_sync_create(self):
        provider, stubber = self._get_stubbed_provider()

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

        list_hosted_zones_resp = {
            'HostedZones': [],
            'Marker': 'm',
            'IsTruncated': False,
            'MaxItems': '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        plan = provider.plan(self.expected)
        self.assertEquals(9, len(plan.changes))
        self.assertFalse(plan.exists)
        for change in plan.changes:
            self.assertIsInstance(change, Create)
        stubber.assert_no_pending_responses()

        create_hosted_zone_resp = {
            'HostedZone': {
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            },
            'ChangeInfo': {
                'Id': 'a12',
                'Status': 'PENDING',
                'SubmittedAt': '2017-01-29T01:02:03Z',
                'Comment': 'hrm',
            },
            'DelegationSet': {
                'Id': 'b23',
                'CallerReference': 'blip',
                'NameServers': [
                    'n12.unit.tests.',
                ],
            },
            'Location': 'us-east-1',
        }
        stubber.add_response('create_hosted_zone', create_hosted_zone_resp, {
            'Name': got.name,
            'CallerReference': ANY,
        })

        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': self.health_checks,
                'IsTruncated': False,
                'MaxItems': '100',
                'Marker': '',
            })

        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })

        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, {
                'HostedZoneId': 'z42',
                'ChangeBatch': ANY
            })

        self.assertEquals(9, provider.apply(plan))
        stubber.assert_no_pending_responses()
Beispiel #9
0
    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] * 16  # individual record creates

        # non-existant zone, create everything
        plan = provider.plan(self.expected)
        self.assertEquals(9, len(plan.changes))
        self.assertEquals(9, provider.apply(plan))

        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(18, 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))
        # recreate for update, and deletes for the 2 parts of the other
        provider._request.assert_has_calls([
            call('POST',
                 '/zones/42/dns_records',
                 data={
                     'content': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl.unit.tests',
                     'ttl': 300
                 }),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997655'),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997653'),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997654')
        ])
    def test_populate(self):
        provider, stubber = self._get_stubbed_provider()

        got = Zone('unit.tests.', [])
        with self.assertRaises(ClientError):
            stubber.add_client_error('list_hosted_zones')
            provider.populate(got)

        with self.assertRaises(ClientError):
            list_hosted_zones_resp = {
                'HostedZones': [{
                    'Name': 'unit.tests.',
                    'Id': 'z42',
                    'CallerReference': 'abc',
                }],
                'Marker':
                'm',
                'IsTruncated':
                False,
                'MaxItems':
                '100',
            }
            stubber.add_response('list_hosted_zones', list_hosted_zones_resp,
                                 {})
            stubber.add_client_error('list_resource_record_sets',
                                     expected_params={'HostedZoneId': u'z42'})
            provider.populate(got)
            stubber.assert_no_pending_responses()

        # list_hosted_zones has been cached from now on so we don't have to
        # worry about stubbing it

        list_resource_record_sets_resp_p1 = {
            'ResourceRecordSets': [{
                'Name':
                'simple.unit.tests.',
                'Type':
                'A',
                'ResourceRecords': [{
                    'Value': '1.2.3.4',
                }, {
                    'Value': '2.2.3.4',
                }],
                'TTL':
                60,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'A',
                'GeoLocation': {
                    'CountryCode': '*',
                },
                'ResourceRecords': [{
                    'Value': '2.2.3.4',
                }, {
                    'Value': '3.2.3.4',
                }],
                'TTL':
                61,
            }, {
                'Name': 'unit.tests.',
                'Type': 'A',
                'GeoLocation': {
                    'ContinentCode': 'AF',
                },
                'ResourceRecords': [{
                    'Value': '4.2.3.4',
                }],
                'TTL': 61,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'A',
                'GeoLocation': {
                    'CountryCode': 'US',
                },
                'ResourceRecords': [{
                    'Value': '5.2.3.4',
                }, {
                    'Value': '6.2.3.4',
                }],
                'TTL':
                61,
            }, {
                'Name': 'unit.tests.',
                'Type': 'A',
                'GeoLocation': {
                    'CountryCode': 'US',
                    'SubdivisionCode': 'CA',
                },
                'ResourceRecords': [{
                    'Value': '7.2.3.4',
                }],
                'TTL': 61,
            }],
            'IsTruncated':
            True,
            'NextRecordName':
            'next_name',
            'NextRecordType':
            'next_type',
            'MaxItems':
            '100',
        }
        stubber.add_response('list_resource_record_sets',
                             list_resource_record_sets_resp_p1,
                             {'HostedZoneId': 'z42'})

        list_resource_record_sets_resp_p2 = {
            'ResourceRecordSets': [{
                'Name':
                'cname.unit.tests.',
                'Type':
                'CNAME',
                'ResourceRecords': [{
                    'Value': 'unit.tests.',
                }],
                'TTL':
                62,
            }, {
                'Name':
                'txt.unit.tests.',
                'Type':
                'TXT',
                'ResourceRecords': [{
                    'Value': '"Hello World!"',
                }, {
                    'Value': '"Goodbye World?"',
                }],
                'TTL':
                63,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'MX',
                'ResourceRecords': [{
                    'Value': '10 smtp-1.unit.tests.',
                }, {
                    'Value': '20 smtp-2.unit.tests.',
                }],
                'TTL':
                64,
            }, {
                'Name':
                'naptr.unit.tests.',
                'Type':
                'NAPTR',
                'ResourceRecords': [{
                    'Value':
                    '10 20 "U" "SIP+D2U" '
                    '"!^.*$!sip:[email protected]!" .',
                }],
                'TTL':
                65,
            }, {
                'Name':
                '_srv._tcp.unit.tests.',
                'Type':
                'SRV',
                'ResourceRecords': [{
                    'Value': '10 20 30 cname.unit.tests.',
                }],
                'TTL':
                66,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'NS',
                'ResourceRecords': [{
                    'Value': 'ns1.unit.tests.',
                }],
                'TTL':
                67,
            }, {
                'Name':
                'sub.unit.tests.',
                'Type':
                'NS',
                'GeoLocation': {
                    'ContinentCode': 'AF',
                },
                'ResourceRecords': [{
                    'Value': '5.2.3.4.',
                }, {
                    'Value': '6.2.3.4.',
                }],
                'TTL':
                68,
            }, {
                'Name':
                'soa.unit.tests.',
                'Type':
                'SOA',
                'ResourceRecords': [{
                    'Value': 'ns1.unit.tests.',
                }],
                'TTL':
                69,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'CAA',
                'ResourceRecords': [{
                    'Value': '0 issue "ca.unit.tests"',
                }],
                'TTL':
                69,
            }, {
                'AliasTarget': {
                    'HostedZoneId': 'Z119WBBTVP5WFX',
                    'EvaluateTargetHealth': False,
                    'DNSName': 'unit.tests.'
                },
                'Type': 'A',
                'Name': 'alias.unit.tests.'
            }],
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response(
            'list_resource_record_sets', list_resource_record_sets_resp_p2, {
                'HostedZoneId': 'z42',
                'StartRecordName': 'next_name',
                'StartRecordType': 'next_type'
            })

        # Load everything
        provider.populate(got)
        # Make sure we got what we expected
        changes = self.expected.changes(got, GeoProvider())
        self.assertEquals(0, len(changes))
        stubber.assert_no_pending_responses()

        # Populate a zone that doesn't exist
        nonexistent = Zone('does.not.exist.', [])
        provider.populate(nonexistent)
        self.assertEquals(set(), nonexistent.records)
class TestRoute53Provider(TestCase):
    expected = Zone('unit.tests.', [])
    for name, data in (
        ('simple', {
            'ttl': 60,
            'type': 'A',
            'values': ['1.2.3.4', '2.2.3.4']
        }),
        ('', {
            'ttl': 61,
            'type': 'A',
            'values': ['2.2.3.4', '3.2.3.4'],
            'geo': {
                'AF': ['4.2.3.4'],
                'NA-US': ['5.2.3.4', '6.2.3.4'],
                'NA-US-CA': ['7.2.3.4']
            }
        }),
        ('cname', {
            'ttl': 62,
            'type': 'CNAME',
            'value': 'unit.tests.'
        }),
        ('txt', {
            'ttl': 63,
            'type': 'TXT',
            'values': ['Hello World!', 'Goodbye World?']
        }),
        ('', {
            'ttl':
            64,
            'type':
            'MX',
            'values': [{
                'preference': 10,
                'exchange': 'smtp-1.unit.tests.',
            }, {
                'preference': 20,
                'exchange': 'smtp-2.unit.tests.',
            }]
        }),
        ('naptr', {
            'ttl': 65,
            'type': 'NAPTR',
            'value': {
                'order': 10,
                'preference': 20,
                'flags': 'U',
                'service': 'SIP+D2U',
                'regexp': '!^.*$!sip:[email protected]!',
                'replacement': '.',
            }
        }),
        ('_srv._tcp', {
            'ttl': 66,
            'type': 'SRV',
            'value': {
                'priority': 10,
                'weight': 20,
                'port': 30,
                'target': 'cname.unit.tests.'
            }
        }),
        ('', {
            'ttl': 67,
            'type': 'NS',
            'values': ['8.2.3.4.', '9.2.3.4.']
        }),
        ('sub', {
            'ttl': 68,
            'type': 'NS',
            'values': ['5.2.3.4.', '6.2.3.4.']
        }),
        ('', {
            'ttl': 69,
            'type': 'CAA',
            'value': {
                'flags': 0,
                'tag': 'issue',
                'value': 'ca.unit.tests'
            }
        }),
    ):
        record = Record.new(expected, name, data)
        expected.add_record(record)

    caller_ref = '{}:A:unit.tests.:1324' \
        .format(Route53Provider.HEALTH_CHECK_VERSION)
    health_checks = [
        {
            'Id': '42',
            'CallerReference': caller_ref,
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '4.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 2,
        },
        {
            'Id': 'ignored-also',
            'CallerReference': 'something-else',
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '5.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 42,
        },
        {
            'Id': '43',
            'CallerReference': caller_ref,
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '5.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 2,
        },
        {
            'Id': '44',
            'CallerReference': caller_ref,
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '7.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 2,
        },
        {
            'Id': '45',
            # won't match anything based on type
            'CallerReference': caller_ref.replace(':A:', ':AAAA:'),
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '7.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 2,
        }
    ]

    def _get_stubbed_provider(self):
        provider = Route53Provider('test', 'abc', '123')

        # Use the stubber
        stubber = Stubber(provider._conn)
        stubber.activate()

        return (provider, stubber)

    def _get_stubbed_fallback_auth_provider(self):
        provider = Route53Provider('test')

        # Use the stubber
        stubber = Stubber(provider._conn)
        stubber.activate()

        return (provider, stubber)

    def test_populate_with_fallback(self):
        provider, stubber = self._get_stubbed_fallback_auth_provider()

        got = Zone('unit.tests.', [])
        with self.assertRaises(ClientError):
            stubber.add_client_error('list_hosted_zones')
            provider.populate(got)

    def test_populate(self):
        provider, stubber = self._get_stubbed_provider()

        got = Zone('unit.tests.', [])
        with self.assertRaises(ClientError):
            stubber.add_client_error('list_hosted_zones')
            provider.populate(got)

        with self.assertRaises(ClientError):
            list_hosted_zones_resp = {
                'HostedZones': [{
                    'Name': 'unit.tests.',
                    'Id': 'z42',
                    'CallerReference': 'abc',
                }],
                'Marker':
                'm',
                'IsTruncated':
                False,
                'MaxItems':
                '100',
            }
            stubber.add_response('list_hosted_zones', list_hosted_zones_resp,
                                 {})
            stubber.add_client_error('list_resource_record_sets',
                                     expected_params={'HostedZoneId': u'z42'})
            provider.populate(got)
            stubber.assert_no_pending_responses()

        # list_hosted_zones has been cached from now on so we don't have to
        # worry about stubbing it

        list_resource_record_sets_resp_p1 = {
            'ResourceRecordSets': [{
                'Name':
                'simple.unit.tests.',
                'Type':
                'A',
                'ResourceRecords': [{
                    'Value': '1.2.3.4',
                }, {
                    'Value': '2.2.3.4',
                }],
                'TTL':
                60,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'A',
                'GeoLocation': {
                    'CountryCode': '*',
                },
                'ResourceRecords': [{
                    'Value': '2.2.3.4',
                }, {
                    'Value': '3.2.3.4',
                }],
                'TTL':
                61,
            }, {
                'Name': 'unit.tests.',
                'Type': 'A',
                'GeoLocation': {
                    'ContinentCode': 'AF',
                },
                'ResourceRecords': [{
                    'Value': '4.2.3.4',
                }],
                'TTL': 61,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'A',
                'GeoLocation': {
                    'CountryCode': 'US',
                },
                'ResourceRecords': [{
                    'Value': '5.2.3.4',
                }, {
                    'Value': '6.2.3.4',
                }],
                'TTL':
                61,
            }, {
                'Name': 'unit.tests.',
                'Type': 'A',
                'GeoLocation': {
                    'CountryCode': 'US',
                    'SubdivisionCode': 'CA',
                },
                'ResourceRecords': [{
                    'Value': '7.2.3.4',
                }],
                'TTL': 61,
            }],
            'IsTruncated':
            True,
            'NextRecordName':
            'next_name',
            'NextRecordType':
            'next_type',
            'MaxItems':
            '100',
        }
        stubber.add_response('list_resource_record_sets',
                             list_resource_record_sets_resp_p1,
                             {'HostedZoneId': 'z42'})

        list_resource_record_sets_resp_p2 = {
            'ResourceRecordSets': [{
                'Name':
                'cname.unit.tests.',
                'Type':
                'CNAME',
                'ResourceRecords': [{
                    'Value': 'unit.tests.',
                }],
                'TTL':
                62,
            }, {
                'Name':
                'txt.unit.tests.',
                'Type':
                'TXT',
                'ResourceRecords': [{
                    'Value': '"Hello World!"',
                }, {
                    'Value': '"Goodbye World?"',
                }],
                'TTL':
                63,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'MX',
                'ResourceRecords': [{
                    'Value': '10 smtp-1.unit.tests.',
                }, {
                    'Value': '20 smtp-2.unit.tests.',
                }],
                'TTL':
                64,
            }, {
                'Name':
                'naptr.unit.tests.',
                'Type':
                'NAPTR',
                'ResourceRecords': [{
                    'Value':
                    '10 20 "U" "SIP+D2U" '
                    '"!^.*$!sip:[email protected]!" .',
                }],
                'TTL':
                65,
            }, {
                'Name':
                '_srv._tcp.unit.tests.',
                'Type':
                'SRV',
                'ResourceRecords': [{
                    'Value': '10 20 30 cname.unit.tests.',
                }],
                'TTL':
                66,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'NS',
                'ResourceRecords': [{
                    'Value': 'ns1.unit.tests.',
                }],
                'TTL':
                67,
            }, {
                'Name':
                'sub.unit.tests.',
                'Type':
                'NS',
                'GeoLocation': {
                    'ContinentCode': 'AF',
                },
                'ResourceRecords': [{
                    'Value': '5.2.3.4.',
                }, {
                    'Value': '6.2.3.4.',
                }],
                'TTL':
                68,
            }, {
                'Name':
                'soa.unit.tests.',
                'Type':
                'SOA',
                'ResourceRecords': [{
                    'Value': 'ns1.unit.tests.',
                }],
                'TTL':
                69,
            }, {
                'Name':
                'unit.tests.',
                'Type':
                'CAA',
                'ResourceRecords': [{
                    'Value': '0 issue "ca.unit.tests"',
                }],
                'TTL':
                69,
            }, {
                'AliasTarget': {
                    'HostedZoneId': 'Z119WBBTVP5WFX',
                    'EvaluateTargetHealth': False,
                    'DNSName': 'unit.tests.'
                },
                'Type': 'A',
                'Name': 'alias.unit.tests.'
            }],
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response(
            'list_resource_record_sets', list_resource_record_sets_resp_p2, {
                'HostedZoneId': 'z42',
                'StartRecordName': 'next_name',
                'StartRecordType': 'next_type'
            })

        # Load everything
        provider.populate(got)
        # Make sure we got what we expected
        changes = self.expected.changes(got, GeoProvider())
        self.assertEquals(0, len(changes))
        stubber.assert_no_pending_responses()

        # Populate a zone that doesn't exist
        nonexistent = Zone('does.not.exist.', [])
        provider.populate(nonexistent)
        self.assertEquals(set(), nonexistent.records)

    def test_sync(self):
        provider, stubber = self._get_stubbed_provider()

        list_hosted_zones_resp = {
            'HostedZones': [{
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            }],
            'Marker':
            'm',
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})
        list_resource_record_sets_resp = {
            'ResourceRecordSets': [],
            'IsTruncated': False,
            'MaxItems': '100',
        }
        stubber.add_response('list_resource_record_sets',
                             list_resource_record_sets_resp,
                             {'HostedZoneId': 'z42'})

        plan = provider.plan(self.expected)
        self.assertEquals(9, len(plan.changes))
        self.assertTrue(plan.exists)
        for change in plan.changes:
            self.assertIsInstance(change, Create)
        stubber.assert_no_pending_responses()

        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': self.health_checks,
                'IsTruncated': False,
                'MaxItems': '100',
                'Marker': '',
            })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, {
                'HostedZoneId': 'z42',
                'ChangeBatch': ANY
            })

        self.assertEquals(9, provider.apply(plan))
        stubber.assert_no_pending_responses()

        # Delete by monkey patching in a populate that includes an extra record
        def add_extra_populate(existing, target, lenient):
            for record in self.expected.records:
                existing.add_record(record)
            record = Record.new(existing, 'extra', {
                'ttl': 99,
                'type': 'A',
                'values': ['9.9.9.9']
            })
            existing.add_record(record)

        provider.populate = add_extra_populate
        change_resource_record_sets_params = {
            'ChangeBatch': {
                'Changes': [{
                    'Action': 'DELETE',
                    'ResourceRecordSet': {
                        'Name': 'extra.unit.tests.',
                        'ResourceRecords': [{
                            'Value': u'9.9.9.9'
                        }],
                        'TTL': 99,
                        'Type': 'A'
                    }
                }],
                u'Comment':
                ANY
            },
            'HostedZoneId': u'z42'
        }
        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, change_resource_record_sets_params)

        plan = provider.plan(self.expected)
        self.assertEquals(1, len(plan.changes))
        self.assertIsInstance(plan.changes[0], Delete)
        self.assertEquals(1, provider.apply(plan))
        stubber.assert_no_pending_responses()

        # Update by monkey patching in a populate that modifies the A record
        # with geos
        def mod_geo_populate(existing, target, lenient):
            for record in self.expected.records:
                if record._type != 'A' or not record.geo:
                    existing.add_record(record)
            record = Record.new(
                existing, '', {
                    'ttl': 61,
                    'type': 'A',
                    'values': ['8.2.3.4', '3.2.3.4'],
                    'geo': {
                        'AF': ['4.2.3.4'],
                        'NA-US': ['5.2.3.4', '6.2.3.4'],
                        'NA-US-KY': ['7.2.3.4']
                    }
                })
            existing.add_record(record)

        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })

        provider.populate = mod_geo_populate
        change_resource_record_sets_params = {
            'ChangeBatch': {
                'Changes': [{
                    'Action': 'DELETE',
                    'ResourceRecordSet': {
                        'GeoLocation': {
                            'CountryCode': 'US',
                            'SubdivisionCode': 'KY'
                        },
                        'Name': 'unit.tests.',
                        'ResourceRecords': [{
                            'Value': '7.2.3.4'
                        }],
                        'SetIdentifier': 'NA-US-KY',
                        'TTL': 61,
                        'Type': 'A'
                    }
                }, {
                    'Action': 'CREATE',
                    'ResourceRecordSet': {
                        'GeoLocation': {
                            'CountryCode': 'US',
                            'SubdivisionCode': 'CA'
                        },
                        'Name': 'unit.tests.',
                        'ResourceRecords': [{
                            'Value': '7.2.3.4'
                        }],
                        'SetIdentifier': 'NA-US-CA',
                        'TTL': 61,
                        'Type': 'A'
                    }
                }, {
                    'Action': 'UPSERT',
                    'ResourceRecordSet': {
                        'GeoLocation': {
                            'ContinentCode': 'AF'
                        },
                        'Name': 'unit.tests.',
                        'ResourceRecords': [{
                            'Value': '4.2.3.4'
                        }],
                        'SetIdentifier': 'AF',
                        'TTL': 61,
                        'Type': 'A'
                    }
                }, {
                    'Action': 'UPSERT',
                    'ResourceRecordSet': {
                        'GeoLocation': {
                            'CountryCode': '*'
                        },
                        'Name':
                        'unit.tests.',
                        'ResourceRecords': [{
                            'Value': '2.2.3.4'
                        }, {
                            'Value': '3.2.3.4'
                        }],
                        'SetIdentifier':
                        'default',
                        'TTL':
                        61,
                        'Type':
                        'A'
                    }
                }, {
                    'Action': 'UPSERT',
                    'ResourceRecordSet': {
                        'GeoLocation': {
                            'CountryCode': 'US'
                        },
                        'Name':
                        'unit.tests.',
                        'ResourceRecords': [{
                            'Value': '5.2.3.4'
                        }, {
                            'Value': '6.2.3.4'
                        }],
                        'SetIdentifier':
                        'NA-US',
                        'TTL':
                        61,
                        'Type':
                        'A'
                    }
                }],
                'Comment':
                ANY
            },
            'HostedZoneId': 'z42'
        }
        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, change_resource_record_sets_params)

        plan = provider.plan(self.expected)
        self.assertEquals(1, len(plan.changes))
        self.assertIsInstance(plan.changes[0], Update)
        self.assertEquals(1, provider.apply(plan))
        stubber.assert_no_pending_responses()

        # Update converting to non-geo by monkey patching in a populate that
        # modifies the A record with geos
        def mod_add_geo_populate(existing, target, lenient):
            for record in self.expected.records:
                if record._type != 'A' or record.geo:
                    existing.add_record(record)
            record = Record.new(
                existing, 'simple', {
                    'ttl': 61,
                    'type': 'A',
                    'values': ['1.2.3.4', '2.2.3.4'],
                    'geo': {
                        'OC': ['3.2.3.4', '4.2.3.4'],
                    }
                })
            existing.add_record(record)

        provider.populate = mod_add_geo_populate
        change_resource_record_sets_params = {
            'ChangeBatch': {
                'Changes': [{
                    'Action': 'DELETE',
                    'ResourceRecordSet': {
                        'GeoLocation': {
                            'CountryCode': '*'
                        },
                        'Name':
                        'simple.unit.tests.',
                        'ResourceRecords': [{
                            'Value': '1.2.3.4'
                        }, {
                            'Value': '2.2.3.4'
                        }],
                        'SetIdentifier':
                        'default',
                        'TTL':
                        61,
                        'Type':
                        'A'
                    }
                }, {
                    'Action': 'DELETE',
                    'ResourceRecordSet': {
                        'GeoLocation': {
                            'ContinentCode': 'OC'
                        },
                        'Name':
                        'simple.unit.tests.',
                        'ResourceRecords': [{
                            'Value': '3.2.3.4'
                        }, {
                            'Value': '4.2.3.4'
                        }],
                        'SetIdentifier':
                        'OC',
                        'TTL':
                        61,
                        'Type':
                        'A'
                    }
                }, {
                    'Action': 'CREATE',
                    'ResourceRecordSet': {
                        'Name':
                        'simple.unit.tests.',
                        'ResourceRecords': [{
                            'Value': '1.2.3.4'
                        }, {
                            'Value': '2.2.3.4'
                        }],
                        'TTL':
                        60,
                        'Type':
                        'A'
                    }
                }],
                'Comment':
                ANY
            },
            'HostedZoneId': 'z42'
        }
        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, change_resource_record_sets_params)
        plan = provider.plan(self.expected)
        self.assertEquals(1, len(plan.changes))
        self.assertIsInstance(plan.changes[0], Update)
        self.assertEquals(1, provider.apply(plan))
        stubber.assert_no_pending_responses()

    def test_sync_create(self):
        provider, stubber = self._get_stubbed_provider()

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

        list_hosted_zones_resp = {
            'HostedZones': [],
            'Marker': 'm',
            'IsTruncated': False,
            'MaxItems': '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        plan = provider.plan(self.expected)
        self.assertEquals(9, len(plan.changes))
        self.assertFalse(plan.exists)
        for change in plan.changes:
            self.assertIsInstance(change, Create)
        stubber.assert_no_pending_responses()

        create_hosted_zone_resp = {
            'HostedZone': {
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            },
            'ChangeInfo': {
                'Id': 'a12',
                'Status': 'PENDING',
                'SubmittedAt': '2017-01-29T01:02:03Z',
                'Comment': 'hrm',
            },
            'DelegationSet': {
                'Id': 'b23',
                'CallerReference': 'blip',
                'NameServers': [
                    'n12.unit.tests.',
                ],
            },
            'Location': 'us-east-1',
        }
        stubber.add_response('create_hosted_zone', create_hosted_zone_resp, {
            'Name': got.name,
            'CallerReference': ANY,
        })

        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': self.health_checks,
                'IsTruncated': False,
                'MaxItems': '100',
                'Marker': '',
            })

        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })

        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, {
                'HostedZoneId': 'z42',
                'ChangeBatch': ANY
            })

        self.assertEquals(9, provider.apply(plan))
        stubber.assert_no_pending_responses()

    def test_health_checks_pagination(self):
        provider, stubber = self._get_stubbed_provider()

        health_checks_p1 = [{
            'Id': '42',
            'CallerReference': self.caller_ref,
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '4.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 2,
        }, {
            'Id': '43',
            'CallerReference': 'abc123',
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '9.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 2,
        }]
        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': health_checks_p1,
                'IsTruncated': True,
                'MaxItems': '2',
                'Marker': '',
                'NextMarker': 'moar',
            })

        health_checks_p2 = [{
            'Id': '44',
            'CallerReference': self.caller_ref,
            'HealthCheckConfig': {
                'Type': 'HTTPS',
                'FullyQualifiedDomainName': 'unit.tests',
                'IPAddress': '8.2.3.4',
                'ResourcePath': '/_dns',
                'Type': 'HTTPS',
                'Port': 443,
            },
            'HealthCheckVersion': 2,
        }]
        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': health_checks_p2,
                'IsTruncated': False,
                'MaxItems': '2',
                'Marker': 'moar',
            }, {'Marker': 'moar'})

        health_checks = provider.health_checks
        self.assertEquals(
            {
                '42': health_checks_p1[0],
                '44': health_checks_p2[0],
            }, health_checks)
        stubber.assert_no_pending_responses()

        # get without create
        record = Record.new(
            self.expected, '', {
                'ttl': 61,
                'type': 'A',
                'values': ['2.2.3.4', '3.2.3.4'],
                'geo': {
                    'AF': ['4.2.3.4'],
                }
            })
        id = provider.get_health_check_id(record, 'AF', record.geo['AF'], True)
        self.assertEquals('42', id)

    def test_health_check_create(self):
        provider, stubber = self._get_stubbed_provider()

        # No match based on type
        caller_ref = \
            '{}:AAAA:foo1234'.format(Route53Provider.HEALTH_CHECK_VERSION)
        health_checks = [
            {
                'Id': '42',
                # No match based on version
                'CallerReference': '9999:A:foo1234',
                'HealthCheckConfig': {
                    'Type': 'HTTPS',
                    'FullyQualifiedDomainName': 'unit.tests',
                    'IPAddress': '4.2.3.4',
                    'ResourcePath': '/_dns',
                    'Type': 'HTTPS',
                    'Port': 443,
                },
                'HealthCheckVersion': 2,
            },
            {
                'Id': '43',
                'CallerReference': caller_ref,
                'HealthCheckConfig': {
                    'Type': 'HTTPS',
                    'FullyQualifiedDomainName': 'unit.tests',
                    'IPAddress': '4.2.3.4',
                    'ResourcePath': '/_dns',
                    'Type': 'HTTPS',
                    'Port': 443,
                },
                'HealthCheckVersion': 2,
            }
        ]
        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': health_checks,
                'IsTruncated': False,
                'MaxItems': '100',
                'Marker': '',
            })

        health_check_config = {
            'EnableSNI': False,
            'FailureThreshold': 6,
            'FullyQualifiedDomainName': 'foo.bar.com',
            'IPAddress': '4.2.3.4',
            'MeasureLatency': True,
            'Port': 8080,
            'RequestInterval': 10,
            'ResourcePath': '/_status',
            'Type': 'HTTP'
        }
        stubber.add_response(
            'create_health_check', {
                'HealthCheck': {
                    'Id': '42',
                    'CallerReference': self.caller_ref,
                    'HealthCheckConfig': health_check_config,
                    'HealthCheckVersion': 1,
                },
                'Location': 'http://url',
            }, {
                'CallerReference': ANY,
                'HealthCheckConfig': health_check_config,
            })

        record = Record.new(
            self.expected, '', {
                'ttl': 61,
                'type': 'A',
                'values': ['2.2.3.4', '3.2.3.4'],
                'geo': {
                    'AF': ['4.2.3.4'],
                },
                'octodns': {
                    'healthcheck': {
                        'host': 'foo.bar.com',
                        'path': '/_status',
                        'port': 8080,
                        'protocol': 'HTTP',
                    },
                }
            })

        # if not allowed to create returns none
        id = provider.get_health_check_id(record, 'AF', record.geo['AF'],
                                          False)
        self.assertFalse(id)

        # when allowed to create we do
        id = provider.get_health_check_id(record, 'AF', record.geo['AF'], True)
        self.assertEquals('42', id)
        stubber.assert_no_pending_responses()

    def test_health_check_gc(self):
        provider, stubber = self._get_stubbed_provider()

        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': self.health_checks,
                'IsTruncated': False,
                'MaxItems': '100',
                'Marker': '',
            })

        record = Record.new(
            self.expected,
            '',
            {
                'ttl': 61,
                'type': 'A',
                'values': ['2.2.3.4', '3.2.3.4'],
                'geo': {
                    'AF': ['4.2.3.4'],
                    'NA-US': ['5.2.3.4', '6.2.3.4'],
                    # removed one geo
                }
            })

        # gc no longer in_use records (directly)
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        provider._gc_health_checks(record, [
            DummyR53Record('42'),
            DummyR53Record('43'),
        ])
        stubber.assert_no_pending_responses()

        # gc through _mod_Create
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        change = Create(record)
        provider._mod_Create(change)
        stubber.assert_no_pending_responses()

        # gc through _mod_Update
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        # first record is ignored for our purposes, we have to pass something
        change = Update(record, record)
        provider._mod_Create(change)
        stubber.assert_no_pending_responses()

        # gc through _mod_Delete, expect 3 to go away, can't check order
        # b/c it's not deterministic
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        change = Delete(record)
        provider._mod_Delete(change)
        stubber.assert_no_pending_responses()

        # gc only AAAA, leave the A's alone
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': '45',
        })
        record = Record.new(
            self.expected, '', {
                'ttl': 61,
                'type': 'AAAA',
                'value': '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b'
            })
        provider._gc_health_checks(record, [])
        stubber.assert_no_pending_responses()

    def test_no_extra_changes(self):
        provider, stubber = self._get_stubbed_provider()

        list_hosted_zones_resp = {
            'HostedZones': [{
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            }],
            'Marker':
            'm',
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        # empty is empty
        desired = Zone('unit.tests.', [])
        extra = provider._extra_changes(desired=desired, changes=[])
        self.assertEquals([], extra)
        stubber.assert_no_pending_responses()

        # single record w/o geo is empty
        desired = Zone('unit.tests.', [])
        record = Record.new(desired, 'a', {
            'ttl': 30,
            'type': 'A',
            'value': '1.2.3.4',
        })
        desired.add_record(record)
        extra = provider._extra_changes(desired=desired, changes=[])
        self.assertEquals([], extra)
        stubber.assert_no_pending_responses()

        # short-circuit for unknown zone
        other = Zone('other.tests.', [])
        extra = provider._extra_changes(desired=other, changes=[])
        self.assertEquals([], extra)
        stubber.assert_no_pending_responses()

    def test_extra_change_no_health_check(self):
        provider, stubber = self._get_stubbed_provider()

        list_hosted_zones_resp = {
            'HostedZones': [{
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            }],
            'Marker':
            'm',
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        # record with geo and no health check returns change
        desired = Zone('unit.tests.', [])
        record = Record.new(
            desired, 'a', {
                'ttl': 30,
                'type': 'A',
                'value': '1.2.3.4',
                'geo': {
                    'NA': ['2.2.3.4'],
                }
            })
        desired.add_record(record)

        extra = provider._extra_changes(desired, [])
        self.assertEquals(0, len(extra))
        stubber.assert_no_pending_responses()

    def test_extra_change_has_wrong_health_check(self):
        provider, stubber = self._get_stubbed_provider()

        list_hosted_zones_resp = {
            'HostedZones': [{
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            }],
            'Marker':
            'm',
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        # record with geo and no health check returns change
        desired = Zone('unit.tests.', [])
        record = Record.new(
            desired, 'a', {
                'ttl': 30,
                'type': 'A',
                'value': '1.2.3.4',
                'geo': {
                    'NA': ['2.2.3.4'],
                }
            })

        desired.add_record(record)
        extra = provider._extra_changes(desired, [])
        self.assertEquals(0, len(extra))
        stubber.assert_no_pending_responses()

        for change in (Create(record), Update(record, record), Delete(record)):
            extra = provider._extra_changes(desired=desired, changes=[change])
            self.assertEquals(0, len(extra))
            stubber.assert_no_pending_responses()

    def test_extra_change_has_health_check(self):
        provider, stubber = self._get_stubbed_provider()

        list_hosted_zones_resp = {
            'HostedZones': [{
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            }],
            'Marker':
            'm',
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        # record with geo and no health check returns change
        desired = Zone('unit.tests.', [])
        record = Record.new(
            desired, 'a', {
                'ttl': 30,
                'type': 'A',
                'value': '1.2.3.4',
                'geo': {
                    'NA': ['2.2.3.4'],
                }
            })
        desired.add_record(record)
        extra = provider._extra_changes(desired, [])
        self.assertEquals(0, len(extra))
        stubber.assert_no_pending_responses()

        # change b/c of healthcheck path
        record._octodns['healthcheck'] = {'path': '/_ready'}
        extra = provider._extra_changes(desired=desired, changes=[])
        self.assertEquals(0, len(extra))
        stubber.assert_no_pending_responses()

        # change b/c of healthcheck host
        record._octodns['healthcheck'] = {'host': 'foo.bar.io'}
        extra = provider._extra_changes(desired=desired, changes=[])
        self.assertEquals(0, len(extra))
        stubber.assert_no_pending_responses()

    def _get_test_plan(self, max_changes):

        provider = Route53Provider('test', 'abc', '123', max_changes)

        # Use the stubber
        stubber = Stubber(provider._conn)
        stubber.activate()

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

        list_hosted_zones_resp = {
            'HostedZones': [],
            'Marker': 'm',
            'IsTruncated': False,
            'MaxItems': '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        create_hosted_zone_resp = {
            'HostedZone': {
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            },
            'ChangeInfo': {
                'Id': 'a12',
                'Status': 'PENDING',
                'SubmittedAt': '2017-01-29T01:02:03Z',
                'Comment': 'hrm',
            },
            'DelegationSet': {
                'Id': 'b23',
                'CallerReference': 'blip',
                'NameServers': [
                    'n12.unit.tests.',
                ],
            },
            'Location': 'us-east-1',
        }
        stubber.add_response('create_hosted_zone', create_hosted_zone_resp, {
            'Name': got.name,
            'CallerReference': ANY,
        })

        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': self.health_checks,
                'IsTruncated': False,
                'MaxItems': '100',
                'Marker': '',
            })

        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })

        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, {
                'HostedZoneId': 'z42',
                'ChangeBatch': ANY
            })

        plan = provider.plan(self.expected)

        return provider, plan

    # _get_test_plan() returns a plan with 11 modifications, 17 RRs

    @patch('octodns.provider.route53.Route53Provider._really_apply')
    def test_apply_1(self, really_apply_mock):

        # 18 RRs with max of 19 should only get applied in one call
        provider, plan = self._get_test_plan(19)
        provider.apply(plan)
        really_apply_mock.assert_called_once()

    @patch('octodns.provider.route53.Route53Provider._really_apply')
    def test_apply_2(self, really_apply_mock):

        # 18 RRs with max of 17 should only get applied in two calls
        provider, plan = self._get_test_plan(18)
        provider.apply(plan)
        self.assertEquals(2, really_apply_mock.call_count)

    @patch('octodns.provider.route53.Route53Provider._really_apply')
    def test_apply_3(self, really_apply_mock):

        # with a max of seven modifications, four calls
        provider, plan = self._get_test_plan(7)
        provider.apply(plan)
        self.assertEquals(4, really_apply_mock.call_count)

    @patch('octodns.provider.route53.Route53Provider._really_apply')
    def test_apply_4(self, really_apply_mock):

        # with a max of 11 modifications, two calls
        provider, plan = self._get_test_plan(11)
        provider.apply(plan)
        self.assertEquals(2, really_apply_mock.call_count)

    @patch('octodns.provider.route53.Route53Provider._really_apply')
    def test_apply_bad(self, really_apply_mock):

        # with a max of 1 modifications, fail
        provider, plan = self._get_test_plan(1)
        with self.assertRaises(Exception) as ctx:
            provider.apply(plan)
        self.assertTrue('modifications' in ctx.exception.message)

    def test_semicolon_fixup(self):
        provider = Route53Provider('test', 'abc', '123')

        self.assertEquals(
            {
                'type': 'TXT',
                'ttl': 30,
                'values': [
                    'abcd\\; ef\\;g',
                    'hij\\; klm\\;n',
                ],
            },
            provider._data_for_quoted({
                'ResourceRecords': [{
                    'Value': '"abcd; ef;g"',
                }, {
                    'Value': '"hij\\; klm\\;n"',
                }],
                'TTL':
                30,
                'Type':
                'TXT',
            }))

    def test_client_max_attempts(self):
        provider = Route53Provider('test',
                                   'abc',
                                   '123',
                                   client_max_attempts=42)
        # NOTE: this will break if boto ever changes the impl details...
        self.assertEquals(
            43, provider._conn.meta.events.
            _unique_id_handlers['retry-config-route53']
            ['handler']._checker.__dict__['_max_attempts'])
    def test_route53_record(self):
        existing = Zone('unit.tests.', [])
        record_a = Record.new(
            existing, '', {
                'geo': {
                    'NA-US': ['2.2.2.2', '3.3.3.3'],
                    'OC': ['4.4.4.4', '5.5.5.5']
                },
                'ttl': 99,
                'type': 'A',
                'values': ['9.9.9.9']
            })
        a = _Route53Record(None, record_a, False)
        self.assertEquals(a, a)
        b = _Route53Record(
            None,
            Record.new(existing, '', {
                'ttl': 32,
                'type': 'A',
                'values': ['8.8.8.8', '1.1.1.1']
            }), False)
        self.assertEquals(b, b)
        c = _Route53Record(
            None,
            Record.new(existing, 'other', {
                'ttl': 99,
                'type': 'A',
                'values': ['9.9.9.9']
            }), False)
        self.assertEquals(c, c)
        d = _Route53Record(
            None,
            Record.new(
                existing, '', {
                    'ttl': 42,
                    'type': 'MX',
                    'value': {
                        'preference': 10,
                        'exchange': 'foo.bar.'
                    }
                }), False)
        self.assertEquals(d, d)

        # Same fqdn & type is same record
        self.assertEquals(a, b)
        # Same name & different type is not the same
        self.assertNotEquals(a, d)
        # Different name & same type is not the same
        self.assertNotEquals(a, c)

        # Same everything, different class is not the same
        e = _Route53GeoDefault(None, record_a, False)
        self.assertNotEquals(a, e)

        class DummyProvider(object):
            def get_health_check_id(self, *args, **kwargs):
                return None

        provider = DummyProvider()
        f = _Route53GeoRecord(provider, record_a, 'NA-US',
                              record_a.geo['NA-US'], False)
        self.assertEquals(f, f)
        g = _Route53GeoRecord(provider, record_a, 'OC', record_a.geo['OC'],
                              False)
        self.assertEquals(g, g)

        # Geo and non-geo are not the same, using Geo as primary to get it's
        # __cmp__
        self.assertNotEquals(f, a)
        # Same everything, different geo's is not the same
        self.assertNotEquals(f, g)

        # Make sure it doesn't blow up
        a.__repr__()
        e.__repr__()
        f.__repr__()
    def _get_test_plan(self, max_changes):

        provider = Route53Provider('test', 'abc', '123', max_changes)

        # Use the stubber
        stubber = Stubber(provider._conn)
        stubber.activate()

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

        list_hosted_zones_resp = {
            'HostedZones': [],
            'Marker': 'm',
            'IsTruncated': False,
            'MaxItems': '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        create_hosted_zone_resp = {
            'HostedZone': {
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            },
            'ChangeInfo': {
                'Id': 'a12',
                'Status': 'PENDING',
                'SubmittedAt': '2017-01-29T01:02:03Z',
                'Comment': 'hrm',
            },
            'DelegationSet': {
                'Id': 'b23',
                'CallerReference': 'blip',
                'NameServers': [
                    'n12.unit.tests.',
                ],
            },
            'Location': 'us-east-1',
        }
        stubber.add_response('create_hosted_zone', create_hosted_zone_resp, {
            'Name': got.name,
            'CallerReference': ANY,
        })

        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': self.health_checks,
                'IsTruncated': False,
                'MaxItems': '100',
                'Marker': '',
            })

        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })
        stubber.add_response('delete_health_check', {}, {
            'HealthCheckId': ANY,
        })

        stubber.add_response(
            'change_resource_record_sets', {
                'ChangeInfo': {
                    'Id': 'id',
                    'Status': 'PENDING',
                    'SubmittedAt': '2017-01-29T01:02:03Z',
                }
            }, {
                'HostedZoneId': 'z42',
                'ChangeBatch': ANY
            })

        plan = provider.plan(self.expected)

        return provider, plan
Beispiel #14
0
    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]
Beispiel #15
0
    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(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]
Beispiel #16
0
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 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('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('Authentication 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-existant 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-existant 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(9, 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(9, 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] * 16  # individual record creates

        # non-existant zone, create everything
        plan = provider.plan(self.expected)
        self.assertEquals(9, len(plan.changes))
        self.assertEquals(9, provider.apply(plan))

        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(18, 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))
        # recreate for update, and deletes for the 2 parts of the other
        provider._request.assert_has_calls([
            call('POST',
                 '/zones/42/dns_records',
                 data={
                     'content': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl.unit.tests',
                     'ttl': 300
                 }),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997655'),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997653'),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997654')
        ])
#
#

from __future__ import absolute_import, division, print_function, \
    unicode_literals

from octodns.record import Create, Delete, Update, Record
from octodns.provider.googlecloud import GoogleCloudProvider

from octodns.zone import Zone
from octodns.provider.base import Plan, BaseProvider

from unittest import TestCase
from mock import Mock, patch, PropertyMock

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(Record.new(zone, 'aaa', {
    'ttl': 2,
Beispiel #18
0
    def test_populate(self):
        provider = CloudflareProvider('test', 'email', 'token')

        # 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('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('Authentication 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-existant 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-existant 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(9, 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(9, len(again.records))
    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))
Beispiel #20
0
def main():
    """check-zone based on octodns config file and dns zone
    Will query all 4 DNS servers configured for the zone in GCP.
    """
    parser = ArgumentParser(description=__doc__.split('\n')[1])

    parser.add_argument('--config-file',
                        required=True,
                        help='The OctoDNS configuration file to use')
    parser.add_argument('--zone',
                        action='append',
                        required=True,
                        help='zone to check')

    args = parser.parse_args()

    manager = Manager(args.config_file)

    for zone_name in args.zone:
        print('Checking records for {}'.format(zone_name))
        zone = Zone(zone_name, manager.configured_sub_zones(zone_name))

        # Read our YAML configuration
        yaml_config = manager.providers['config']

        # Build a GCP provider in our project to read the nameservers from it
        gcp = manager.providers['gcp']
        project = gcp.gcloud_client.project

        # Retrieve the DNS Servers directly from the GCP configuration
        dns_servers = gcp.gcloud_zones[zone_name].name_servers

        # k8s.io resolvers for testing without access to gcp
        #dns_servers = ["NS-CLOUD-D1.GOOGLEDOMAINS.COM", "NS-CLOUD-D2.GOOGLEDOMAINS.COM", "NS-CLOUD-D3.GOOGLEDOMAINS.COM", "NS-CLOUD-D4.GOOGLEDOMAINS.COM"]
        print('Using GCP project {}'.format(project))
        print('name,type,ttl,{},consistent'.format(','.join(dns_servers)))

        # Populate the zone with those records defined in our YAML config
        yaml_config.populate(zone)

        # This would populate the zone with records already defined in Google Cloud DNS
        # gcp.populate(zone)

        # Configure Resolvers (one per DNS server)
        resolvers = configure_resolvers(dns_servers)

        # Populate the queries to make based on zone record configuration
        queries = {}
        for record in sorted(zone.records):
            queries[record] = [
                r.query(record.fqdn, record._type) for r in resolvers
            ]
        # No dns_error unless we find one
        dns_error = False

        dns_error = verify_dns(queries)

        if dns_error:
            sys.exit(1)

    sys.exit(0)
Beispiel #21
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', 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='{"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(1, 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-existent 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 some of the record with expected data
            call('POST', '/zones/unit.tests/records', data={
                'content': '1.2.3.4',
                'type': 'A',
                'name': '',
                'ttl': 300}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '1.2.3.5',
                'type': 'A',
                'name': '',
                'ttl': 300}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '0 issue "ca.unit.tests"',
                'type': 'CAA',
                'name': '',
                'ttl': 3600}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49',
                'type': 'SSHFP',
                'name': '',
                'ttl': 3600}),
            call('POST', '/zones/unit.tests/records', data={
                'content': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73',
                'type': 'SSHFP',
                'name': '',
                'ttl': 3600}),
            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)
Beispiel #22
0
 def test_missing_dot(self):
     with self.assertRaises(Exception) as ctx:
         Zone('not.allowed', [])
     self.assertTrue('missing ending dot' in ctx.exception.message)
Beispiel #23
0
    def test_provider(self):
        provider = PowerDnsProvider('test',
                                    'non.existent',
                                    'api-key',
                                    nameserver_values=['8.8.8.8.', '9.9.9.9.'])

        # Test version detection
        with requests_mock() as mock:
            mock.get('http://non.existent:8081/api/v1/servers/localhost',
                     status_code=200,
                     json={'version': "4.1.10"})
            self.assertEquals(provider.powerdns_version, [4, 1, 10])

        # Bad auth
        with requests_mock() as mock:
            mock.get(ANY, status_code=401, text='Unauthorized')

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertTrue('unauthorized' in 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 in PowerDNS <4.3.0 doesn't populate anything
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=422,
                     json={'error': "Could not find domain 'unit.tests.'"})
            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        # Non-existent zone in PowerDNS >=4.2.0 doesn't populate anything

        provider._powerdns_version = [4, 2, 0]
        with requests_mock() as mock:
            mock.get(ANY, status_code=404, text='Not Found')
            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(set(), zone.records)

        provider._powerdns_version = [4, 1, 0]

        # The rest of this is messy/complicated b/c it's dealing with mocking

        expected = Zone('unit.tests.', [])
        source = YamlProvider('test', join(dirname(__file__), 'config'))
        source.populate(expected)
        expected_n = len(expected.records) - 3
        self.assertEquals(16, expected_n)

        # No diffs == no changes
        with requests_mock() as mock:
            mock.get(ANY, status_code=200, text=FULL_TEXT)

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

        # Used in a minute
        def assert_rrsets_callback(request, context):
            data = loads(request.body)
            self.assertEquals(expected_n, len(data['rrsets']))
            return ''

        # No existing records -> creates for every record in expected
        with requests_mock() as mock:
            mock.get(ANY, status_code=200, text=EMPTY_TEXT)
            # post 201, is response to the create with data
            mock.patch(ANY, status_code=201, text=assert_rrsets_callback)

            plan = provider.plan(expected)
            self.assertEquals(expected_n, len(plan.changes))
            self.assertEquals(expected_n, provider.apply(plan))
            self.assertTrue(plan.exists)

        # Non-existent zone -> creates for every record in expected
        # OMG this is f*****g ugly, probably better to ditch requests_mocks and
        # just mock things for real as it doesn't seem to provide a way to get
        # at the request params or verify that things were called from what I
        # can tell
        not_found = {'error': "Could not find domain 'unit.tests.'"}
        with requests_mock() as mock:
            # get 422's, unknown zone
            mock.get(ANY, status_code=422, text=dumps(not_found))
            # patch 422's, unknown zone
            mock.patch(ANY, status_code=422, text=dumps(not_found))
            # post 201, is response to the create with data
            mock.post(ANY, status_code=201, text=assert_rrsets_callback)

            plan = provider.plan(expected)
            self.assertEquals(expected_n, len(plan.changes))
            self.assertEquals(expected_n, provider.apply(plan))
            self.assertFalse(plan.exists)

        provider._powerdns_version = [4, 2, 0]
        with requests_mock() as mock:
            # get 404's, unknown zone
            mock.get(ANY, status_code=404, text='')
            # patch 404's, unknown zone
            mock.patch(ANY, status_code=404, text=dumps(not_found))
            # post 201, is response to the create with data
            mock.post(ANY, status_code=201, text=assert_rrsets_callback)

            plan = provider.plan(expected)
            self.assertEquals(expected_n, len(plan.changes))
            self.assertEquals(expected_n, provider.apply(plan))
            self.assertFalse(plan.exists)

        provider._powerdns_version = [4, 1, 0]
        with requests_mock() as mock:
            # get 422's, unknown zone
            mock.get(ANY, status_code=422, text=dumps(not_found))
            # patch 422's,
            data = {'error': "Key 'name' not present or not a String"}
            mock.patch(ANY, status_code=422, text=dumps(data))

            with self.assertRaises(HTTPError) as ctx:
                plan = provider.plan(expected)
                provider.apply(plan)
            response = ctx.exception.response
            self.assertEquals(422, response.status_code)
            self.assertTrue('error' in response.json())

        with requests_mock() as mock:
            # get 422's, unknown zone
            mock.get(ANY, status_code=422, text=dumps(not_found))
            # patch 500's, things just blew up
            mock.patch(ANY, status_code=500, text='')

            with self.assertRaises(HTTPError):
                plan = provider.plan(expected)
                provider.apply(plan)

        with requests_mock() as mock:
            # get 422's, unknown zone
            mock.get(ANY, status_code=422, text=dumps(not_found))
            # patch 500's, things just blew up
            mock.patch(ANY, status_code=422, text=dumps(not_found))
            # post 422's, something wrong with create
            mock.post(ANY, status_code=422, text='Hello Word!')

            with self.assertRaises(HTTPError):
                plan = provider.plan(expected)
                provider.apply(plan)
Beispiel #24
0
 def test_lowering(self):
     zone = Zone('UniT.TEsTs.', [])
     self.assertEquals('unit.tests.', zone.name)
#
#
#

from __future__ import absolute_import, division, print_function, \
    unicode_literals

from unittest import TestCase

from octodns.processor.ownership import OwnershipProcessor
from octodns.record import Delete, Record
from octodns.zone import Zone

from helpers import PlannableProvider

zone = Zone('unit.tests.', [])
records = {}
for record in [
        Record.new(zone, '', {
            'ttl': 30,
            'type': 'A',
            'values': [
                '1.2.3.4',
                '5.6.7.8',
            ],
        }),
        Record.new(zone, 'the-a', {
            'ttl': 30,
            'type': 'A',
            'value': '1.2.3.4',
        }),
Beispiel #26
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) - 7
        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(19, 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)
Beispiel #27
0
 def make_expected(self):
     expected = Zone('unit.tests.', [])
     source = YamlProvider('test', join(dirname(__file__), 'config'))
     source.populate(expected)
     return expected
Beispiel #28
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.'
        }))

    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(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]

    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) - 7
        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(19, 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)
    def test_extra_change_has_wrong_health_check(self):
        provider, stubber = self._get_stubbed_provider()

        list_hosted_zones_resp = {
            'HostedZones': [{
                'Name': 'unit.tests.',
                'Id': 'z42',
                'CallerReference': 'abc',
            }],
            'Marker':
            'm',
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})

        # record with geo and no health check returns change
        existing = Zone('unit.tests.', [])
        record = Record.new(
            existing, 'a', {
                'ttl': 30,
                'type': 'A',
                'value': '1.2.3.4',
                'geo': {
                    'NA': ['2.2.3.4'],
                }
            })
        existing.add_record(record)
        list_resource_record_sets_resp = {
            'ResourceRecordSets': [{
                'Name': 'a.unit.tests.',
                'Type': 'A',
                'GeoLocation': {
                    'ContinentCode': 'NA',
                },
                'ResourceRecords': [{
                    'Value': '2.2.3.4',
                }],
                'TTL': 61,
                'HealthCheckId': '42',
            }],
            'IsTruncated':
            False,
            'MaxItems':
            '100',
        }
        stubber.add_response('list_resource_record_sets',
                             list_resource_record_sets_resp,
                             {'HostedZoneId': 'z42'})
        stubber.add_response(
            'list_health_checks', {
                'HealthChecks': [{
                    'Id': '42',
                    'CallerReference': 'foo',
                    'HealthCheckConfig': {
                        'Type': 'HTTPS',
                        'FullyQualifiedDomainName': 'unit.tests',
                        'IPAddress': '2.2.3.4',
                    },
                    'HealthCheckVersion': 2,
                }],
                'IsTruncated':
                False,
                'MaxItems':
                '100',
                'Marker':
                '',
            })
        extra = provider._extra_changes(existing, [])
        self.assertEquals(1, len(extra))
        stubber.assert_no_pending_responses()

        for change in (Create(record), Update(record, record), Delete(record)):
            extra = provider._extra_changes(existing, [change])
            self.assertEquals(0, len(extra))
            stubber.assert_no_pending_responses()
Beispiel #30
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)