def test_apply_unescapes_semicolons(self):
        desired = Zone('unit.tests.', [])
        source = YamlProvider('test', join(dirname(__file__), 'config'))
        source.populate(desired)

        # save the rrsets sent to the API so we can assert them later.
        patch_rrsets = []

        def save_rrsets_callback(request, context):
            data = loads(request.body)
            patch_rrsets.extend(data['rrsets'])
            return ''

        provider = PowerDnsProvider('test', 'non.existant', 'api-key')
        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=save_rrsets_callback)
            plan = provider.plan(desired)
            provider.apply(plan)

        txts = [c for c in patch_rrsets if c['type'] == 'TXT']
        self.assertEquals(
            '"v=DKIM1;k=rsa;s=email;h=sha256;'
            'p=A/kinda+of/long/string+with+numb3rs"',
            txts[0]['records'][2]['content'])
Exemplo n.º 2
0
    def test_unsorted(self):
        source = YamlProvider('test', join(dirname(__file__), 'config'))

        zone = Zone('unordered.', [])

        with self.assertRaises(ConstructorError):
            source.populate(zone)
Exemplo n.º 3
0
    def test_empty(self):
        source = YamlProvider('test', join(dirname(__file__), 'config'))

        zone = Zone('empty.', [])

        # without it we see everything
        source.populate(zone)
        self.assertEquals(0, len(zone.records))
Exemplo n.º 4
0
    def test_subzone_handling(self):
        source = YamlProvider('test', join(dirname(__file__), 'config'))

        # If we add `sub` as a sub-zone we'll reject `www.sub`
        zone = Zone('unit.tests.', ['sub'])
        with self.assertRaises(SubzoneRecordException) as ctx:
            source.populate(zone)
        self.assertEquals('Record www.sub.unit.tests. is under a managed '
                          'subzone', text_type(ctx.exception))
Exemplo n.º 5
0
    def test_provider(self):
        config = join(dirname(__file__), 'config')
        override_config = join(dirname(__file__), 'config', 'override')
        base = YamlProvider('base', config, populate_should_replace=False)
        override = YamlProvider('test', override_config,
                                populate_should_replace=True)

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

        # Load the base, should see the 5 records
        base.populate(zone)
        got = {r.name: r for r in zone.records}
        self.assertEquals(6, len(got))
        # We get the "dynamic" A from the base config
        self.assertTrue('dynamic' in got['a'].data)
        # No added
        self.assertFalse('added' in got)

        # Load the overrides, should replace one and add 1
        override.populate(zone)
        got = {r.name: r for r in zone.records}
        self.assertEquals(7, len(got))
        # 'a' was replaced with a generic record
        self.assertEquals({
            'ttl': 3600,
            'values': ['4.4.4.4', '5.5.5.5']
        }, got['a'].data)
        # And we have the new one
        self.assertTrue('added' in got)
Exemplo n.º 6
0
    def test_small_change(self):
        provider = PowerDnsProvider('test', 'non.existent', 'api-key')

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

        # A small change to a single record
        with requests_mock() as mock:
            mock.get(ANY, status_code=200, text=FULL_TEXT)
            mock.get('http://non.existent:8081/api/v1/servers/localhost',
                     status_code=200,
                     json={'version': '4.1.0'})

            missing = Zone(expected.name, [])
            # Find and delete the SPF record
            for record in expected.records:
                if record._type != 'SPF':
                    missing.add_record(record)

            def assert_delete_callback(request, context):
                self.assertEquals(
                    {
                        'rrsets': [{
                            'records': [{
                                'content': '"v=spf1 ip4:192.168.0.1/16-all"',
                                'disabled': False
                            }],
                            'changetype':
                            'DELETE',
                            'type':
                            'SPF',
                            'name':
                            'spf.unit.tests.',
                            'ttl':
                            600
                        }]
                    }, loads(request.body))
                return ''

            mock.patch(ANY, status_code=201, text=assert_delete_callback)

            plan = provider.plan(missing)
            self.assertEquals(1, len(plan.changes))
            self.assertEquals(1, provider.apply(plan))
Exemplo n.º 7
0
    def test_unsorted(self):
        source = YamlProvider('test', join(dirname(__file__), 'config'))

        zone = Zone('unordered.', [])

        with self.assertRaises(ConstructorError):
            source.populate(zone)

        source = YamlProvider('test', join(dirname(__file__), 'config'),
                              enforce_order=False)
        # no exception
        source.populate(zone)
        self.assertEqual(2, len(zone.records))
Exemplo n.º 8
0
def octodns_test_zone():
    '''Load the unit.tests zone config into an octodns.zone.Zone object.'''

    zone = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(zone)
    # Replace the unit test fixture's NS record with one of ours.
    remove_octodns_record(zone, '', 'NS')
    zone.add_record(
        Record.new(
            zone, '', {
                'ttl':
                3600,
                'type':
                'NS',
                'values': [
                    'dns1.p01.nsone.net.', 'dns2.p01.nsone.net.',
                    'dns3.p01.nsone.net.', 'dns4.p01.nsone.net.'
                ]
            }))
    return zone
Exemplo n.º 9
0
    def test_copy(self):
        # going to put some sentinal values in here to ensure, these aren't
        # valid, but we shouldn't hit any code that cares during this test
        source = YamlProvider(
            'test',
            42,
            default_ttl=43,
            enforce_order=44,
            populate_should_replace=45,
            supports_root_ns=46,
        )
        copy = source.copy()
        self.assertEqual(source.directory, copy.directory)
        self.assertEqual(source.default_ttl, copy.default_ttl)
        self.assertEqual(source.enforce_order, copy.enforce_order)
        self.assertEqual(source.populate_should_replace,
                         copy.populate_should_replace)
        self.assertEqual(source.supports_root_ns, copy.supports_root_ns)

        # same for split
        source = SplitYamlProvider(
            'test',
            42,
            extension=42.5,
            default_ttl=43,
            enforce_order=44,
            populate_should_replace=45,
            supports_root_ns=46,
        )
        copy = source.copy()
        self.assertEqual(source.directory, copy.directory)
        self.assertEqual(source.extension, copy.extension)
        self.assertEqual(source.default_ttl, copy.default_ttl)
        self.assertEqual(source.enforce_order, copy.enforce_order)
        self.assertEqual(source.populate_should_replace,
                         copy.populate_should_replace)
        self.assertEqual(source.supports_root_ns, copy.supports_root_ns)
class TestDigitalOceanProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

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

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

        # General error
        with requests_mock() as mock:
            mock.get(ANY, status_code=502, text='Things caught fire')

            with self.assertRaises(HTTPError) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals(502, ctx.exception.response.status_code)

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

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

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

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

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

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

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

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

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

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

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

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

        provider._client._request.reset_mock()

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

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

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

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/domains/unit.tests/records/11189899'),
            call('DELETE', '/domains/unit.tests/records/11189897'),
            call('DELETE', '/domains/unit.tests/records/11189898')
        ],
                                                   any_order=True)
Exemplo n.º 11
0
class TestEasyDNSProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

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

        # Bad request
        with requests_mock() as mock:
            mock.get(ANY, status_code=400,
                     text='{"id":"invalid",'
                     '"message":"Bad request"}')

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

        # General error
        with requests_mock() as mock:
            mock.get(ANY, status_code=502, text='Things caught fire')

            with self.assertRaises(HTTPError) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals(502, ctx.exception.response.status_code)

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

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

        # No diffs == no changes
        with requests_mock() as mock:
            base = 'https://rest.easydns.net/zones/records/'
            with open('tests/fixtures/easydns-records.json') as fh:
                mock.get('{}{}'.format(base, 'parsed/unit.tests'),
                         text=fh.read())
            with open('tests/fixtures/easydns-records.json') as fh:
                mock.get('{}{}'.format(base, 'all/unit.tests'),
                         text=fh.read())

                provider.populate(zone)
                self.assertEquals(13, 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(13, len(again.records))

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

    def test_domain(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')

        with requests_mock() as mock:
            base = 'https://rest.easydns.net/'
            mock.get('{}{}'.format(base, 'domain/unit.tests'), status_code=400,
                     text='{"id":"not_found","message":"The resource you '
                     'were accessing could not be found."}')

            with self.assertRaises(Exception) as ctx:
                provider._client.domain('unit.tests')

            self.assertEquals('Not Found', text_type(ctx.exception))

    def test_apply_not_found(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')

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

        with requests_mock() as mock:
            base = 'https://rest.easydns.net/'
            mock.get('{}{}'.format(base, 'domain/unit.tests'), status_code=404,
                     text='{"id":"not_found","message":"The resource you '
                     'were accessing could not be found."}')
            mock.put('{}{}'.format(base, 'domains/add/unit.tests'),
                     status_code=200,
                     text='{"id":"OK","message":"Zone created."}')
            mock.get('{}{}'.format(base, 'zones/records/parsed/unit.tests'),
                     status_code=404,
                     text='{"id":"not_found","message":"The resource you '
                     'were accessing could not be found."}')
            mock.get('{}{}'.format(base, 'zones/records/all/unit.tests'),
                     status_code=404,
                     text='{"id":"not_found","message":"The resource you '
                     'were accessing could not be found."}')

            plan = provider.plan(wanted)
            self.assertFalse(plan.exists)
            self.assertEquals(1, len(plan.changes))
            with self.assertRaises(Exception) as ctx:
                provider.apply(plan)

            self.assertEquals('Not Found', text_type(ctx.exception))

    def test_domain_create(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')
        domain_after_creation = {
            "tm": 1000000000,
            "data": [{
                "id": "12341001",
                "domain": "unit.tests",
                "host": "@",
                "ttl": "0",
                "prio": "0",
                "type": "SOA",
                "rdata": "dns1.easydns.com. zone.easydns.com. "
                "2020010101 3600 600 604800 0",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }, {
                "id": "12341002",
                "domain": "unit.tests",
                "host": "@",
                "ttl": "0",
                "prio": "0",
                "type": "NS",
                "rdata": "LOCAL.",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }, {
                "id": "12341003",
                "domain": "unit.tests",
                "host": "@",
                "ttl": "0",
                "prio": "0",
                "type": "MX",
                "rdata": "LOCAL.",
                "geozone_id": "0",
                "last_mod": "2020-01-01 01:01:01"
            }],
            "count": 3,
            "total": 3,
            "start": 0,
            "max": 1000,
            "status": 200
        }
        with requests_mock() as mock:
            base = 'https://rest.easydns.net/'
            mock.put('{}{}'.format(base, 'domains/add/unit.tests'),
                     status_code=201, text='{"id":"OK"}')
            mock.get('{}{}'.format(base, 'zones/records/all/unit.tests'),
                     text=json.dumps(domain_after_creation))
            mock.delete(ANY, text='{"id":"OK"}')
            provider._client.domain_create('unit.tests')

    def test_caa(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')

        # Invalid rdata records
        caa_record_invalid = [{
            "domain": "unit.tests",
            "host": "@",
            "ttl": "3600",
            "prio": "0",
            "type": "CAA",
            "rdata": "0",
        }]

        # Valid rdata records
        caa_record_valid = [{
            "domain": "unit.tests",
            "host": "@",
            "ttl": "3600",
            "prio": "0",
            "type": "CAA",
            "rdata": "0 issue ca.unit.tests",
        }]

        provider._data_for_CAA('CAA', caa_record_invalid)
        provider._data_for_CAA('CAA', caa_record_valid)

    def test_naptr(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')

        # Invalid rdata records
        naptr_record_invalid = [{
            "domain": "unit.tests",
            "host": "naptr",
            "ttl": "600",
            "prio": "10",
            "type": "NAPTR",
            "rdata": "100",
        }]

        # Valid rdata records
        naptr_record_valid = [{
            "domain": "unit.tests",
            "host": "naptr",
            "ttl": "600",
            "prio": "10",
            "type": "NAPTR",
            "rdata": "10 10 'U' 'SIP+D2U' '!^.*$!sip:[email protected]!' .",
        }]

        provider._data_for_NAPTR('NAPTR', naptr_record_invalid)
        provider._data_for_NAPTR('NAPTR', naptr_record_valid)

    def test_srv(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')

        # Invalid rdata records
        srv_invalid = [{
            "domain": "unit.tests",
            "host": "_srv._tcp",
            "ttl": "600",
            "type": "SRV",
            "rdata": "",
        }]
        srv_invalid2 = [{
            "domain": "unit.tests",
            "host": "_srv._tcp",
            "ttl": "600",
            "type": "SRV",
            "rdata": "11",
        }]
        srv_invalid3 = [{
            "domain": "unit.tests",
            "host": "_srv._tcp",
            "ttl": "600",
            "type": "SRV",
            "rdata": "12 30",
        }]
        srv_invalid4 = [{
            "domain": "unit.tests",
            "host": "_srv._tcp",
            "ttl": "600",
            "type": "SRV",
            "rdata": "13 40 1234",
        }]

        # Valid rdata
        srv_valid = [{
            "domain": "unit.tests",
            "host": "_srv._tcp",
            "ttl": "600",
            "type": "SRV",
            "rdata": "100 20 5678 foo-2.unit.tests.",
        }]

        srv_invalid_content = provider._data_for_SRV('SRV', srv_invalid)
        srv_invalid_content2 = provider._data_for_SRV('SRV', srv_invalid2)
        srv_invalid_content3 = provider._data_for_SRV('SRV', srv_invalid3)
        srv_invalid_content4 = provider._data_for_SRV('SRV', srv_invalid4)
        srv_valid_content = provider._data_for_SRV('SRV', srv_valid)

        self.assertEqual(srv_valid_content['values'][0]['priority'], 100)
        self.assertEqual(srv_invalid_content['values'][0]['priority'], 0)
        self.assertEqual(srv_invalid_content2['values'][0]['priority'], 11)
        self.assertEqual(srv_invalid_content3['values'][0]['priority'], 12)
        self.assertEqual(srv_invalid_content4['values'][0]['priority'], 13)

        self.assertEqual(srv_valid_content['values'][0]['weight'], 20)
        self.assertEqual(srv_invalid_content['values'][0]['weight'], 0)
        self.assertEqual(srv_invalid_content2['values'][0]['weight'], 0)
        self.assertEqual(srv_invalid_content3['values'][0]['weight'], 30)
        self.assertEqual(srv_invalid_content4['values'][0]['weight'], 40)

        self.assertEqual(srv_valid_content['values'][0]['port'], 5678)
        self.assertEqual(srv_invalid_content['values'][0]['port'], 0)
        self.assertEqual(srv_invalid_content2['values'][0]['port'], 0)
        self.assertEqual(srv_invalid_content3['values'][0]['port'], 0)
        self.assertEqual(srv_invalid_content4['values'][0]['port'], 1234)

        self.assertEqual(srv_valid_content['values'][0]['target'],
                         'foo-2.unit.tests.')
        self.assertEqual(srv_invalid_content['values'][0]['target'], '')
        self.assertEqual(srv_invalid_content2['values'][0]['target'], '')
        self.assertEqual(srv_invalid_content3['values'][0]['target'], '')
        self.assertEqual(srv_invalid_content4['values'][0]['target'], '')

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

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

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

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

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

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

        provider._client._request.reset_mock()

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

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

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

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('PUT', '/zones/records/add/unit.tests/A', data={
                'rdata': '1.2.3.4',
                'name': 'test1',
                'ttl': 300,
                'type': 'A',
                'host': 'test1',
            }),
            call('DELETE', '/zones/records/unit.tests/12342001'),
            call('DELETE', '/zones/records/unit.tests/12342002'),
            call('DELETE', '/zones/records/unit.tests/12342003')
        ], any_order=True)
Exemplo n.º 12
0
class TestEdgeDnsProvider(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 = AkamaiProvider("test", "secret", "akam.com", "atok", "ctok")

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

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

            self.assertEquals(401, ctx.exception.response.status_code)

        # general error
        with requests_mock() as mock:
            mock.get(ANY, status_code=502, text='Things caught fire')

            with self.assertRaises(HTTPError) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals(502, ctx.exception.response.status_code)

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

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

        # No diffs == no changes
        with requests_mock() as mock:

            with open('tests/fixtures/edgedns-records.json') as fh:
                mock.get(ANY, text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(18, 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(18, len(again.records))

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

    def test_apply(self):
        provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok",
                                  "cid", "gid")

        # tests create update delete through previous state config json
        with requests_mock() as mock:

            with open('tests/fixtures/edgedns-records-prev.json') as fh:
                mock.get(ANY, text=fh.read())

            plan = provider.plan(self.expected)
            mock.post(ANY, status_code=201)
            mock.put(ANY, status_code=200)
            mock.delete(ANY, status_code=204)

            changes = provider.apply(plan)
            self.assertEquals(31, changes)

        # Test against a zone that doesn't exist yet
        with requests_mock() as mock:
            with open('tests/fixtures/edgedns-records-prev-other.json') as fh:
                mock.get(ANY, status_code=404)

            plan = provider.plan(self.expected)
            mock.post(ANY, status_code=201)
            mock.put(ANY, status_code=200)
            mock.delete(ANY, status_code=204)

            changes = provider.apply(plan)
            self.assertEquals(16, changes)

        # Test against a zone that doesn't exist yet, but gid not provided
        with requests_mock() as mock:
            with open('tests/fixtures/edgedns-records-prev-other.json') as fh:
                mock.get(ANY, status_code=404)
            provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok",
                                      "cid")
            plan = provider.plan(self.expected)
            mock.post(ANY, status_code=201)
            mock.put(ANY, status_code=200)
            mock.delete(ANY, status_code=204)

            changes = provider.apply(plan)
            self.assertEquals(16, changes)

        # Test against a zone that doesn't exist, but cid not provided

        with requests_mock() as mock:
            mock.get(ANY, status_code=404)

            provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok")
            plan = provider.plan(self.expected)
            mock.post(ANY, status_code=201)
            mock.put(ANY, status_code=200)
            mock.delete(ANY, status_code=204)

            try:
                changes = provider.apply(plan)
            except NameError as e:
                expected = "contractId not specified to create zone"
                self.assertEquals(text_type(e), expected)
class TestCloudflareProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

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

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

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

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

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

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

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

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

        # General error
        with requests_mock() as mock:
            mock.get(ANY, status_code=502, text='Things caught fire')

            with self.assertRaises(HTTPError) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals(502, ctx.exception.response.status_code)

        # Non-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(12, len(zone.records))

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

            self.assertEquals(0, len(changes))

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

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

        provider._request = Mock()

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

        # non-existant zone, create everything
        plan = provider.plan(self.expected)
        self.assertEquals(12, len(plan.changes))
        self.assertEquals(12, 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(22, provider._request.call_count)

        provider._request.reset_mock()

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

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

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

        plan = provider.plan(wanted)
        # only see the delete & ttl update, below min-ttl is filtered out
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and deletes for the 2 parts of the other
        provider._request.assert_has_calls([
            call('PUT', '/zones/ff12ab34cd5611334422ab3322997650/dns_records/'
                 'fc12ab34cd5611334422ab3322997655',
                 data={
                     'content': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl.unit.tests',
                     'ttl': 300
                 }),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997653'),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997654')
        ])

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

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

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

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

        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call('PUT', '/zones/ff12ab34cd5611334422ab3322997650/dns_records/'
                 'fc12ab34cd5611334422ab3322997653',
                 data={
                     'content': '4.4.4.4',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'ttl': 300
                 }),
            call('POST',
                 '/zones/42/dns_records',
                 data={
                     'content': '3.3.3.3',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'ttl': 300
                 })
        ])

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

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

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

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

        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call(
                'DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
                'dns_records/fc12ab34cd5611334422ab3322997653')
        ])

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        plan = provider.plan(wanted)
        self.assertEquals(False, hasattr(plan, 'changes'))
    def test_provider(self):
        provider = PowerDnsProvider('test',
                                    'non.existent',
                                    'api-key',
                                    nameserver_values=['8.8.8.8.', '9.9.9.9.'])

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

        # 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) - 2
        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='')
            # 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)

        with requests_mock() as mock:
            # get 422's, unknown zone
            mock.get(ANY, status_code=422, text='')
            # 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='')
            # 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='')
            # 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)
Exemplo n.º 15
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')
        ])
class TestConstellixProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

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

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

    @staticmethod
    def _fixture(filename):
        return join(join(dirname(__file__), 'fixtures'), filename)

    @staticmethod
    def json_func(json_element):
        def json_inner_func():
            return json_element

        return json_inner_func

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

        # 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  - "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'
            with open(self._fixture('constellix-domains.json')) as fh:
                mock.get('{}{}'.format(base, '/domains'), text=fh.read())
            with open(self._fixture('constellix-records.json')) as fh:
                mock.get('{}{}'.format(base, '/domains/123123/records'),
                         text=fh.read())

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

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

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

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

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

        with open(self._fixture('constellix-domains.json')) as fh:
            domains = json.load(fh)

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

        # No root NS, no ignored, no excluded, no unsupported
        n = len(self.expected.records) - 4
        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', '/'),
        ])
        # 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
                }, {
                    u'priority': 12,
                    u'weight': 20,
                    u'value': 'foo-2.unit.tests.',
                    u'port': 30
                }],
                u'name': u'_srv._tcp',
                u'ttl': 600
            }),
            call(u'POST', u'/domains/123123/records/SRV', data={
                u'roundRobin': [{
                    u'priority': 10,
                    u'weight': 20,
                    u'value': 'foo-1.unit.tests.',
                    u'port': 30
                }, {
                    u'priority': 12,
                    u'weight': 20,
                    u'value': 'foo-2.unit.tests.',
                    u'port': 30
                }],
                u'name': u'_srv._tcp',
                u'ttl': 600
            }),
            call(u'POST', u'/domains/123123/records/CNAME', data={
                u'host': 'unit.tests.',
                u'name': u'cname',
                u'ttl': 300
            }),
            call(u'POST', u'/domains/123123/records/NS', data={
                u'roundRobin': [{
                    u'value': u'ns1.unit.tests.'
                }, {
                    u'value': u'ns2.unit.tests.'
                }],
                u'name': u'under',
                u'ttl': 3600
            }),
            call(u'POST', u'/domains/123123/records/TXT', data={
                u'roundRobin': [{
                    u'value': u'"Bah bah black sheep"'
                }, {
                    u'value': u'"have you any wool."'
                }, {
                    u'value': u'"v=DKIM1;k=rsa;s=email;h=sha256;' + \
                              'p=A/kinda+of/long/string+with+numb3rs"'
                }],
                u'name': u'txt',
                u'ttl': 600
            }),
            call(u'POST', u'/domains/123123/records/ANAME', data={
                u'roundRobin': [{
                    u'disableFlag': False,
                    u'value': u'aname.unit.tests.'
                }],
                u'name': u'sub',
                u'ttl': 1800
            }),
            call(u'POST', u'/domains/123123/records/A', data={
                u'roundRobin': [{
                    u'value': '1.2.3.4'
                }, {
                    u'value': '1.2.3.5'
                }],
                u'name': '',
                u'ttl': 300
            }),
            call(u'POST', u'/domains/123123/records/A', data={
                u'roundRobin': [{
                    u'value': '2.2.3.6'
                }],
                u'name': u'www',
                u'ttl': 300
            }),
            call(u'POST', u'/domains/123123/records/ANAME', data={
                u'roundRobin': [{
                    u'disableFlag': False,
                    u'value': u'aname.unit.tests.'
                }],
                u'name': u'',
                u'ttl': 1800
            }),
            call(u'POST', u'/domains/123123/records/CNAME', data={
                u'host': 'unit.tests.',
                u'name': u'included',
                u'ttl': 3600
            }),
            call(u'POST', u'/domains/123123/records/AAAA', data={
                u'roundRobin': [{
                    u'value': '2601:644:500:e210:62f8:1dff:feb8:947a'
                }],
                u'name': u'aaaa',
                u'ttl': 600
            }),
            call(u'POST', u'/domains/123123/records/MX', data={
                u'roundRobin': [{
                    u'value': 'smtp-4.unit.tests.', u'level': 10
                }, {
                    u'value': 'smtp-2.unit.tests.', u'level': 20
                }, {
                    u'value': 'smtp-3.unit.tests.', u'level': 30
                }, {
                    u'value': 'smtp-1.unit.tests.', u'level': 40
                }],
                u'name': u'mx',
                u'value': 'smtp-1.unit.tests.',
                u'ttl': 300
            }),
            call(u'POST', u'/domains/123123/records/CAA', data={
                u'roundRobin': [{
                    u'flag': 0,
                    u'tag': 'issue',
                    u'data': 'ca.unit.tests'
                }],
                u'name': '',
                u'ttl': 3600
            }),
            call(u'POST', u'/domains/123123/records/PTR', data={
                u'host': 'foo.bar.com.',
                u'name': u'ptr',
                u'ttl': 300
            }),
            call(u'POST', u'/domains/123123/records/A', data={
                u'roundRobin': [{
                    u'value': '2.2.3.6'
                }],
                u'name': u'www.sub',
                u'ttl': 300
            }),
            call(u'POST', u'/domains/123123/records/NAPTR', data={
                u'roundRobin': [{
                    u'service': 'SIP+D2U',
                    u'regularExpression': '!^.*$!sip:[email protected]!',
                    u'flags': 'S',
                    u'preference': 100,
                    u'order': 10,
                    u'replacement': '.'
                }, {
                    u'service': 'SIP+D2U',
                    u'regularExpression': '!^.*$!sip:[email protected]!',
                    u'flags': 'U',
                    u'preference': 100,
                    u'order': 100,
                    u'replacement': '.'
                }],
                u'name': u'naptr',
                u'ttl': 600
            }),
            call(u'POST', u'/domains/123123/records/SPF', data={
                u'roundRobin': [{
                    u'value': u'"v=spf1 ip4:192.168.0.1/16-all"'
                }],
                u'name': u'spf',
                u'ttl': 600
            })
        ])
        self.assertEquals(20, 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,
                '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 = ['{}']
        provider._client.domains = Mock(return_value={"unit.tests.": 123123})

        resp = Mock()
        resp.json = Mock(
            return_value={
                "id":
                123123,
                "name":
                "unit.tests.",
                "soa": {
                    "primaryNameserver": "ns11.constellix.com.",
                    "email": "dns.constellix.com.",
                    "ttl": 86400,
                    "serial": 2015010118,
                    "refresh": 43200,
                    "retry": 3600,
                    "expire": 1209600,
                    "negCache": 180
                },
                "createdTs":
                "2019-08-02T12:52:10Z",
                "modifiedTs":
                "2019-08-13T11:45:59Z",
                "typeId":
                1,
                "domainTags": [],
                "hasGtdRegions":
                False,
                "hasGeoIP":
                False,
                "nameserverGroup":
                1,
                "nameservers": [
                    "ns11.constellix.com.", "ns21.constellix.com.",
                    "ns31.constellix.com.", "ns41.constellix.net.",
                    "ns51.constellix.net.", "ns61.constellix.net."
                ],
                "note":
                "",
                "status":
                "ACTIVE",
            })
        provider._client._request = Mock(return_value=resp)

        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(u'GET', u'/domains/123123'),
            call(u'DELETE', u'/domains/123123/records/A/11189899'),
            call(u'POST',
                 u'/domains/123123/records/A',
                 data={
                     u'roundRobin': [{
                         u'value': u'3.2.3.4'
                     }],
                     u'name': u'ttl',
                     u'ttl': 300
                 }),
            call('DELETE', '/123123/records/A/11189897'),
            call('DELETE', '/123123/records/A/11189898'),
            call('DELETE', '/123123/records/ANAME/11189899')
        ],
                                                   any_order=True)
Exemplo n.º 17
0
    def test_provider(self):
        source = YamlProvider('test', join(dirname(__file__), 'config'))

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

        # With target we don't add anything
        source.populate(zone, target=source)
        self.assertEqual(0, len(zone.records))

        # without it we see everything
        source.populate(zone)
        self.assertEqual(25, len(zone.records))

        source.populate(dynamic_zone)
        self.assertEqual(6, len(dynamic_zone.records))

        # Assumption here is that a clean round-trip means that everything
        # worked as expected, data that went in came back out and could be
        # pulled in yet again and still match up. That assumes that the input
        # data completely exercises things. This assumption can be tested by
        # relatively well by running
        #   ./script/coverage tests/test_octodns_provider_yaml.py and
        # looking at the coverage file
        #   ./htmlcov/octodns_provider_yaml_py.html

        with TemporaryDirectory() as td:
            # Add some subdirs to make sure that it can create them
            directory = join(td.dirname, 'sub', 'dir')
            yaml_file = join(directory, 'unit.tests.yaml')
            dynamic_yaml_file = join(directory, 'dynamic.tests.yaml')
            target = YamlProvider('test', directory, supports_root_ns=False)

            # We add everything
            plan = target.plan(zone)
            self.assertEqual(
                22, len([c for c in plan.changes if isinstance(c, Create)]))
            self.assertFalse(isfile(yaml_file))

            # Now actually do it
            self.assertEqual(22, target.apply(plan))
            self.assertTrue(isfile(yaml_file))

            # Dynamic plan
            plan = target.plan(dynamic_zone)
            self.assertEqual(
                6, len([c for c in plan.changes if isinstance(c, Create)]))
            self.assertFalse(isfile(dynamic_yaml_file))
            # Apply it
            self.assertEqual(6, target.apply(plan))
            self.assertTrue(isfile(dynamic_yaml_file))

            # There should be no changes after the round trip
            reloaded = Zone('unit.tests.', [])
            target.populate(reloaded)
            self.assertDictEqual(
                {'included': ['test']},
                [x for x in reloaded.records
                 if x.name == 'included'][0]._octodns,
            )

            # manually copy over the root since it will have been ignored
            # when things were written out
            reloaded.add_record(zone.root_ns)

            self.assertFalse(zone.changes(reloaded, target=source))

            # A 2nd sync should still create everything
            plan = target.plan(zone)
            self.assertEqual(
                22, len([c for c in plan.changes if isinstance(c, Create)]))

            with open(yaml_file) as fh:
                data = safe_load(fh.read())

                # '' has some of both
                roots = sorted(data.pop(''), key=lambda r: r['type'])
                self.assertTrue('values' in roots[0])  # A
                self.assertTrue('geo' in roots[0])  # geo made the trip
                self.assertTrue('value' in roots[1])  # CAA
                self.assertTrue('values' in roots[2])  # SSHFP

                # these are stored as plural 'values'
                self.assertTrue('values' in data.pop('_srv._tcp'))
                self.assertTrue('values' in data.pop('mx'))
                self.assertTrue('values' in data.pop('naptr'))
                self.assertTrue('values' in data.pop('sub'))
                self.assertTrue('values' in data.pop('txt'))
                self.assertTrue('values' in data.pop('loc'))
                self.assertTrue('values' in data.pop('urlfwd'))
                self.assertTrue('values' in data.pop('sub.txt'))
                self.assertTrue('values' in data.pop('subzone'))
                # these are stored as singular 'value'
                self.assertTrue('value' in data.pop('_imap._tcp'))
                self.assertTrue('value' in data.pop('_pop3._tcp'))
                self.assertTrue('value' in data.pop('aaaa'))
                self.assertTrue('value' in data.pop('cname'))
                self.assertTrue('value' in data.pop('dname'))
                self.assertTrue('value' in data.pop('included'))
                self.assertTrue('value' in data.pop('ptr'))
                self.assertTrue('value' in data.pop('spf'))
                self.assertTrue('value' in data.pop('www'))
                self.assertTrue('value' in data.pop('www.sub'))

                # make sure nothing is left
                self.assertEqual([], list(data.keys()))

            with open(dynamic_yaml_file) as fh:
                data = safe_load(fh.read())

                # make sure new dynamic records made the trip
                dyna = data.pop('a')
                self.assertTrue('values' in dyna)
                # self.assertTrue('dynamic' in dyna)
                # TODO:

                # make sure new dynamic records made the trip
                dyna = data.pop('aaaa')
                self.assertTrue('values' in dyna)
                # self.assertTrue('dynamic' in dyna)

                dyna = data.pop('cname')
                self.assertTrue('value' in dyna)
                # self.assertTrue('dynamic' in dyna)

                dyna = data.pop('real-ish-a')
                self.assertTrue('values' in dyna)
                # self.assertTrue('dynamic' in dyna)

                dyna = data.pop('simple-weighted')
                self.assertTrue('value' in dyna)
                # self.assertTrue('dynamic' in dyna)

                dyna = data.pop('pool-only-in-fallback')
                self.assertTrue('value' in dyna)
                # self.assertTrue('dynamic' in dyna)

                # make sure nothing is left
                self.assertEqual([], list(data.keys()))
Exemplo n.º 18
0
class TestUltraProvider(TestCase):
    expected = Zone('unit.tests.', [])
    host = 'https://restapi.ultradns.com'
    empty_body = [{"errorCode": 70002, "errorMessage": "Data not found."}]

    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

    def test_login(self):
        path = '/v2/authorization/token'

        # Bad Auth
        with requests_mock() as mock:
            mock.post('{}{}'.format(self.host, path),
                      status_code=401,
                      text='{"errorCode": 60001}')
            with self.assertRaises(Exception) as ctx:
                UltraProvider('test', 'account', 'user', 'wrongpass')
            self.assertEquals('Unauthorized', text_type(ctx.exception))

        # Good Auth
        with requests_mock() as mock:
            headers = {'Content-Type': 'application/x-www-form-urlencoded'}
            mock.post('{}{}'.format(self.host, path),
                      status_code=200,
                      request_headers=headers,
                      text='{"token type": "Bearer", "refresh_token": "abc", '
                      '"access_token":"123", "expires_in": "3600"}')
            UltraProvider('test', 'account', 'user', 'rightpass')
            self.assertEquals(1, mock.call_count)
            expected_payload = "grant_type=password&username=user&"\
                               "password=rightpass"
            self.assertEquals(mock.last_request.text, expected_payload)

    def test_get_zones(self):
        provider = _get_provider()
        path = "/v2/zones"

        # Test authorization issue
        with requests_mock() as mock:
            mock.get('{}{}'.format(self.host, path),
                     status_code=400,
                     json={
                         "errorCode": 60004,
                         "errorMessage": "Authorization Header required"
                     })
            with self.assertRaises(HTTPError) as ctx:
                zones = provider.zones
            self.assertEquals(400, ctx.exception.response.status_code)

        # Test no zones exist error
        with requests_mock() as mock:
            mock.get('{}{}'.format(self.host, path),
                     status_code=404,
                     headers={'Authorization': 'Bearer 123'},
                     json=self.empty_body)
            zones = provider.zones
            self.assertEquals(1, mock.call_count)
            self.assertEquals(list(), zones)

        # Reset zone cache so they are queried again
        provider._zones = None

        with requests_mock() as mock:
            payload = {
                "resultInfo": {
                    "totalCount": 1,
                    "offset": 0,
                    "returnedCount": 1
                },
                "zones": [{
                    "properties": {
                        "name": "testzone123.com.",
                        "accountName": "testaccount",
                        "type": "PRIMARY",
                        "dnssecStatus": "UNSIGNED",
                        "status": "ACTIVE",
                        "owner": "user",
                        "resourceRecordCount": 5,
                        "lastModifiedDateTime": "2020-06-19T00:47Z"
                    }
                }]
            }

            mock.get('{}{}'.format(self.host, path),
                     status_code=200,
                     headers={'Authorization': 'Bearer 123'},
                     json=payload)
            zones = provider.zones
            self.assertEquals(1, mock.call_count)
            self.assertEquals(1, len(zones))
            self.assertEquals('testzone123.com.', zones[0])

        # Test different paging behavior
        provider._zones = None
        with requests_mock() as mock:
            mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=0'.format(
                self.host, path),
                     status_code=200,
                     json={
                         "resultInfo": {
                             "totalCount": 15,
                             "offset": 0,
                             "returnedCount": 10
                         },
                         "zones": []
                     })
            mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=10'.format(
                self.host, path),
                     status_code=200,
                     json={
                         "resultInfo": {
                             "totalCount": 15,
                             "offset": 10,
                             "returnedCount": 5
                         },
                         "zones": []
                     })
            zones = provider.zones
            self.assertEquals(2, mock.call_count)

    def test_request(self):
        provider = _get_provider()
        path = '/foo'
        payload = {'a': 1}

        with requests_mock() as mock:
            mock.get('{}{}'.format(self.host, path),
                     status_code=401,
                     headers={'Authorization': 'Bearer 123'},
                     json={})
            with self.assertRaises(Exception) as ctx:
                provider._get(path)
            self.assertEquals('Unauthorized', text_type(ctx.exception))

        # Test all GET patterns
        with requests_mock() as mock:
            mock.get('{}{}'.format(self.host, path),
                     status_code=200,
                     headers={'Authorization': 'Bearer 123'},
                     json=payload)
            provider._get(path, json=payload)

            mock.get('{}{}?a=1'.format(self.host, path),
                     status_code=200,
                     headers={'Authorization': 'Bearer 123'})
            provider._get(path, params=payload, json_response=False)

        # Test all POST patterns
        with requests_mock() as mock:
            mock.post('{}{}'.format(self.host, path),
                      status_code=200,
                      headers={'Authorization': 'Bearer 123'},
                      json=payload)
            provider._post(path, json=payload)

            mock.post('{}{}'.format(self.host, path),
                      status_code=200,
                      headers={'Authorization': 'Bearer 123'},
                      text="{'a':1}")
            provider._post(path, data=payload, json_response=False)

        # Test all PUT patterns
        with requests_mock() as mock:
            mock.put('{}{}'.format(self.host, path),
                     status_code=200,
                     headers={'Authorization': 'Bearer 123'},
                     json=payload)
            provider._put(path, json=payload)

        # Test all DELETE patterns
        with requests_mock() as mock:
            mock.delete('{}{}'.format(self.host, path),
                        status_code=200,
                        headers={'Authorization': 'Bearer 123'})
            provider._delete(path, json_response=False)

    def test_zone_records(self):
        provider = _get_provider()
        zone_payload = {
            "resultInfo": {
                "totalCount": 1,
                "offset": 0,
                "returnedCount": 1
            },
            "zones": [{
                "properties": {
                    "name": "octodns1.test."
                }
            }]
        }

        records_payload = {
            "zoneName":
            "octodns1.test.",
            "rrSets": [
                {
                    "ownerName": "octodns1.test.",
                    "rrtype": "NS (2)",
                    "ttl": 86400,
                    "rdata": ["ns1.octodns1.test."]
                },
                {
                    "ownerName":
                    "octodns1.test.",
                    "rrtype":
                    "SOA (6)",
                    "ttl":
                    86400,
                    "rdata":
                    ["pdns1.ultradns.com. phelps.netflix.com. 1 10 10 10 10"]
                },
            ],
            "resultInfo": {
                "totalCount": 2,
                "offset": 0,
                "returnedCount": 2
            }
        }

        zone_path = '/v2/zones'
        rec_path = '/v2/zones/octodns1.test./rrsets'
        with requests_mock() as mock:
            mock.get('{}{}?limit=100&q=zone_type%3APRIMARY&offset=0'.format(
                self.host, zone_path),
                     status_code=200,
                     json=zone_payload)
            mock.get('{}{}?offset=0&limit=100'.format(self.host, rec_path),
                     status_code=200,
                     json=records_payload)

            zone = Zone('octodns1.test.', [])
            self.assertTrue(provider.zone_records(zone))
            self.assertEquals(mock.call_count, 2)

            # Populate the same zone again and confirm cache is hit
            self.assertTrue(provider.zone_records(zone))
            self.assertEquals(mock.call_count, 2)

    def test_populate(self):
        provider = _get_provider()

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

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

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

        # Test zones with data
        provider._zones = None
        path = '/v2/zones'
        with requests_mock() as mock:
            with open('tests/fixtures/ultra-zones-page-1.json') as fh:
                mock.get(
                    '{}{}?limit=100&q=zone_type%3APRIMARY&offset=0'.format(
                        self.host, path),
                    status_code=200,
                    text=fh.read())
            with open('tests/fixtures/ultra-zones-page-2.json') as fh:
                mock.get(
                    '{}{}?limit=100&q=zone_type%3APRIMARY&offset=10'.format(
                        self.host, path),
                    status_code=200,
                    text=fh.read())
            with open('tests/fixtures/ultra-records-page-1.json') as fh:
                rec_path = '/v2/zones/octodns1.test./rrsets'
                mock.get('{}{}?offset=0&limit=100'.format(self.host, rec_path),
                         status_code=200,
                         text=fh.read())
            with open('tests/fixtures/ultra-records-page-2.json') as fh:
                rec_path = '/v2/zones/octodns1.test./rrsets'
                mock.get('{}{}?offset=10&limit=100'.format(
                    self.host, rec_path),
                         status_code=200,
                         text=fh.read())

            zone = Zone('octodns1.test.', [])

            self.assertTrue(provider.populate(zone))
            self.assertEquals('octodns1.test.', zone.name)
            self.assertEquals(12, len(zone.records))
            self.assertEquals(4, mock.call_count)

    def test_apply(self):
        provider = _get_provider()

        provider._request = Mock()

        provider._request.side_effect = [
            UltraNoZonesExistException('No Zones'),
            None,  # zone create
        ] + [None] * 15  # individual record creates

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

        provider._request.assert_has_calls(
            [
                # created the domain
                call('POST',
                     '/v2/zones',
                     json={
                         'properties': {
                             'name': 'unit.tests.',
                             'accountName': 'testacct',
                             'type': 'PRIMARY'
                         },
                         'primaryCreateInfo': {
                             'createType': 'NEW'
                         }
                     }),
                # Validate multi-ip apex A record is correct
                call('POST',
                     '/v2/zones/unit.tests./rrsets/A/unit.tests.',
                     json={
                         'ttl': 300,
                         'rdata': ['1.2.3.4', '1.2.3.5'],
                         'profile': {
                             '@context':
                             'http://schemas.ultradns.com/RDPool.jsonschema',
                             'order': 'FIXED',
                             'description': 'unit.tests.'
                         }
                     }),
                # make sure semicolons are not escaped when sending data
                call('POST',
                     '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.',
                     json={
                         'ttl':
                         600,
                         'rdata': [
                             'Bah bah black sheep', 'have you any wool.',
                             'v=DKIM1;k=rsa;s=email;h=sha256;'
                             'p=A/kinda+of/long/string+with+numb3rs'
                         ]
                     }),
            ],
            True)
        # expected number of total calls
        self.assertEquals(17, provider._request.call_count)

        # Create sample rrset payload to attempt to alter
        page1 = json_load(open('tests/fixtures/ultra-records-page-1.json'))
        page2 = json_load(open('tests/fixtures/ultra-records-page-2.json'))
        mock_rrsets = list()
        mock_rrsets.extend(page1['rrSets'])
        mock_rrsets.extend(page2['rrSets'])

        # Seed a bunch of records into a zone and verify update / delete ops
        provider._request.reset_mock()
        provider._zones = ['octodns1.test.']
        provider.zone_records = Mock(return_value=mock_rrsets)

        provider._request.side_effect = [None] * 13

        wanted = Zone('octodns1.test.', [])
        wanted.add_record(
            Record.new(
                wanted,
                '',
                {
                    'ttl': 60,  # Change TTL
                    'type': 'A',
                    'value': '5.6.7.8'  # Change number of IPs (3 -> 1)
                }))
        wanted.add_record(
            Record.new(
                wanted,
                'txt',
                {
                    'ttl':
                    3600,
                    'type':
                    'TXT',
                    'values': [  # Alter TXT value
                        "foobar", "v=spf1 include:mail.server.net ?all"
                    ]
                }))

        plan = provider.plan(wanted)
        self.assertEquals(11, len(plan.changes))
        self.assertEquals(11, provider.apply(plan))
        self.assertTrue(plan.exists)

        provider._request.assert_has_calls(
            [
                # Validate multi-ip apex A record replaced with standard A
                call('PUT',
                     '/v2/zones/octodns1.test./rrsets/A/octodns1.test.',
                     json={
                         'ttl': 60,
                         'rdata': ['5.6.7.8']
                     }),
                # Make sure TXT value is properly updated
                call('PUT',
                     '/v2/zones/octodns1.test./rrsets/TXT/txt.octodns1.test.',
                     json={
                         'ttl': 3600,
                         'rdata':
                         ["foobar", "v=spf1 include:mail.server.net ?all"]
                     }),
                # Confirm a few of the DELETE operations properly occur
                call('DELETE',
                     '/v2/zones/octodns1.test./rrsets/A/a.octodns1.test.',
                     json_response=False),
                call(
                    'DELETE',
                    '/v2/zones/octodns1.test./rrsets/AAAA/aaaa.octodns1.test.',
                    json_response=False),
                call('DELETE',
                     '/v2/zones/octodns1.test./rrsets/CAA/caa.octodns1.test.',
                     json_response=False),
                call(
                    'DELETE',
                    '/v2/zones/octodns1.test./rrsets/CNAME/cname.octodns1.test.',
                    json_response=False),
            ],
            True)

    def test_gen_data(self):
        provider = _get_provider()
        zone = Zone('unit.tests.', [])

        for name, _type, expected_path, expected_payload, expected_record in (
                # A
            ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', {
                'ttl': 60,
                'rdata': ['1.2.3.4']
            },
             Record.new(zone, 'a', {
                 'ttl': 60,
                 'type': 'A',
                 'values': ['1.2.3.4']
             })),
            ('a', 'A', '/v2/zones/unit.tests./rrsets/A/a.unit.tests.', {
                'ttl': 60,
                'rdata': ['1.2.3.4', '5.6.7.8'],
                'profile': {
                    '@context':
                    'http://schemas.ultradns.com/RDPool.jsonschema',
                    'order': 'FIXED',
                    'description': 'a.unit.tests.'
                }
            },
             Record.new(zone, 'a', {
                 'ttl': 60,
                 'type': 'A',
                 'values': ['1.2.3.4', '5.6.7.8']
             })),

                # AAAA
            ('aaaa', 'AAAA',
             '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', {
                 'ttl': 60,
                 'rdata': ['::1']
             },
             Record.new(zone, 'aaaa', {
                 'ttl': 60,
                 'type': 'AAAA',
                 'values': ['::1']
             })),
            ('aaaa', 'AAAA',
             '/v2/zones/unit.tests./rrsets/AAAA/aaaa.unit.tests.', {
                 'ttl': 60,
                 'rdata': ['::1', '::2'],
                 'profile': {
                     '@context':
                     'http://schemas.ultradns.com/RDPool.jsonschema',
                     'order': 'FIXED',
                     'description': 'aaaa.unit.tests.'
                 }
             },
             Record.new(zone, 'aaaa', {
                 'ttl': 60,
                 'type': 'AAAA',
                 'values': ['::1', '::2']
             })),

                # CAA
            ('caa', 'CAA', '/v2/zones/unit.tests./rrsets/CAA/caa.unit.tests.',
             {
                 'ttl': 60,
                 'rdata': ['0 issue foo.com']
             },
             Record.new(
                 zone, 'caa', {
                     'ttl': 60,
                     'type': 'CAA',
                     'values': [{
                         'flags': 0,
                         'tag': 'issue',
                         'value': 'foo.com'
                     }]
                 })),

                # CNAME
            ('cname', 'CNAME',
             '/v2/zones/unit.tests./rrsets/CNAME/cname.unit.tests.', {
                 'ttl': 60,
                 'rdata': ['netflix.com.']
             },
             Record.new(zone, 'cname', {
                 'ttl': 60,
                 'type': 'CNAME',
                 'value': 'netflix.com.'
             })),

                # MX
            ('mx', 'MX', '/v2/zones/unit.tests./rrsets/MX/mx.unit.tests.', {
                'ttl': 60,
                'rdata': ['1 mx1.unit.tests.', '1 mx2.unit.tests.']
            },
             Record.new(
                 zone, 'mx', {
                     'ttl':
                     60,
                     'type':
                     'MX',
                     'values': [{
                         'preference': 1,
                         'exchange': 'mx1.unit.tests.'
                     }, {
                         'preference': 1,
                         'exchange': 'mx2.unit.tests.'
                     }]
                 })),

                # NS
            ('ns', 'NS', '/v2/zones/unit.tests./rrsets/NS/ns.unit.tests.', {
                'ttl': 60,
                'rdata': ['ns1.unit.tests.', 'ns2.unit.tests.']
            },
             Record.new(
                 zone, 'ns', {
                     'ttl': 60,
                     'type': 'NS',
                     'values': ['ns1.unit.tests.', 'ns2.unit.tests.']
                 })),

                # PTR
            ('ptr', 'PTR', '/v2/zones/unit.tests./rrsets/PTR/ptr.unit.tests.',
             {
                 'ttl': 60,
                 'rdata': ['a.unit.tests.']
             },
             Record.new(zone, 'ptr', {
                 'ttl': 60,
                 'type': 'PTR',
                 'value': 'a.unit.tests.'
             })),

                # SPF
            ('spf', 'SPF', '/v2/zones/unit.tests./rrsets/SPF/spf.unit.tests.',
             {
                 'ttl': 60,
                 'rdata': ['v=spf1 -all']
             },
             Record.new(zone, 'spf', {
                 'ttl': 60,
                 'type': 'SPF',
                 'values': ['v=spf1 -all']
             })),

                # SRV
            ('_srv._tcp', 'SRV',
             '/v2/zones/unit.tests./rrsets/SRV/_srv._tcp.unit.tests.', {
                 'ttl': 60,
                 'rdata': ['10 20 443 target.unit.tests.']
             },
             Record.new(
                 zone, '_srv._tcp', {
                     'ttl':
                     60,
                     'type':
                     'SRV',
                     'values': [{
                         'priority': 10,
                         'weight': 20,
                         'port': 443,
                         'target': 'target.unit.tests.'
                     }]
                 })),

                # TXT
            ('txt', 'TXT', '/v2/zones/unit.tests./rrsets/TXT/txt.unit.tests.',
             {
                 'ttl': 60,
                 'rdata': ['abc', 'def']
             },
             Record.new(zone, 'txt', {
                 'ttl': 60,
                 'type': 'TXT',
                 'values': ['abc', 'def']
             })),

                # ALIAS
            ('', 'ALIAS', '/v2/zones/unit.tests./rrsets/APEXALIAS/unit.tests.',
             {
                 'ttl': 60,
                 'rdata': ['target.unit.tests.']
             },
             Record.new(zone, '', {
                 'ttl': 60,
                 'type': 'ALIAS',
                 'value': 'target.unit.tests.'
             })),
        ):
            # Validate path and payload based on record meet expectations
            path, payload = provider._gen_data(expected_record)
            self.assertEqual(expected_path, path)
            self.assertEqual(expected_payload, payload)

            # Use generator for record and confirm the output matches
            rec = provider._record_for(zone, name, _type, expected_payload,
                                       False)
            path, payload = provider._gen_data(rec)
            self.assertEqual(expected_path, path)
            self.assertEqual(expected_payload, payload)
Exemplo n.º 19
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', retry_period=0)

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

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

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

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

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

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

            with self.assertRaises(Exception) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('CloudflareAuthenticationError',
                              type(ctx.exception).__name__)
            self.assertEquals('Cloudflare error', 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)

        # Rate Limit error
        with requests_mock() as mock:
            mock.get(ANY, status_code=429,
                     text='{"success":false,"errors":[{"code":10100,'
                     '"message":"More than 1200 requests per 300 seconds '
                     'reached. Please wait and consider throttling your '
                     'request speed"}],"messages":[],"result":null}')

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

            self.assertEquals('CloudflareRateLimitError',
                              type(ctx.exception).__name__)
            self.assertEquals('More than 1200 requests per 300 seconds '
                              'reached. Please wait and consider throttling '
                              'your request speed', text_type(ctx.exception))

        # Rate Limit error, unknown resp
        with requests_mock() as mock:
            mock.get(ANY, status_code=429, text='{}')

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

            self.assertEquals('CloudflareRateLimitError',
                              type(ctx.exception).__name__)
            self.assertEquals('Cloudflare error', text_type(ctx.exception))

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

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

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

        # bust zone cache
        provider._zones = None

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

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

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

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(13, 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(13, len(again.records))

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

        provider._request = Mock()

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

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

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

        # Test out the create rate-limit handling, then 9 successes
        provider._request.side_effect = [
            CloudflareRateLimitError('{}'),
        ] + ([None] * 3)

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

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

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

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

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

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

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

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

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

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

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

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

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

        zone = Zone('unit.tests.', [])
        # PTR record
        ptr_record = Record.new(zone, 'ptr', {
            'ttl': 300,
            'type': 'PTR',
            'value': 'foo.bar.com.'
        })

        ptr_record_contents = provider._gen_data(ptr_record)
        self.assertEquals({
            'name': 'ptr.unit.tests',
            'ttl': 300,
            'type': 'PTR',
            'content': 'foo.bar.com.'
        }, list(ptr_record_contents)[0])

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

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

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

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

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

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

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

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

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

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

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

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

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

        ordered = sorted(zone.records, key=lambda r: r.name)

        record = ordered[0]
        self.assertEquals('a', record.name)
        self.assertEquals('a.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value)

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

        record = ordered[2]
        self.assertEquals('multi', record.name)
        self.assertEquals('multi.unit.tests.', record.fqdn)
        self.assertEquals('CNAME', record._type)
        self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        include_change = provider._include_change(change)

        self.assertFalse(include_change)

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

        data = next(provider._gen_data(record))

        self.assertFalse('proxied' in data)

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

        data = next(provider._gen_data(record))

        self.assertFalse(data['proxied'])

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

        data = next(provider._gen_data(record))

        self.assertTrue(data['proxied'])

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

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

        self.assertFalse(extra_changes)

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

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

        self.assertFalse(extra_changes)

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

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

        self.assertFalse(extra_changes)

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

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

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

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

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

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

    def test_emailless_auth(self):
        provider = CloudflareProvider('test', token='token 123',
                                      email='email 234')
        headers = provider._sess.headers
        self.assertEquals('email 234', headers['X-Auth-Email'])
        self.assertEquals('token 123', headers['X-Auth-Key'])

        provider = CloudflareProvider('test', token='token 123')
        headers = provider._sess.headers
        self.assertEquals('Bearer token 123', headers['Authorization'])

    def test_retry_behavior(self):
        provider = CloudflareProvider('test', token='token 123',
                                      email='email 234', retry_period=0)
        result = {
            "success": True,
            "errors": [],
            "messages": [],
            "result": [],
            "result_info": {
                "count": 1,
                "per_page": 50
            }
        }
        zone = Zone('unit.tests.', [])
        provider._request = Mock()

        # No retry required, just calls and is returned
        provider._zones = None
        provider._request.reset_mock()
        provider._request.side_effect = [result]
        self.assertEquals([], provider.zone_records(zone))
        provider._request.assert_has_calls([call('GET', '/zones',
                                           params={'page': 1})])

        # One retry required
        provider._zones = None
        provider._request.reset_mock()
        provider._request.side_effect = [
            CloudflareRateLimitError('{}'),
            result
        ]
        self.assertEquals([], provider.zone_records(zone))
        provider._request.assert_has_calls([call('GET', '/zones',
                                           params={'page': 1})])

        # Two retries required
        provider._zones = None
        provider._request.reset_mock()
        provider._request.side_effect = [
            CloudflareRateLimitError('{}'),
            CloudflareRateLimitError('{}'),
            result
        ]
        self.assertEquals([], provider.zone_records(zone))
        provider._request.assert_has_calls([call('GET', '/zones',
                                           params={'page': 1})])

        # # Exhaust our retries
        provider._zones = None
        provider._request.reset_mock()
        provider._request.side_effect = [
            CloudflareRateLimitError({"errors": [{"message": "first"}]}),
            CloudflareRateLimitError({"errors": [{"message": "boo"}]}),
            CloudflareRateLimitError({"errors": [{"message": "boo"}]}),
            CloudflareRateLimitError({"errors": [{"message": "boo"}]}),
            CloudflareRateLimitError({"errors": [{"message": "last"}]}),
        ]
        with self.assertRaises(CloudflareRateLimitError) as ctx:
            provider.zone_records(zone)
            self.assertEquals('last', text_type(ctx.exception))
class TestGandiProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

    # We remove this record from the test zone as Gandi API reject it
    # (rightfully).
    expected._remove_record(
        Record.new(expected, 'sub', {
            'ttl': 1800,
            'type': 'NS',
            'values': ['6.2.3.4.', '7.2.3.4.']
        }))

    def test_populate(self):

        provider = GandiProvider('test_id', 'token')

        # 400 - Bad Request.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=400,
                     text='{"status": "error", "errors": [{"location": '
                     '"body", "name": "items", "description": '
                     '"\'6.2.3.4.\': invalid hostname (param: '
                     '{\'rrset_type\': u\'NS\', \'rrset_ttl\': 3600, '
                     '\'rrset_name\': u\'sub\', \'rrset_values\': '
                     '[u\'6.2.3.4.\', u\'7.2.3.4.\']})"}, {"location": '
                     '"body", "name": "items", "description": '
                     '"\'7.2.3.4.\': invalid hostname (param: '
                     '{\'rrset_type\': u\'NS\', \'rrset_ttl\': 3600, '
                     '\'rrset_name\': u\'sub\', \'rrset_values\': '
                     '[u\'6.2.3.4.\', u\'7.2.3.4.\']})"}]}')

            with self.assertRaises(GandiClientBadRequest) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertIn('"status": "error"', str(ctx.exception))

        # 401 - Unauthorized.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=401,
                     text='{"code":401,"message":"The server could not verify '
                     'that you authorized to access the document you '
                     'requested. Either you supplied the wrong '
                     'credentials (e.g., bad api key), or your access '
                     'token has expired","object":"HTTPUnauthorized",'
                     '"cause":"Unauthorized"}')

            with self.assertRaises(GandiClientUnauthorized) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertIn('"cause":"Unauthorized"', str(ctx.exception))

        # 403 - Forbidden.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=403,
                     text='{"code":403,"message":"Access was denied to this '
                     'resource.","object":"HTTPForbidden","cause":'
                     '"Forbidden"}')

            with self.assertRaises(GandiClientForbidden) as ctx:
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertIn('"cause":"Forbidden"', str(ctx.exception))

        # 404 - Not Found.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=404,
                     text='{"code": 404, "message": "The resource could not '
                     'be found.", "object": "HTTPNotFound", "cause": '
                     '"Not Found"}')

            with self.assertRaises(GandiClientNotFound) as ctx:
                zone = Zone('unit.tests.', [])
                provider._client.zone(zone)
            self.assertIn('"cause": "Not Found"', str(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)

        # No diffs == no changes
        with requests_mock() as mock:
            base = 'https://api.gandi.net/v5/livedns/domains/unit.tests' \
                '/records'
            with open('tests/fixtures/gandi-no-changes.json') as fh:
                mock.get(base, 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))

        del provider._zone_records[zone.name]

        # Default Gandi zone file.
        with requests_mock() as mock:
            base = 'https://api.gandi.net/v5/livedns/domains/unit.tests' \
                '/records'
            with open('tests/fixtures/gandi-records.json') as fh:
                mock.get(base, text=fh.read())

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

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

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

    def test_apply(self):
        provider = GandiProvider('test_id', 'token')

        # Zone does not exists but can be created.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=404,
                     text='{"code": 404, "message": "The resource could not '
                     'be found.", "object": "HTTPNotFound", "cause": '
                     '"Not Found"}')
            mock.post(ANY,
                      status_code=201,
                      text='{"message": "Domain Created"}')

            plan = provider.plan(self.expected)
            provider.apply(plan)

        # Zone does not exists and can't be created.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=404,
                     text='{"code": 404, "message": "The resource could not '
                     'be found.", "object": "HTTPNotFound", "cause": '
                     '"Not Found"}')
            mock.post(ANY,
                      status_code=404,
                      text='{"code": 404, "message": "The resource could not '
                      'be found.", "object": "HTTPNotFound", "cause": '
                      '"Not Found"}')

            with self.assertRaises(
                (GandiClientNotFound, GandiClientUnknownDomainName)) as ctx:
                plan = provider.plan(self.expected)
                provider.apply(plan)
            self.assertIn('This domain is not registered at Gandi.',
                          str(ctx.exception))

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

        with open('tests/fixtures/gandi-zone.json') as fh:
            zone = fh.read()

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

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

        provider._client._request.assert_has_calls([
            call('GET', '/livedns/domains/unit.tests/records'),
            call('GET', '/livedns/domains/unit.tests'),
            call('POST',
                 '/livedns/domains',
                 data={
                     'fqdn': 'unit.tests',
                     'zone': {}
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'www.sub',
                     'rrset_ttl': 300,
                     'rrset_type': 'A',
                     'rrset_values': ['2.2.3.6']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'www',
                     'rrset_ttl': 300,
                     'rrset_type': 'A',
                     'rrset_values': ['2.2.3.6']
                 }),
            call(
                'POST',
                '/livedns/domains/unit.tests/records',
                data={
                    'rrset_name':
                    'txt',
                    'rrset_ttl':
                    600,
                    'rrset_type':
                    'TXT',
                    'rrset_values': [
                        'Bah bah black sheep', 'have you any wool.',
                        'v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string'
                        '+with+numb3rs'
                    ]
                }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'spf',
                     'rrset_ttl': 600,
                     'rrset_type': 'SPF',
                     'rrset_values': ['v=spf1 ip4:192.168.0.1/16-all']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'ptr',
                     'rrset_ttl': 300,
                     'rrset_type': 'PTR',
                     'rrset_values': ['foo.bar.com.']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name':
                     'mx',
                     'rrset_ttl':
                     300,
                     'rrset_type':
                     'MX',
                     'rrset_values': [
                         '10 smtp-4.unit.tests.', '20 smtp-2.unit.tests.',
                         '30 smtp-3.unit.tests.', '40 smtp-1.unit.tests.'
                     ]
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'excluded',
                     'rrset_ttl': 3600,
                     'rrset_type': 'CNAME',
                     'rrset_values': ['unit.tests.']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'dname',
                     'rrset_ttl': 300,
                     'rrset_type': 'DNAME',
                     'rrset_values': ['unit.tests.']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'cname',
                     'rrset_ttl': 300,
                     'rrset_type': 'CNAME',
                     'rrset_values': ['unit.tests.']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'aaaa',
                     'rrset_ttl': 600,
                     'rrset_type': 'AAAA',
                     'rrset_values': ['2601:644:500:e210:62f8:1dff:feb8:947a']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name':
                     '_srv._tcp',
                     'rrset_ttl':
                     600,
                     'rrset_type':
                     'SRV',
                     'rrset_values': [
                         '10 20 30 foo-1.unit.tests.',
                         '12 20 30 foo-2.unit.tests.'
                     ]
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': '_pop3._tcp',
                     'rrset_ttl': 600,
                     'rrset_type': 'SRV',
                     'rrset_values': [
                         '0 0 0 .',
                     ]
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': '_imap._tcp',
                     'rrset_ttl': 600,
                     'rrset_type': 'SRV',
                     'rrset_values': [
                         '0 0 0 .',
                     ]
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name':
                     '@',
                     'rrset_ttl':
                     3600,
                     'rrset_type':
                     'SSHFP',
                     'rrset_values': [
                         '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49',
                         '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73'
                     ]
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': '@',
                     'rrset_ttl': 3600,
                     'rrset_type': 'CAA',
                     'rrset_values': ['0 issue "ca.unit.tests"']
                 }),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': '@',
                     'rrset_ttl': 300,
                     'rrset_type': 'A',
                     'rrset_values': ['1.2.3.4', '1.2.3.5']
                 })
        ])
        # expected number of total calls
        self.assertEquals(19, provider._client._request.call_count)

        provider._client._request.reset_mock()

        # delete 1 and update 1
        provider._client.zone_records = Mock(
            return_value=[{
                'rrset_name': 'www',
                'rrset_ttl': 300,
                'rrset_type': 'A',
                'rrset_values': ['1.2.3.4']
            }, {
                'rrset_name': 'www',
                'rrset_ttl': 300,
                'rrset_type': 'A',
                'rrset_values': ['2.2.3.4']
            }, {
                'rrset_name': 'ttl',
                'rrset_ttl': 600,
                'rrset_type': 'A',
                'rrset_values': ['3.2.3.4']
            }])

        # 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('DELETE', '/livedns/domains/unit.tests/records/www/A'),
            call('DELETE', '/livedns/domains/unit.tests/records/ttl/A'),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'ttl',
                     'rrset_ttl': 300,
                     'rrset_type': 'A',
                     'rrset_values': ['3.2.3.4']
                 })
        ],
                                                   any_order=True)
Exemplo n.º 21
0
class TestHetznerProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

            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='{"zone":{"id":"","name":"","ttl":0,"registrar":"",'
                     '"legacy_dns_host":"","legacy_ns":null,"ns":null,'
                     '"created":"","verified":"","modified":"","project":"",'
                     '"owner":"","permission":"","zone_type":{"id":"",'
                     '"name":"","description":"","prices":null},"status":"",'
                     '"paused":false,"is_secondary_dns":false,'
                     '"txt_verification":{"name":"","token":""},'
                     '"records_count":0},"error":{'
                     '"message":"zone not found","code":404}}')

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

        # No diffs == no changes
        with requests_mock() as mock:
            base = provider._client.BASE_URL
            with open('tests/fixtures/hetzner-zones.json') as fh:
                mock.get('{}/zones'.format(base), text=fh.read())
            with open('tests/fixtures/hetzner-records.json') as fh:
                mock.get('{}/records'.format(base), text=fh.read())

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(13, 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(13, len(again.records))

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

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

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

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

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

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

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

        provider._client._do.reset_mock()

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

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

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

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._do.assert_has_calls([
            call('POST', '/records', data={
                'name': 'ttl',
                'ttl': 300,
                'type': 'A',
                'value': '3.2.3.4',
                'zone_id': 'unit.tests',
            }),
            call('DELETE', '/records/one'),
            call('DELETE', '/records/two'),
            call('DELETE', '/records/three'),
        ], any_order=True)
Exemplo n.º 22
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(f'{base}/', text=fh.read())
            with open('tests/fixtures/dnsmadeeasy-records.json') as fh:
                mock.get(f'{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)
Exemplo n.º 23
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):

        # Sandbox
        provider = DnsimpleProvider('test', 'token', 42, 'true')
        self.assertTrue('sandbox' in provider._client.base)

        provider = DnsimpleProvider('test', 'token', 42)
        self.assertFalse('sandbox' in provider._client.base)

        # 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(0, len(changes))

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

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

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

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

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

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

        # non-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) - 7
        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)
class TestGCoreProvider(TestCase):
    expected = Zone("unit.tests.", [])
    source = YamlProvider("test", join(dirname(__file__), "config"))
    source.populate(expected)

    default_filters = [
        {
            "type": "geodns"
        },
        {
            "type": "default",
            "limit": 1,
            "strict": False,
        },
        {
            "type": "first_n",
            "limit": 1
        },
    ]

    def test_populate(self):

        provider = GCoreProvider("test_id", token="token")

        # TC: 400 - Bad Request.
        with requests_mock() as mock:
            mock.get(ANY, status_code=400, text='{"error":"bad body"}')

            with self.assertRaises(GCoreClientBadRequest) as ctx:
                zone = Zone("unit.tests.", [])
                provider.populate(zone)
            self.assertIn('"error":"bad body"', str(ctx.exception))

        # TC: 404 - Not Found.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=404,
                     text='{"error":"zone is not found"}')

            with self.assertRaises(GCoreClientNotFound) as ctx:
                zone = Zone("unit.tests.", [])
                provider._client.zone(zone.name)
            self.assertIn('"error":"zone is not found"', str(ctx.exception))

        # TC: General error
        with requests_mock() as mock:
            mock.get(ANY, status_code=500, text="Things caught fire")

            with self.assertRaises(GCoreClientException) as ctx:
                zone = Zone("unit.tests.", [])
                provider.populate(zone)
            self.assertEqual("Things caught fire", str(ctx.exception))

        # TC: No credentials or token error
        with requests_mock() as mock:
            with self.assertRaises(ValueError) as ctx:
                GCoreProvider("test_id")
            self.assertEqual(
                "either token or login & password must be set",
                str(ctx.exception),
            )

        # TC: Auth with login password
        with requests_mock() as mock:

            def match_body(request):
                return {"username": "******", "password": "******"} == request.json()

            auth_url = "http://api/auth/jwt/login"
            mock.post(
                auth_url,
                additional_matcher=match_body,
                status_code=200,
                json={"access": "access"},
            )

            providerPassword = GCoreProvider(
                "test_id",
                url="http://dns",
                auth_url="http://api",
                login="******",
                password="******",
            )
            assert mock.called

            # make sure token passed in header
            zone_rrset_url = "http://dns/zones/unit.tests/rrsets?all=true"
            mock.get(
                zone_rrset_url,
                request_headers={"Authorization": "Bearer access"},
                status_code=404,
            )
            zone = Zone("unit.tests.", [])
            assert not providerPassword.populate(zone)

        # TC: No diffs == no changes
        with requests_mock() as mock:
            base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
            with open("tests/fixtures/gcore-no-changes.json") as fh:
                mock.get(base, text=fh.read())

            zone = Zone("unit.tests.", [])
            provider.populate(zone)
            self.assertEqual(14, len(zone.records))
            self.assertEqual(
                {
                    "",
                    "_imap._tcp",
                    "_pop3._tcp",
                    "_srv._tcp",
                    "aaaa",
                    "cname",
                    "excluded",
                    "mx",
                    "ptr",
                    "sub",
                    "txt",
                    "www",
                    "www.sub",
                },
                {r.name
                 for r in zone.records},
            )
            changes = self.expected.changes(zone, provider)
            self.assertEqual(0, len(changes))

        # TC: 4 create (dynamic) + 1 removed + 7 modified
        with requests_mock() as mock:
            base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
            with open("tests/fixtures/gcore-records.json") as fh:
                mock.get(base, text=fh.read())

            zone = Zone("unit.tests.", [])
            provider.populate(zone)
            self.assertEqual(16, len(zone.records))
            changes = self.expected.changes(zone, provider)
            self.assertEqual(11, len(changes))
            self.assertEqual(
                3, len([c for c in changes if isinstance(c, Create)]))
            self.assertEqual(
                1, len([c for c in changes if isinstance(c, Delete)]))
            self.assertEqual(
                7, len([c for c in changes if isinstance(c, Update)]))

        # TC: no pools can be built
        with requests_mock() as mock:
            base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
            mock.get(
                base,
                json={
                    "rrsets": [{
                        "name": "unit.tests.",
                        "type": "A",
                        "ttl": 300,
                        "filters": self.default_filters,
                        "resource_records": [{
                            "content": ["7.7.7.7"]
                        }],
                    }]
                },
            )

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

            self.assertTrue(
                str(ctx.exception).startswith(
                    "filter is enabled, but no pools where built for"),
                f"{ctx.exception} - is not start from desired text",
            )

    def test_apply(self):
        provider = GCoreProvider("test_id", url="http://api", token="token")

        # TC: Zone does not exists but can be created.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=404,
                     text='{"error":"zone is not found"}')
            mock.post(ANY, status_code=200, text='{"id":1234}')

            plan = provider.plan(self.expected)
            provider.apply(plan)

        # TC: Zone does not exists and can't be created.
        with requests_mock() as mock:
            mock.get(ANY,
                     status_code=404,
                     text='{"error":"zone is not found"}')
            mock.post(
                ANY,
                status_code=400,
                text='{"error":"parent zone is already'
                ' occupied by another client"}',
            )

            with self.assertRaises(
                (GCoreClientNotFound, GCoreClientBadRequest)) as ctx:
                plan = provider.plan(self.expected)
                provider.apply(plan)
            self.assertIn(
                "parent zone is already occupied by another client",
                str(ctx.exception),
            )

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

        with open("tests/fixtures/gcore-zone.json") as fh:
            zone = fh.read()

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

        # TC: create all
        self.assertEqual(13, len(plan.changes))
        self.assertEqual(13, provider.apply(plan))
        self.assertFalse(plan.exists)

        provider._client._request.assert_has_calls([
            call(
                "GET",
                "http://api/zones/unit.tests/rrsets",
                params={"all": "true"},
            ),
            call("GET", "http://api/zones/unit.tests"),
            call("POST", "http://api/zones", data={"name": "unit.tests"}),
            call(
                "POST",
                "http://api/zones/unit.tests/www.sub.unit.tests./A",
                data={
                    "ttl": 300,
                    "resource_records": [{
                        "content": ["2.2.3.6"]
                    }],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/www.unit.tests./A",
                data={
                    "ttl": 300,
                    "resource_records": [{
                        "content": ["2.2.3.6"]
                    }],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/txt.unit.tests./TXT",
                data={
                    "ttl":
                    600,
                    "resource_records": [
                        {
                            "content": ["Bah bah black sheep"]
                        },
                        {
                            "content": ["have you any wool."]
                        },
                        {
                            "content": [
                                "v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+"
                                "of/long/string+with+numb3rs"
                            ]
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/sub.unit.tests./NS",
                data={
                    "ttl":
                    3600,
                    "resource_records": [
                        {
                            "content": ["6.2.3.4."]
                        },
                        {
                            "content": ["7.2.3.4."]
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/ptr.unit.tests./PTR",
                data={
                    "ttl": 300,
                    "resource_records": [
                        {
                            "content": ["foo.bar.com."]
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/mx.unit.tests./MX",
                data={
                    "ttl":
                    300,
                    "resource_records": [
                        {
                            "content": [10, "smtp-4.unit.tests."]
                        },
                        {
                            "content": [20, "smtp-2.unit.tests."]
                        },
                        {
                            "content": [30, "smtp-3.unit.tests."]
                        },
                        {
                            "content": [40, "smtp-1.unit.tests."]
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/excluded.unit.tests./CNAME",
                data={
                    "ttl": 3600,
                    "resource_records": [{
                        "content": ["unit.tests."]
                    }],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/cname.unit.tests./CNAME",
                data={
                    "ttl": 300,
                    "resource_records": [{
                        "content": ["unit.tests."]
                    }],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/aaaa.unit.tests./AAAA",
                data={
                    "ttl":
                    600,
                    "resource_records": [{
                        "content": ["2601:644:500:e210:62f8:1dff:feb8:947a"]
                    }],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/_srv._tcp.unit.tests./SRV",
                data={
                    "ttl":
                    600,
                    "resource_records": [
                        {
                            "content": [10, 20, 30, "foo-1.unit.tests."]
                        },
                        {
                            "content": [12, 20, 30, "foo-2.unit.tests."]
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/_pop3._tcp.unit.tests./SRV",
                data={
                    "ttl": 600,
                    "resource_records": [{
                        "content": [0, 0, 0, "."]
                    }],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/_imap._tcp.unit.tests./SRV",
                data={
                    "ttl": 600,
                    "resource_records": [{
                        "content": [0, 0, 0, "."]
                    }],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/unit.tests./A",
                data={
                    "ttl":
                    300,
                    "resource_records": [
                        {
                            "content": ["1.2.3.4"]
                        },
                        {
                            "content": ["1.2.3.5"]
                        },
                    ],
                },
            ),
        ])
        # expected number of total calls
        self.assertEqual(16, provider._client._request.call_count)

        # TC: delete 1 and update 1
        provider._client._request.reset_mock()
        provider._client.zone_records = Mock(return_value=[
            {
                "name": "www",
                "ttl": 300,
                "type": "A",
                "resource_records": [{
                    "content": ["1.2.3.4"]
                }],
            },
            {
                "name": "ttl",
                "ttl": 600,
                "type": "A",
                "resource_records": [{
                    "content": ["3.2.3.4"]
                }],
            },
        ])

        # 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.assertEqual(2, len(plan.changes))
        self.assertEqual(2, provider.apply(plan))

        provider._client._request.assert_has_calls([
            call("DELETE", "http://api/zones/unit.tests/www.unit.tests./A"),
            call(
                "PUT",
                "http://api/zones/unit.tests/ttl.unit.tests./A",
                data={
                    "ttl": 300,
                    "resource_records": [{
                        "content": ["3.2.3.4"]
                    }],
                },
            ),
        ])

        # TC: create dynamics
        provider._client._request.reset_mock()
        provider._client.zone_records = Mock(return_value=[])

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

        wanted = Zone("unit.tests.", [])
        wanted.add_record(
            Record.new(
                wanted,
                "geo-simple",
                {
                    "ttl": 300,
                    "type": "A",
                    "value": "3.3.3.3",
                    "dynamic": {
                        "pools": {
                            "pool-1": {
                                "fallback":
                                "other",
                                "values": [
                                    {
                                        "value": "1.1.1.1"
                                    },
                                    {
                                        "value": "1.1.1.2"
                                    },
                                ],
                            },
                            "pool-2": {
                                "fallback": "other",
                                "values": [
                                    {
                                        "value": "2.2.2.1"
                                    },
                                ],
                            },
                            "other": {
                                "values": [{
                                    "value": "3.3.3.3"
                                }]
                            },
                        },
                        "rules": [
                            {
                                "pool": "pool-1",
                                "geos": ["EU-RU"]
                            },
                            {
                                "pool": "pool-2",
                                "geos": ["EU"]
                            },
                            {
                                "pool": "other"
                            },
                        ],
                    },
                },
            ), )
        wanted.add_record(
            Record.new(
                wanted,
                "geo-defaults",
                {
                    "ttl": 300,
                    "type": "A",
                    "value": "3.2.3.4",
                    "dynamic": {
                        "pools": {
                            "pool-1": {
                                "values": [
                                    {
                                        "value": "2.2.2.1"
                                    },
                                ],
                            },
                        },
                        "rules": [
                            {
                                "pool": "pool-1",
                                "geos": ["EU"]
                            },
                        ],
                    },
                },
            ), )
        wanted.add_record(
            Record.new(
                wanted,
                "cname-smpl",
                {
                    "ttl": 300,
                    "type": "CNAME",
                    "value": "en.unit.tests.",
                    "dynamic": {
                        "pools": {
                            "pool-1": {
                                "fallback":
                                "other",
                                "values": [
                                    {
                                        "value": "ru-1.unit.tests."
                                    },
                                    {
                                        "value": "ru-2.unit.tests."
                                    },
                                ],
                            },
                            "pool-2": {
                                "fallback": "other",
                                "values": [
                                    {
                                        "value": "eu.unit.tests."
                                    },
                                ],
                            },
                            "other": {
                                "values": [{
                                    "value": "en.unit.tests."
                                }]
                            },
                        },
                        "rules": [
                            {
                                "pool": "pool-1",
                                "geos": ["EU-RU"]
                            },
                            {
                                "pool": "pool-2",
                                "geos": ["EU"]
                            },
                            {
                                "pool": "other"
                            },
                        ],
                    },
                },
            ), )
        wanted.add_record(
            Record.new(
                wanted,
                "cname-dflt",
                {
                    "ttl": 300,
                    "type": "CNAME",
                    "value": "en.unit.tests.",
                    "dynamic": {
                        "pools": {
                            "pool-1": {
                                "values": [
                                    {
                                        "value": "eu.unit.tests."
                                    },
                                ],
                            },
                        },
                        "rules": [
                            {
                                "pool": "pool-1",
                                "geos": ["EU"]
                            },
                        ],
                    },
                },
            ), )

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEqual(4, len(plan.changes))
        self.assertEqual(4, provider.apply(plan))

        provider._client._request.assert_has_calls([
            call(
                "POST",
                "http://api/zones/unit.tests/geo-simple.unit.tests./A",
                data={
                    "ttl":
                    300,
                    "filters":
                    self.default_filters,
                    "resource_records": [
                        {
                            "content": ["1.1.1.1"],
                            "meta": {
                                "countries": ["RU"]
                            },
                        },
                        {
                            "content": ["1.1.1.2"],
                            "meta": {
                                "countries": ["RU"]
                            },
                        },
                        {
                            "content": ["2.2.2.1"],
                            "meta": {
                                "continents": ["EU"]
                            },
                        },
                        {
                            "content": ["3.3.3.3"],
                            "meta": {
                                "default": True
                            },
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/geo-defaults.unit.tests./A",
                data={
                    "ttl":
                    300,
                    "filters":
                    self.default_filters,
                    "resource_records": [
                        {
                            "content": ["2.2.2.1"],
                            "meta": {
                                "continents": ["EU"]
                            },
                        },
                        {
                            "content": ["3.2.3.4"],
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/cname-smpl.unit.tests./CNAME",
                data={
                    "ttl":
                    300,
                    "filters":
                    self.default_filters,
                    "resource_records": [
                        {
                            "content": ["ru-1.unit.tests."],
                            "meta": {
                                "countries": ["RU"]
                            },
                        },
                        {
                            "content": ["ru-2.unit.tests."],
                            "meta": {
                                "countries": ["RU"]
                            },
                        },
                        {
                            "content": ["eu.unit.tests."],
                            "meta": {
                                "continents": ["EU"]
                            },
                        },
                        {
                            "content": ["en.unit.tests."],
                            "meta": {
                                "default": True
                            },
                        },
                    ],
                },
            ),
            call(
                "POST",
                "http://api/zones/unit.tests/cname-dflt.unit.tests./CNAME",
                data={
                    "ttl":
                    300,
                    "filters":
                    self.default_filters,
                    "resource_records": [
                        {
                            "content": ["eu.unit.tests."],
                            "meta": {
                                "continents": ["EU"]
                            },
                        },
                        {
                            "content": ["en.unit.tests."],
                        },
                    ],
                },
            ),
        ])
Exemplo n.º 25
0
    def test_provider(self):
        source = YamlProvider('test', join(dirname(__file__), 'config'))

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

        # With target we don't add anything
        source.populate(zone, target=source)
        self.assertEquals(0, len(zone.records))

        # without it we see everything
        source.populate(zone)
        self.assertEquals(18, len(zone.records))

        # Assumption here is that a clean round-trip means that everything
        # worked as expected, data that went in came back out and could be
        # pulled in yet again and still match up. That assumes that the input
        # data completely exercises things. This assumption can be tested by
        # relatively well by running
        #   ./script/coverage tests/test_octodns_provider_yaml.py and
        # looking at the coverage file
        #   ./htmlcov/octodns_provider_yaml_py.html

        with TemporaryDirectory() as td:
            # Add some subdirs to make sure that it can create them
            directory = join(td.dirname, 'sub', 'dir')
            yaml_file = join(directory, 'unit.tests.yaml')
            target = YamlProvider('test', directory)

            # We add everything
            plan = target.plan(zone)
            self.assertEquals(
                14, len(filter(lambda c: isinstance(c, Create), plan.changes)))
            self.assertFalse(isfile(yaml_file))

            # Now actually do it
            self.assertEquals(14, target.apply(plan))
            self.assertTrue(isfile(yaml_file))

            # There should be no changes after the round trip
            reloaded = Zone('unit.tests.', [])
            target.populate(reloaded)
            self.assertFalse(zone.changes(reloaded, target=source))

            # A 2nd sync should still create everything
            plan = target.plan(zone)
            self.assertEquals(
                14, len(filter(lambda c: isinstance(c, Create), plan.changes)))

            with open(yaml_file) as fh:
                data = safe_load(fh.read())

                # these are stored as plural 'values'
                for r in data['']:
                    self.assertTrue('values' in r)
                self.assertTrue('values' in data['mx'])
                self.assertTrue('values' in data['naptr'])
                self.assertTrue('values' in data['_srv._tcp'])
                self.assertTrue('values' in data['txt'])
                # these are stored as singular 'value'
                self.assertTrue('value' in data['aaaa'])
                self.assertTrue('value' in data['ptr'])
                self.assertTrue('value' in data['spf'])
                self.assertTrue('value' in data['www'])
Exemplo n.º 26
0
 def make_expected(self):
     expected = Zone('unit.tests.', [])
     source = YamlProvider('test', join(dirname(__file__), 'config'))
     source.populate(expected)
     return expected
Exemplo n.º 27
0
class TestMythicBeastsProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test_expected', join(dirname(__file__), 'config'))
    source.populate(expected)

    # Dump anything we don't support from expected
    for record in list(expected.records):
        if record._type not in MythicBeastsProvider.SUPPORTS:
            expected._remove_record(record)

    def test_trailing_dot(self):
        with self.assertRaises(AssertionError) as err:
            add_trailing_dot('unit.tests.')
        self.assertEquals('Value already has trailing dot',
                          text_type(err.exception))

        with self.assertRaises(AssertionError) as err:
            remove_trailing_dot('unit.tests')
        self.assertEquals('Value already missing trailing dot',
                          text_type(err.exception))

        self.assertEquals(add_trailing_dot('unit.tests'), 'unit.tests.')
        self.assertEquals(remove_trailing_dot('unit.tests.'), 'unit.tests')

    def test_data_for_single(self):
        test_data = {
            'raw_values': [{
                'value': 'a:a::c',
                'ttl': 0
            }],
            'zone': 'unit.tests.',
        }
        test_single = MythicBeastsProvider._data_for_single('', test_data)
        self.assertTrue(isinstance(test_single, dict))
        self.assertEquals('a:a::c', test_single['value'])

    def test_data_for_multiple(self):
        test_data = {
            'raw_values': [{
                'value': 'b:b::d',
                'ttl': 60
            }, {
                'value': 'a:a::c',
                'ttl': 60
            }],
            'zone':
            'unit.tests.',
        }
        test_multiple = MythicBeastsProvider._data_for_multiple('', test_data)
        self.assertTrue(isinstance(test_multiple, dict))
        self.assertEquals(2, len(test_multiple['values']))

    def test_data_for_txt(self):
        test_data = {
            'raw_values': [{
                'value': 'v=DKIM1; k=rsa; p=prawf',
                'ttl': 60
            }, {
                'value': 'prawf prawf dyma prawf',
                'ttl': 300
            }],
            'zone':
            'unit.tests.',
        }
        test_txt = MythicBeastsProvider._data_for_TXT('', test_data)
        self.assertTrue(isinstance(test_txt, dict))
        self.assertEquals(2, len(test_txt['values']))
        self.assertEquals('v=DKIM1\\; k=rsa\\; p=prawf', test_txt['values'][0])

    def test_data_for_MX(self):
        test_data = {
            'raw_values': [{
                'value': '10 un.unit',
                'ttl': 60
            }, {
                'value': '20 dau.unit',
                'ttl': 60
            }, {
                'value': '30 tri.unit',
                'ttl': 60
            }],
            'zone':
            'unit.tests.',
        }
        test_MX = MythicBeastsProvider._data_for_MX('', test_data)
        self.assertTrue(isinstance(test_MX, dict))
        self.assertEquals(3, len(test_MX['values']))

        with self.assertRaises(AssertionError) as err:
            test_MX = MythicBeastsProvider._data_for_MX(
                '', {'raw_values': [{
                    'value': '',
                    'ttl': 0
                }]})
        self.assertEquals('Unable to parse MX data', text_type(err.exception))

    def test_data_for_CNAME(self):
        test_data = {
            'raw_values': [{
                'value': 'cname',
                'ttl': 60
            }],
            'zone': 'unit.tests.',
        }
        test_cname = MythicBeastsProvider._data_for_CNAME('', test_data)
        self.assertTrue(isinstance(test_cname, dict))
        self.assertEquals('cname.unit.tests.', test_cname['value'])

    def test_data_for_ANAME(self):
        test_data = {
            'raw_values': [{
                'value': 'aname',
                'ttl': 60
            }],
            'zone': 'unit.tests.',
        }
        test_aname = MythicBeastsProvider._data_for_ANAME('', test_data)
        self.assertTrue(isinstance(test_aname, dict))
        self.assertEquals('aname', test_aname['value'])

    def test_data_for_SRV(self):
        test_data = {
            'raw_values': [{
                'value': '10 20 30 un.srv.unit',
                'ttl': 60
            }, {
                'value': '20 30 40 dau.srv.unit',
                'ttl': 60
            }, {
                'value': '30 30 50 tri.srv.unit',
                'ttl': 60
            }],
            'zone':
            'unit.tests.',
        }
        test_SRV = MythicBeastsProvider._data_for_SRV('', test_data)
        self.assertTrue(isinstance(test_SRV, dict))
        self.assertEquals(3, len(test_SRV['values']))

        with self.assertRaises(AssertionError) as err:
            test_SRV = MythicBeastsProvider._data_for_SRV(
                '', {'raw_values': [{
                    'value': '',
                    'ttl': 0
                }]})
        self.assertEquals('Unable to parse SRV data', text_type(err.exception))

    def test_data_for_SSHFP(self):
        test_data = {
            'raw_values': [{
                'value': '1 1 0123456789abcdef',
                'ttl': 60
            }, {
                'value': '1 2 0123456789abcdef',
                'ttl': 60
            }, {
                'value': '2 3 0123456789abcdef',
                'ttl': 60
            }],
            'zone':
            'unit.tests.',
        }
        test_SSHFP = MythicBeastsProvider._data_for_SSHFP('', test_data)
        self.assertTrue(isinstance(test_SSHFP, dict))
        self.assertEquals(3, len(test_SSHFP['values']))

        with self.assertRaises(AssertionError) as err:
            test_SSHFP = MythicBeastsProvider._data_for_SSHFP(
                '', {'raw_values': [{
                    'value': '',
                    'ttl': 0
                }]})
        self.assertEquals('Unable to parse SSHFP data',
                          text_type(err.exception))

    def test_data_for_CAA(self):
        test_data = {
            'raw_values': [{
                'value': '1 issue letsencrypt.org',
                'ttl': 60
            }],
            'zone': 'unit.tests.',
        }
        test_CAA = MythicBeastsProvider._data_for_CAA('', test_data)
        self.assertTrue(isinstance(test_CAA, dict))
        self.assertEquals(3, len(test_CAA['value']))

        with self.assertRaises(AssertionError) as err:
            test_CAA = MythicBeastsProvider._data_for_CAA(
                '', {'raw_values': [{
                    'value': '',
                    'ttl': 0
                }]})
        self.assertEquals('Unable to parse CAA data', text_type(err.exception))

    def test_command_generation(self):
        zone = Zone('unit.tests.', [])
        zone.add_record(
            Record.new(zone, '', {
                'ttl': 60,
                'type': 'ALIAS',
                'value': 'alias.unit.tests.',
            }))
        zone.add_record(
            Record.new(
                zone, 'prawf-ns', {
                    'ttl': 300,
                    'type': 'NS',
                    'values': [
                        'alias.unit.tests.',
                        'alias2.unit.tests.',
                    ],
                }))
        zone.add_record(
            Record.new(zone, 'prawf-a', {
                'ttl': 60,
                'type': 'A',
                'values': [
                    '1.2.3.4',
                    '5.6.7.8',
                ],
            }))
        zone.add_record(
            Record.new(
                zone, 'prawf-aaaa', {
                    'ttl': 60,
                    'type': 'AAAA',
                    'values': [
                        'a:a::a',
                        'b:b::b',
                        'c:c::c:c',
                    ],
                }))
        zone.add_record(
            Record.new(zone, 'prawf-txt', {
                'ttl': 60,
                'type': 'TXT',
                'value': 'prawf prawf dyma prawf',
            }))
        zone.add_record(
            Record.new(zone, 'prawf-txt2', {
                'ttl': 60,
                'type': 'TXT',
                'value': 'v=DKIM1\\; k=rsa\\; p=prawf',
            }))
        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text='')

            provider = MythicBeastsProvider('test',
                                            {'unit.tests.': 'mypassword'})

            plan = provider.plan(zone)
            changes = plan.changes
            generated_commands = []

            for change in changes:
                generated_commands.extend(
                    provider._compile_commands('ADD', change.new))

            expected_commands = [
                'ADD unit.tests 60 ANAME alias.unit.tests.',
                'ADD prawf-ns.unit.tests 300 NS alias.unit.tests.',
                'ADD prawf-ns.unit.tests 300 NS alias2.unit.tests.',
                'ADD prawf-a.unit.tests 60 A 1.2.3.4',
                'ADD prawf-a.unit.tests 60 A 5.6.7.8',
                'ADD prawf-aaaa.unit.tests 60 AAAA a:a::a',
                'ADD prawf-aaaa.unit.tests 60 AAAA b:b::b',
                'ADD prawf-aaaa.unit.tests 60 AAAA c:c::c:c',
                'ADD prawf-txt.unit.tests 60 TXT prawf prawf dyma prawf',
                'ADD prawf-txt2.unit.tests 60 TXT v=DKIM1; k=rsa; p=prawf',
            ]

            generated_commands.sort()
            expected_commands.sort()

            self.assertEquals(generated_commands, expected_commands)

            # Now test deletion
            existing = 'prawf-txt 300 TXT prawf prawf dyma prawf\n' \
                'prawf-txt2 300 TXT v=DKIM1; k=rsa; p=prawf\n' \
                'prawf-a 60 A 1.2.3.4'

            with requests_mock() as mock:
                mock.post(ANY, status_code=200, text=existing)
                wanted = Zone('unit.tests.', [])

                plan = provider.plan(wanted)
                changes = plan.changes
                generated_commands = []

                for change in changes:
                    generated_commands.extend(
                        provider._compile_commands('DELETE', change.existing))

            expected_commands = [
                'DELETE prawf-a.unit.tests 60 A 1.2.3.4',
                'DELETE prawf-txt.unit.tests 300 TXT prawf prawf dyma prawf',
                'DELETE prawf-txt2.unit.tests 300 TXT v=DKIM1; k=rsa; p=prawf',
            ]

            generated_commands.sort()
            expected_commands.sort()

            self.assertEquals(generated_commands, expected_commands)

    def test_fake_command_generation(self):
        class FakeChangeRecord(object):
            def __init__(self):
                self.__fqdn = 'prawf.unit.tests.'
                self._type = 'NOOP'
                self.value = 'prawf'
                self.ttl = 60

            @property
            def record(self):
                return self

            @property
            def fqdn(self):
                return self.__fqdn

        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text='')

            provider = MythicBeastsProvider('test',
                                            {'unit.tests.': 'mypassword'})
            record = FakeChangeRecord()
            command = provider._compile_commands('ADD', record)
            self.assertEquals([], command)

    def test_populate(self):
        provider = None

        # Null passwords dict
        with self.assertRaises(AssertionError) as err:
            provider = MythicBeastsProvider('test', None)
        self.assertEquals('Passwords must be a dictionary',
                          text_type(err.exception))

        # Missing password
        with requests_mock() as mock:
            mock.post(ANY, status_code=401, text='ERR Not authenticated')

            with self.assertRaises(AssertionError) as err:
                provider = MythicBeastsProvider('test', dict())
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals('Missing password for domain: unit.tests',
                              text_type(err.exception))

        # Failed authentication
        with requests_mock() as mock:
            mock.post(ANY, status_code=401, text='ERR Not authenticated')

            with self.assertRaises(Exception) as err:
                provider = MythicBeastsProvider('test',
                                                {'unit.tests.': 'mypassword'})
                zone = Zone('unit.tests.', [])
                provider.populate(zone)
            self.assertEquals(
                'Mythic Beasts unauthorized for zone: unit.tests',
                err.exception.message)

        # Check unmatched lines are ignored
        test_data = 'This should not match'
        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text=test_data)

            provider = MythicBeastsProvider('test',
                                            {'unit.tests.': 'mypassword'})
            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(0, len(zone.records))

        # Check unsupported records are skipped
        test_data = '@ 60 NOOP prawf\n@ 60 SPF prawf prawf prawf'
        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text=test_data)

            provider = MythicBeastsProvider('test',
                                            {'unit.tests.': 'mypassword'})
            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(0, len(zone.records))

        # Check no changes between what we support and what's parsed
        # from the unit.tests. config YAML. Also make sure we see the same
        # for both after we've thrown away records we don't support
        with requests_mock() as mock:
            with open('tests/fixtures/mythicbeasts-list.txt') as file_handle:
                mock.post(ANY, status_code=200, text=file_handle.read())

            provider = MythicBeastsProvider('test',
                                            {'unit.tests.': 'mypassword'})
            zone = Zone('unit.tests.', [])
            provider.populate(zone)

            self.assertEquals(15, len(zone.records))
            self.assertEquals(15, len(self.expected.records))
            changes = self.expected.changes(zone, provider)
            self.assertEquals(0, len(changes))

    def test_apply(self):
        provider = MythicBeastsProvider('test', {'unit.tests.': 'mypassword'})
        zone = Zone('unit.tests.', [])

        # Create blank zone
        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text='')
            provider.populate(zone)

        self.assertEquals(0, len(zone.records))

        # Record change failed
        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text='')
            provider.populate(zone)
            zone.add_record(
                Record.new(zone, 'prawf', {
                    'ttl': 300,
                    'type': 'TXT',
                    'value': 'prawf',
                }))
            plan = provider.plan(zone)

        with requests_mock() as mock:
            mock.post(ANY, status_code=400, text='NADD 300 TXT prawf')

            with self.assertRaises(Exception) as err:
                provider.apply(plan)
            self.assertEquals(
                'Mythic Beasts could not action command: unit.tests '
                'ADD prawf.unit.tests 300 TXT prawf', err.exception.message)

        # Check deleting and adding/changing test record
        existing = 'prawf 300 TXT prawf prawf prawf\ndileu 300 TXT dileu'

        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text=existing)

            # Mash up a new zone with records so a plan
            # is generated with changes and applied. For some reason
            # passing self.expected, or just changing each record's zone
            # doesn't work. Nor does this without a single add_record after
            wanted = Zone('unit.tests.', [])
            for record in list(self.expected.records):
                data = {'type': record._type}
                data.update(record.data)
                wanted.add_record(Record.new(wanted, record.name, data))

            wanted.add_record(
                Record.new(wanted, 'prawf', {
                    'ttl': 60,
                    'type': 'TXT',
                    'value': 'prawf yw e',
                }))

            plan = provider.plan(wanted)

            # Octo ignores NS records (15-1)
            self.assertEquals(
                1, len([c for c in plan.changes if isinstance(c, Update)]))
            self.assertEquals(
                1, len([c for c in plan.changes if isinstance(c, Delete)]))
            self.assertEquals(
                14, len([c for c in plan.changes if isinstance(c, Create)]))
            self.assertEquals(16, provider.apply(plan))
            self.assertTrue(plan.exists)
Exemplo n.º 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)