Ejemplo n.º 1
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))
Ejemplo n.º 2
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.assertEquals(0, len(zone.records))

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

        source.populate(dynamic_zone)
        self.assertEquals(5, 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)

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

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

            # Dynamic plan
            plan = target.plan(dynamic_zone)
            self.assertEquals(
                5, len([c for c in plan.changes if isinstance(c, Create)]))
            self.assertFalse(isfile(dynamic_yaml_file))
            # Apply it
            self.assertEquals(5, 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)

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

            # A 2nd sync should still create everything
            plan = target.plan(zone)
            self.assertEquals(
                15, 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'))
                # these are stored as singular 'value'
                self.assertTrue('value' in data.pop('aaaa'))
                self.assertTrue('value' in data.pop('cname'))
                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.assertEquals([], 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)

                # make sure nothing is left
                self.assertEquals([], list(data.keys()))
 def test_ignores_subs(self):
     got = Zone('example.com.', ['sub'])
     self.source.populate(got)
     self.assertEquals(10, len(got.records))
Ejemplo n.º 4
0
    def test_ignored_records(self):
        zone_normal = Zone('unit.tests.', [])
        zone_ignored = Zone('unit.tests.', [])
        zone_missing = Zone('unit.tests.', [])

        normal = Record.new(zone_normal, 'www', {
            'ttl': 60,
            'type': 'A',
            'value': '9.9.9.9',
        })
        zone_normal.add_record(normal)

        ignored = Record.new(zone_ignored, 'www', {
            'octodns': {
                'ignored': True
            },
            'ttl': 60,
            'type': 'A',
            'value': '9.9.9.9',
        })
        zone_ignored.add_record(ignored)

        provider = SimpleProvider()

        self.assertFalse(zone_normal.changes(zone_ignored, provider))
        self.assertTrue(zone_normal.changes(zone_missing, provider))

        self.assertFalse(zone_ignored.changes(zone_normal, provider))
        self.assertFalse(zone_ignored.changes(zone_missing, provider))

        self.assertTrue(zone_missing.changes(zone_normal, provider))
        self.assertFalse(zone_missing.changes(zone_ignored, provider))
Ejemplo n.º 5
0
    def test_changes(self):
        before = Zone('unit.tests.', [])
        a = ARecord(before, 'a', {'ttl': 42, 'value': '1.1.1.1'})
        before.add_record(a)
        b = AaaaRecord(before, 'b', {'ttl': 42, 'value': '1:1:1::1'})
        before.add_record(b)

        after = Zone('unit.tests.', [])
        after.add_record(a)
        after.add_record(b)

        target = SimpleProvider()

        # before == after -> no changes
        self.assertFalse(before.changes(after, target))

        # add a record, delete a record -> [Delete, Create]
        c = ARecord(before, 'c', {'ttl': 42, 'value': '1.1.1.1'})
        after.add_record(c)
        after.records.remove(b)
        self.assertEquals(after.records, set([a, c]))
        changes = before.changes(after, target)
        self.assertEquals(2, len(changes))
        for change in changes:
            if isinstance(change, Create):
                create = change
            elif isinstance(change, Delete):
                delete = change
        self.assertEquals(b, delete.existing)
        self.assertFalse(delete.new)
        self.assertEquals(c, create.new)
        self.assertFalse(create.existing)
        delete.__repr__()
        create.__repr__()

        after = Zone('unit.tests.', [])
        changed = ARecord(before, 'a', {'ttl': 42, 'value': '2.2.2.2'})
        after.add_record(changed)
        after.add_record(b)
        changes = before.changes(after, target)
        self.assertEquals(1, len(changes))
        update = changes[0]
        self.assertIsInstance(update, Update)
        # Using changes here to get a full equality
        self.assertFalse(a.changes(update.existing, target))
        self.assertFalse(changed.changes(update.new, target))
        update.__repr__()
class TestNs1Provider(TestCase):
    zone = Zone('unit.tests.', [])
    expected = set()
    expected.add(
        Record.new(zone, '', {
            'ttl': 32,
            'type': 'A',
            'value': '1.2.3.4',
            'meta': {},
        }))
    expected.add(
        Record.new(zone, 'foo', {
            'ttl': 33,
            'type': 'A',
            'values': ['1.2.3.4', '1.2.3.5'],
            'meta': {},
        }))
    expected.add(
        Record.new(
            zone, 'geo', {
                'ttl': 34,
                'type': 'A',
                'values': ['101.102.103.104', '101.102.103.105'],
                'geo': {
                    'NA-US-NY': ['201.202.203.204']
                },
                'meta': {},
            }))
    expected.add(
        Record.new(zone, 'cname', {
            'ttl': 34,
            'type': 'CNAME',
            'value': 'foo.unit.tests.',
        }))
    expected.add(
        Record.new(
            zone, '', {
                'ttl':
                35,
                'type':
                'MX',
                'values': [{
                    'preference': 10,
                    'exchange': 'mx1.unit.tests.',
                }, {
                    'preference': 20,
                    'exchange': 'mx2.unit.tests.',
                }]
            }))
    expected.add(
        Record.new(
            zone, 'naptr', {
                'ttl':
                36,
                'type':
                'NAPTR',
                'values': [{
                    'flags': 'U',
                    'order': 100,
                    'preference': 100,
                    'regexp': '!^.*$!sip:[email protected]!',
                    'replacement': '.',
                    'service': 'SIP+D2U',
                }, {
                    'flags': 'S',
                    'order': 10,
                    'preference': 100,
                    'regexp': '!^.*$!sip:[email protected]!',
                    'replacement': '.',
                    'service': 'SIP+D2U',
                }]
            }))
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 37,
                'type': 'NS',
                'values': ['ns1.unit.tests.', 'ns2.unit.tests.'],
            }))
    expected.add(
        Record.new(
            zone, '_srv._tcp', {
                'ttl':
                38,
                'type':
                'SRV',
                'values': [{
                    'priority': 10,
                    'weight': 20,
                    'port': 30,
                    'target': 'foo-1.unit.tests.',
                }, {
                    'priority': 12,
                    'weight': 30,
                    'port': 30,
                    'target': 'foo-2.unit.tests.',
                }]
            }))
    expected.add(
        Record.new(
            zone, 'sub', {
                'ttl': 39,
                'type': 'NS',
                'values': ['ns3.unit.tests.', 'ns4.unit.tests.'],
            }))
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 40,
                'type': 'CAA',
                'value': {
                    'flags': 0,
                    'tag': 'issue',
                    'value': 'ca.unit.tests',
                },
            }))

    nsone_records = [{
        'type': 'A',
        'ttl': 32,
        'short_answers': ['1.2.3.4'],
        'domain': 'unit.tests.',
    }, {
        'type': 'A',
        'ttl': 33,
        'short_answers': ['1.2.3.4', '1.2.3.5'],
        'domain': 'foo.unit.tests.',
    }, {
        'type': 'A',
        'ttl': 34,
        'short_answers': ['101.102.103.104', '101.102.103.105'],
        'domain': 'geo.unit.tests',
    }, {
        'type': 'CNAME',
        'ttl': 34,
        'short_answers': ['foo.unit.tests'],
        'domain': 'cname.unit.tests.',
    }, {
        'type':
        'MX',
        'ttl':
        35,
        'short_answers': ['10 mx1.unit.tests.', '20 mx2.unit.tests'],
        'domain':
        'unit.tests.',
    }, {
        'type':
        'NAPTR',
        'ttl':
        36,
        'short_answers': [
            '10 100 S SIP+D2U !^.*$!sip:[email protected]! .',
            '100 100 U SIP+D2U !^.*$!sip:[email protected]! .'
        ],
        'domain':
        'naptr.unit.tests.',
    }, {
        'type': 'NS',
        'ttl': 37,
        'short_answers': ['ns1.unit.tests.', 'ns2.unit.tests'],
        'domain': 'unit.tests.',
    }, {
        'type':
        'SRV',
        'ttl':
        38,
        'short_answers':
        ['12 30 30 foo-2.unit.tests.', '10 20 30 foo-1.unit.tests'],
        'domain':
        '_srv._tcp.unit.tests.',
    }, {
        'type': 'NS',
        'ttl': 39,
        'short_answers': ['ns3.unit.tests.', 'ns4.unit.tests'],
        'domain': 'sub.unit.tests.',
    }, {
        'type': 'CAA',
        'ttl': 40,
        'short_answers': ['0 issue ca.unit.tests'],
        'domain': 'unit.tests.',
    }]

    @patch('nsone.NSONE.loadZone')
    def test_populate(self, load_mock):
        provider = Ns1Provider('test', 'api-key')

        # Bad auth
        load_mock.side_effect = AuthException('unauthorized')
        zone = Zone('unit.tests.', [])
        with self.assertRaises(AuthException) as ctx:
            provider.populate(zone)
        self.assertEquals(load_mock.side_effect, ctx.exception)

        # General error
        load_mock.reset_mock()
        load_mock.side_effect = ResourceException('boom')
        zone = Zone('unit.tests.', [])
        with self.assertRaises(ResourceException) as ctx:
            provider.populate(zone)
        self.assertEquals(load_mock.side_effect, ctx.exception)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

        # Non-existent zone doesn't populate anything
        load_mock.reset_mock()
        load_mock.side_effect = \
            ResourceException('server error: zone not found')
        zone = Zone('unit.tests.', [])
        exists = provider.populate(zone)
        self.assertEquals(set(), zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])
        self.assertFalse(exists)

        # Existing zone w/o records
        load_mock.reset_mock()
        nsone_zone = DummyZone([])
        load_mock.side_effect = [nsone_zone]
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(1, len(zone.records))
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

        # Existing zone w/records
        load_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records)
        load_mock.side_effect = [nsone_zone]
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(self.expected, zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

        # Test skipping unsupported record type
        load_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records +
                               [{
                                   'type': 'UNSUPPORTED',
                                   'ttl': 42,
                                   'short_answers': ['unsupported'],
                                   'domain': 'unsupported.unit.tests.',
                               }])
        load_mock.side_effect = [nsone_zone]
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(self.expected, zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

    @patch('nsone.NSONE.createZone')
    @patch('nsone.NSONE.loadZone')
    def test_sync(self, load_mock, create_mock):
        provider = Ns1Provider('test', 'api-key')

        desired = Zone('unit.tests.', [])
        for r in self.expected:
            desired.add_record(r)

        plan = provider.plan(desired)
        # everything except the root NS
        expected_n = len(self.expected) - 1
        self.assertEquals(expected_n, len(plan.changes))
        self.assertTrue(plan.exists)

        # Fails, general error
        load_mock.reset_mock()
        create_mock.reset_mock()
        load_mock.side_effect = ResourceException('boom')
        with self.assertRaises(ResourceException) as ctx:
            provider.apply(plan)
        self.assertEquals(load_mock.side_effect, ctx.exception)

        # Fails, bad auth
        load_mock.reset_mock()
        create_mock.reset_mock()
        load_mock.side_effect = \
            ResourceException('server error: zone not found')
        create_mock.side_effect = AuthException('unauthorized')
        with self.assertRaises(AuthException) as ctx:
            provider.apply(plan)
        self.assertEquals(create_mock.side_effect, ctx.exception)

        # non-existent zone, create
        load_mock.reset_mock()
        create_mock.reset_mock()
        load_mock.side_effect = \
            ResourceException('server error: zone not found')
        # ugh, need a mock zone with a mock prop since we're using getattr, we
        # can actually control side effects on `meth` with that.
        mock_zone = Mock()
        mock_zone.add_SRV = Mock()
        mock_zone.add_SRV.side_effect = [
            RateLimitException('boo', period=0),
            None,
        ]
        create_mock.side_effect = [mock_zone]
        got_n = provider.apply(plan)
        self.assertEquals(expected_n, got_n)

        # Update & delete
        load_mock.reset_mock()
        create_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records +
                               [{
                                   'type': 'A',
                                   'ttl': 42,
                                   'short_answers': ['9.9.9.9'],
                                   'domain': 'delete-me.unit.tests.',
                               }])
        nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2'
        nsone_zone.loadRecord = Mock()
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        load_mock.side_effect = [nsone_zone, nsone_zone]
        plan = provider.plan(desired)
        self.assertEquals(3, len(plan.changes))
        self.assertIsInstance(plan.changes[0], Update)
        self.assertIsInstance(plan.changes[2], Delete)
        # ugh, we need a mock record that can be returned from loadRecord for
        # the update and delete targets, we can add our side effects to that to
        # trigger rate limit handling
        mock_record = Mock()
        mock_record.update.side_effect = [
            RateLimitException('one', period=0),
            None,
            None,
        ]
        mock_record.delete.side_effect = [
            RateLimitException('two', period=0),
            None,
            None,
        ]
        nsone_zone.loadRecord.side_effect = [
            mock_record, mock_record, mock_record
        ]
        got_n = provider.apply(plan)
        self.assertEquals(3, got_n)
        nsone_zone.loadRecord.assert_has_calls([
            call('unit.tests', u'A'),
            call('geo', u'A'),
            call('delete-me', u'A'),
        ])
        mock_record.assert_has_calls([
            call.update(answers=[{
                'answer': [u'1.2.3.4'],
                'meta': {}
            }],
                        filters=[],
                        ttl=32),
            call.update(answers=[{
                u'answer': [u'1.2.3.4'],
                u'meta': {}
            }],
                        filters=[],
                        ttl=32),
            call.update(answers=[
                {
                    u'answer': [u'101.102.103.104'],
                    u'meta': {}
                },
                {
                    u'answer': [u'101.102.103.105'],
                    u'meta': {}
                },
                {
                    u'answer': [u'201.202.203.204'],
                    u'meta': {
                        u'iso_region_code': [u'NA-US-NY']
                    },
                },
            ],
                        filters=[
                            {
                                u'filter': u'shuffle',
                                u'config': {}
                            },
                            {
                                u'filter': u'geotarget_country',
                                u'config': {}
                            },
                            {
                                u'filter': u'select_first_n',
                                u'config': {
                                    u'N': 1
                                }
                            },
                        ],
                        ttl=34),
            call.delete(),
            call.delete()
        ])

    def test_escaping(self):
        provider = Ns1Provider('test', 'api-key')
        record = {'ttl': 31, 'short_answers': ['foo; bar baz; blip']}
        self.assertEquals(['foo\\; bar baz\\; blip'],
                          provider._data_for_SPF('SPF', record)['values'])

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

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

        record = Record.new(zone, 'txt', {
            'ttl': 35,
            'type': 'TXT',
            'value': 'foo\\; bar baz\\; blip'
        })
        self.assertEquals(['foo; bar baz; blip'],
                          provider._params_for_TXT(record)['answers'])

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

        # answers from nsone
        a_record = {
            'ttl': 31,
            'type': 'CNAME',
            'short_answers': ['foo.unit.tests.']
        }
        a_expected = {'ttl': 31, 'type': 'CNAME', 'value': 'foo.unit.tests.'}
        self.assertEqual(a_expected,
                         provider._data_for_CNAME(a_record['type'], a_record))

        # no answers from nsone
        b_record = {'ttl': 32, 'type': 'CNAME', 'short_answers': []}
        b_expected = {'ttl': 32, 'type': 'CNAME', 'value': None}
        self.assertEqual(b_expected,
                         provider._data_for_CNAME(b_record['type'], b_record))
Ejemplo n.º 7
0
 def test_missing_dot(self):
     with self.assertRaises(Exception) as ctx:
         Zone('not.allowed', [])
     self.assertTrue('missing ending dot' in ctx.exception.message)
    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(
                f'{self.host}{zone_path}?limit=100&q=zone_type%3APRIMARY&'
                'offset=0',
                status_code=200,
                json=zone_payload)
            mock.get(f'{self.host}{rec_path}?offset=0&limit=100',
                     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)
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(f'{self.host}{path}',
                      status_code=401,
                      text='{"errorCode": 60001}')
            with self.assertRaises(Exception) as ctx:
                UltraProvider('test', 'account', 'user', 'wrongpass')
            self.assertEquals('Unauthorized', str(ctx.exception))

        # Good Auth
        with requests_mock() as mock:
            headers = {'Content-Type': 'application/x-www-form-urlencoded'}
            mock.post(f'{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(parse_qs(mock.last_request.text),
                              parse_qs(expected_payload))

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

        # Test authorization issue
        with requests_mock() as mock:
            mock.get(f'{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(f'{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(f'{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(
                f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY&'
                'offset=0',
                status_code=200,
                json={
                    "resultInfo": {
                        "totalCount": 15,
                        "offset": 0,
                        "returnedCount": 10
                    },
                    "zones": []
                })
            mock.get(
                f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY'
                '&offset=10',
                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(f'{self.host}{path}',
                     status_code=401,
                     headers={'Authorization': 'Bearer 123'},
                     json={})
            with self.assertRaises(Exception) as ctx:
                provider._get(path)
            self.assertEquals('Unauthorized', str(ctx.exception))

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

            mock.get(f'{self.host}{path}?a=1',
                     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(f'{self.host}{path}',
                      status_code=200,
                      headers={'Authorization': 'Bearer 123'},
                      json=payload)
            provider._post(path, json=payload)

            mock.post(f'{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(f'{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(f'{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(
                f'{self.host}{zone_path}?limit=100&q=zone_type%3APRIMARY&'
                'offset=0',
                status_code=200,
                json=zone_payload)
            mock.get(f'{self.host}{rec_path}?offset=0&limit=100',
                     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(
                    f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY&'
                    'offset=0',
                    status_code=200,
                    text=fh.read())
            with open('tests/fixtures/ultra-zones-page-2.json') as fh:
                mock.get(
                    f'{self.host}{path}?limit=100&q=zone_type%3APRIMARY&'
                    'offset=10',
                    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(f'{self.host}{rec_path}?offset=0&limit=100',
                         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(f'{self.host}{rec_path}?offset=10&limit=100',
                         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)
Ejemplo n.º 10
0
class TestOvhProvider(TestCase):
    api_record = []
    valid_dkim = []
    invalid_dkim = []

    valid_dkim_key = "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxLaG16G4SaE" \
                     "cXVdiIxTg7gKSGbHKQLm30CHib1h9FzS9nkcyvQSyQj1rMFyqC//" \
                     "tft3ohx3nvJl+bGCWxdtLYDSmir9PW54e5CTdxEh8MWRkBO3StF6" \
                     "QG/tAh3aTGDmkqhIJGLb87iHvpmVKqURmEUzJPv5KPJfWLofADI+" \
                     "q9lQIDAQAB"

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

    # A, subdomain=''
    api_record.append({
        'fieldType': 'A',
        'ttl': 100,
        'target': '1.2.3.4',
        'subDomain': '',
        'id': 1
    })
    expected.add(
        Record.new(zone, '', {
            'ttl': 100,
            'type': 'A',
            'value': '1.2.3.4',
        }))

    # A, subdomain='sub
    api_record.append({
        'fieldType': 'A',
        'ttl': 200,
        'target': '1.2.3.4',
        'subDomain': 'sub',
        'id': 2
    })
    expected.add(
        Record.new(zone, 'sub', {
            'ttl': 200,
            'type': 'A',
            'value': '1.2.3.4',
        }))

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

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

    # NAPTR
    api_record.append({
        'fieldType': 'NAPTR',
        'ttl': 500,
        'target': '10 100 "S" "SIP+D2U" "!^.*$!sip:[email protected]!" .',
        'subDomain': 'naptr',
        'id': 5
    })
    expected.add(
        Record.new(
            zone, 'naptr', {
                'ttl':
                500,
                'type':
                'NAPTR',
                'values': [{
                    'flags': 'S',
                    'order': 10,
                    'preference': 100,
                    'regexp': '!^.*$!sip:[email protected]!',
                    'replacement': '.',
                    'service': 'SIP+D2U',
                }]
            }))

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

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

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

    # PTR
    api_record.append({
        'fieldType': 'PTR',
        'ttl': 900,
        'target': 'unit.tests.',
        'subDomain': '4',
        'id': 12
    })
    expected.add(
        Record.new(zone, '4', {
            'ttl': 900,
            'type': 'PTR',
            'value': 'unit.tests.'
        }))

    # SPF
    api_record.append({
        'fieldType': 'SPF',
        'ttl': 1000,
        'target': 'v=spf1 include:unit.texts.redirect ~all',
        'subDomain': '',
        'id': 13
    })
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 1000,
                'type': 'SPF',
                'value': 'v=spf1 include:unit.texts.redirect ~all'
            }))

    # SSHFP
    api_record.append({
        'fieldType': 'SSHFP',
        'ttl': 1100,
        'target': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73 ',
        'subDomain': '',
        'id': 14
    })
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 1100,
                'type': 'SSHFP',
                'value': {
                    'algorithm': 1,
                    'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73',
                    'fingerprint_type': 1
                }
            }))

    # AAAA
    api_record.append({
        'fieldType': 'AAAA',
        'ttl': 1200,
        'target': '1:1ec:1::1',
        'subDomain': '',
        'id': 15
    })
    expected.add(
        Record.new(zone, '', {
            'ttl': 200,
            'type': 'AAAA',
            'value': '1:1ec:1::1',
        }))

    # DKIM
    api_record.append({
        'fieldType': 'DKIM',
        'ttl': 1300,
        'target': valid_dkim_key,
        'subDomain': 'dkim',
        'id': 16
    })
    expected.add(
        Record.new(zone, 'dkim', {
            'ttl': 1300,
            'type': 'TXT',
            'value': valid_dkim_key,
        }))

    # TXT
    api_record.append({
        'fieldType': 'TXT',
        'ttl': 1400,
        'target': 'TXT text',
        'subDomain': 'txt',
        'id': 17
    })
    expected.add(
        Record.new(zone, 'txt', {
            'ttl': 1400,
            'type': 'TXT',
            'value': 'TXT text',
        }))

    # LOC
    # We do not have associated record for LOC, as it's not managed
    api_record.append({
        'fieldType': 'LOC',
        'ttl': 1500,
        'target': '1 1 1 N 1 1 1 E 1m 1m',
        'subDomain': '',
        'id': 18
    })

    valid_dkim = [
        valid_dkim_key,
        'v=DKIM1 \\; %s' % valid_dkim_key,
        'h=sha256 \\; %s' % valid_dkim_key,
        'h=sha1 \\; %s' % valid_dkim_key,
        's=* \\; %s' % valid_dkim_key,
        's=email \\; %s' % valid_dkim_key,
        't=y \\; %s' % valid_dkim_key,
        't=s \\; %s' % valid_dkim_key,
        'k=rsa \\; %s' % valid_dkim_key,
        'n=notes \\; %s' % valid_dkim_key,
        'g=granularity \\; %s' % valid_dkim_key,
    ]
    invalid_dkim = [
        'p=%invalid%',  # Invalid public key
        'v=DKIM1',  # Missing public key
        'v=DKIM2 \\; %s' % valid_dkim_key,  # Invalid version
        'h=sha512 \\; %s' % valid_dkim_key,  # Invalid hash algo
        's=fake \\; %s' % valid_dkim_key,  # Invalid selector
        't=fake \\; %s' % valid_dkim_key,  # Invalid flag
        'u=invalid \\; %s' % valid_dkim_key,  # Invalid key
    ]

    @patch('ovh.Client')
    def test_populate(self, client_mock):
        provider = OvhProvider('test', 'endpoint', 'application_key',
                               'application_secret', 'consumer_key')

        with patch.object(provider._client, 'get') as get_mock:
            zone = Zone('unit.tests.', [])
            get_mock.side_effect = ResourceNotFoundError('boom')
            with self.assertRaises(APIError) as ctx:
                provider.populate(zone)
            self.assertEquals(get_mock.side_effect, ctx.exception)

            get_mock.side_effect = InvalidCredential('boom')
            with self.assertRaises(APIError) as ctx:
                provider.populate(zone)
            self.assertEquals(get_mock.side_effect, ctx.exception)

            zone = Zone('unit.tests.', [])
            get_mock.side_effect = ResourceNotFoundError('This service does '
                                                         'not exist')
            exists = provider.populate(zone)
            self.assertEquals(set(), zone.records)
            self.assertFalse(exists)

            zone = Zone('unit.tests.', [])
            get_returns = [[record['id'] for record in self.api_record]]
            get_returns += self.api_record
            get_mock.side_effect = get_returns
            exists = provider.populate(zone)
            self.assertEquals(self.expected, zone.records)
            self.assertTrue(exists)

    @patch('ovh.Client')
    def test_is_valid_dkim(self, client_mock):
        """Test _is_valid_dkim"""
        provider = OvhProvider('test', 'endpoint', 'application_key',
                               'application_secret', 'consumer_key')
        for dkim in self.valid_dkim:
            self.assertTrue(provider._is_valid_dkim(dkim))
        for dkim in self.invalid_dkim:
            self.assertFalse(provider._is_valid_dkim(dkim))

    @patch('ovh.Client')
    def test_apply(self, client_mock):
        provider = OvhProvider('test', 'endpoint', 'application_key',
                               'application_secret', 'consumer_key')

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

        for r in self.expected:
            desired.add_record(r)

        with patch.object(provider._client, 'post') as get_mock:
            plan = provider.plan(desired)
            get_mock.side_effect = APIError('boom')
            with self.assertRaises(APIError) as ctx:
                provider.apply(plan)
            self.assertEquals(get_mock.side_effect, ctx.exception)

        # Records get by API call
        with patch.object(provider._client, 'get') as get_mock:
            get_returns = [[1, 2, 3, 4], {
                'fieldType': 'A',
                'ttl': 600,
                'target': '5.6.7.8',
                'subDomain': '',
                'id': 100
            }, {
                'fieldType': 'A',
                'ttl': 600,
                'target': '5.6.7.8',
                'subDomain': 'fake',
                'id': 101
            }, {
                'fieldType': 'TXT',
                'ttl': 600,
                'target': 'fake txt record',
                'subDomain': 'txt',
                'id': 102
            }, {
                'fieldType': 'DKIM',
                'ttl': 600,
                'target': 'v=DKIM1; %s' % self.valid_dkim_key,
                'subDomain': 'dkim',
                'id': 103
            }]
            get_mock.side_effect = get_returns

            plan = provider.plan(desired)

            with patch.object(provider._client, 'post') as post_mock, \
                    patch.object(provider._client, 'delete') as delete_mock:
                get_mock.side_effect = [[100], [101], [102], [103]]
                provider.apply(plan)
                wanted_calls = [
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'TXT',
                         subDomain='txt',
                         target=u'TXT text',
                         ttl=1400),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'DKIM',
                         subDomain='dkim',
                         target=self.valid_dkim_key,
                         ttl=1300),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain=u'',
                         target=u'1.2.3.4',
                         ttl=100),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'SRV',
                         subDomain='_srv._tcp',
                         target=u'10 20 30 foo-1.unit.tests.',
                         ttl=800),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'SRV',
                         subDomain='_srv._tcp',
                         target=u'40 50 60 foo-2.unit.tests.',
                         ttl=800),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'PTR',
                         subDomain='4',
                         target=u'unit.tests.',
                         ttl=900),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'NS',
                         subDomain='www3',
                         target=u'ns3.unit.tests.',
                         ttl=700),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'NS',
                         subDomain='www3',
                         target=u'ns4.unit.tests.',
                         ttl=700),
                    call(
                        u'/domain/zone/unit.tests/record',
                        fieldType=u'SSHFP',
                        subDomain=u'',
                        ttl=1100,
                        target=u'1 1 bf6b6825d2977c511a475bbefb88a'
                        u'ad54'
                        u'a92ac73',
                    ),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'AAAA',
                         subDomain=u'',
                         target=u'1:1ec:1::1',
                         ttl=200),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'MX',
                         subDomain=u'',
                         target=u'10 mx1.unit.tests.',
                         ttl=400),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'CNAME',
                         subDomain='www2',
                         target=u'unit.tests.',
                         ttl=300),
                    call(
                        u'/domain/zone/unit.tests/record',
                        fieldType=u'SPF',
                        subDomain=u'',
                        ttl=1000,
                        target=u'v=spf1 include:unit.texts.'
                        u'redirect ~all',
                    ),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain='sub',
                         target=u'1.2.3.4',
                         ttl=200),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'NAPTR',
                         subDomain='naptr',
                         ttl=500,
                         target=u'10 100 "S" "SIP+D2U" "!^.*$!sip:'
                         u'info@bar'
                         u'.example.com!" .'),
                    call(u'/domain/zone/unit.tests/refresh')
                ]

                post_mock.assert_has_calls(wanted_calls)

                # Get for delete calls
                wanted_get_calls = [
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'TXT',
                         subDomain='txt'),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'DKIM',
                         subDomain='dkim'),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain=u''),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain='fake')
                ]
                get_mock.assert_has_calls(wanted_get_calls)
                # 4 delete calls for update and delete
                delete_mock.assert_has_calls([
                    call(u'/domain/zone/unit.tests/record/100'),
                    call(u'/domain/zone/unit.tests/record/101'),
                    call(u'/domain/zone/unit.tests/record/102'),
                    call(u'/domain/zone/unit.tests/record/103')
                ])
Ejemplo n.º 11
0
    def test_apply(self, client_mock):
        provider = OvhProvider('test', 'endpoint', 'application_key',
                               'application_secret', 'consumer_key')

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

        for r in self.expected:
            desired.add_record(r)

        with patch.object(provider._client, 'post') as get_mock:
            plan = provider.plan(desired)
            get_mock.side_effect = APIError('boom')
            with self.assertRaises(APIError) as ctx:
                provider.apply(plan)
            self.assertEquals(get_mock.side_effect, ctx.exception)

        # Records get by API call
        with patch.object(provider._client, 'get') as get_mock:
            get_returns = [[1, 2, 3, 4], {
                'fieldType': 'A',
                'ttl': 600,
                'target': '5.6.7.8',
                'subDomain': '',
                'id': 100
            }, {
                'fieldType': 'A',
                'ttl': 600,
                'target': '5.6.7.8',
                'subDomain': 'fake',
                'id': 101
            }, {
                'fieldType': 'TXT',
                'ttl': 600,
                'target': 'fake txt record',
                'subDomain': 'txt',
                'id': 102
            }, {
                'fieldType': 'DKIM',
                'ttl': 600,
                'target': 'v=DKIM1; %s' % self.valid_dkim_key,
                'subDomain': 'dkim',
                'id': 103
            }]
            get_mock.side_effect = get_returns

            plan = provider.plan(desired)

            with patch.object(provider._client, 'post') as post_mock, \
                    patch.object(provider._client, 'delete') as delete_mock:
                get_mock.side_effect = [[100], [101], [102], [103]]
                provider.apply(plan)
                wanted_calls = [
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'TXT',
                         subDomain='txt',
                         target=u'TXT text',
                         ttl=1400),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'DKIM',
                         subDomain='dkim',
                         target=self.valid_dkim_key,
                         ttl=1300),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain=u'',
                         target=u'1.2.3.4',
                         ttl=100),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'SRV',
                         subDomain='_srv._tcp',
                         target=u'10 20 30 foo-1.unit.tests.',
                         ttl=800),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'SRV',
                         subDomain='_srv._tcp',
                         target=u'40 50 60 foo-2.unit.tests.',
                         ttl=800),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'PTR',
                         subDomain='4',
                         target=u'unit.tests.',
                         ttl=900),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'NS',
                         subDomain='www3',
                         target=u'ns3.unit.tests.',
                         ttl=700),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'NS',
                         subDomain='www3',
                         target=u'ns4.unit.tests.',
                         ttl=700),
                    call(
                        u'/domain/zone/unit.tests/record',
                        fieldType=u'SSHFP',
                        subDomain=u'',
                        ttl=1100,
                        target=u'1 1 bf6b6825d2977c511a475bbefb88a'
                        u'ad54'
                        u'a92ac73',
                    ),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'AAAA',
                         subDomain=u'',
                         target=u'1:1ec:1::1',
                         ttl=200),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'MX',
                         subDomain=u'',
                         target=u'10 mx1.unit.tests.',
                         ttl=400),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'CNAME',
                         subDomain='www2',
                         target=u'unit.tests.',
                         ttl=300),
                    call(
                        u'/domain/zone/unit.tests/record',
                        fieldType=u'SPF',
                        subDomain=u'',
                        ttl=1000,
                        target=u'v=spf1 include:unit.texts.'
                        u'redirect ~all',
                    ),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain='sub',
                         target=u'1.2.3.4',
                         ttl=200),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'NAPTR',
                         subDomain='naptr',
                         ttl=500,
                         target=u'10 100 "S" "SIP+D2U" "!^.*$!sip:'
                         u'info@bar'
                         u'.example.com!" .'),
                    call(u'/domain/zone/unit.tests/refresh')
                ]

                post_mock.assert_has_calls(wanted_calls)

                # Get for delete calls
                wanted_get_calls = [
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'TXT',
                         subDomain='txt'),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'DKIM',
                         subDomain='dkim'),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain=u''),
                    call(u'/domain/zone/unit.tests/record',
                         fieldType=u'A',
                         subDomain='fake')
                ]
                get_mock.assert_has_calls(wanted_get_calls)
                # 4 delete calls for update and delete
                delete_mock.assert_has_calls([
                    call(u'/domain/zone/unit.tests/record/100'),
                    call(u'/domain/zone/unit.tests/record/101'),
                    call(u'/domain/zone/unit.tests/record/102'),
                    call(u'/domain/zone/unit.tests/record/103')
                ])
    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', str(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', 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)

        # 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]
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', str(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', 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)

        # 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)
    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)
Ejemplo n.º 15
0
class TestDigitalOceanProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

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

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

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

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

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

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

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

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(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):
        provider = DigitalOceanProvider('test', 'token')

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

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

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

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

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST',
                 '/domains',
                 data={
                     'ip_address': '192.0.2.1',
                     'name': 'unit.tests'
                 }),
            # get all records in newly created zone
            call('GET', '/domains/unit.tests/records', {'page': 1}),
            # delete the initial A record
            call('DELETE', '/domains/unit.tests/records/11189877'),
            # created at least some of the record with expected data
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.4',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.5',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': 'ca.unit.tests.',
                     'flags': 0,
                     'name': '@',
                     'tag': 'issue',
                     'ttl': 3600,
                     'type': 'CAA'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'name': '_imap._tcp',
                     'weight': 0,
                     'data': '.',
                     'priority': 0,
                     'ttl': 600,
                     'type': 'SRV',
                     'port': 0
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'name': '_pop3._tcp',
                     'weight': 0,
                     'data': '.',
                     'priority': 0,
                     'ttl': 600,
                     'type': 'SRV',
                     'port': 0
                 }),
            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(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',
            'data': '1.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189898,
            'name': 'www',
            'data': '2.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189899,
            'name': 'ttl',
            'data': '3.2.3.4',
            'ttl': 600,
            'type': 'A',
        }])

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

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

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/domains/unit.tests/records/11189899'),
            call('DELETE', '/domains/unit.tests/records/11189897'),
            call('DELETE', '/domains/unit.tests/records/11189898')
        ],
                                                   any_order=True)
    def test_apply(self):
        provider = _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_populate(self, load_mock):
        provider = Ns1Provider('test', 'api-key')

        # Bad auth
        load_mock.side_effect = AuthException('unauthorized')
        zone = Zone('unit.tests.', [])
        with self.assertRaises(AuthException) as ctx:
            provider.populate(zone)
        self.assertEquals(load_mock.side_effect, ctx.exception)

        # General error
        load_mock.reset_mock()
        load_mock.side_effect = ResourceException('boom')
        zone = Zone('unit.tests.', [])
        with self.assertRaises(ResourceException) as ctx:
            provider.populate(zone)
        self.assertEquals(load_mock.side_effect, ctx.exception)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

        # Non-existent zone doesn't populate anything
        load_mock.reset_mock()
        load_mock.side_effect = \
            ResourceException('server error: zone not found')
        zone = Zone('unit.tests.', [])
        exists = provider.populate(zone)
        self.assertEquals(set(), zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])
        self.assertFalse(exists)

        # Existing zone w/o records
        load_mock.reset_mock()
        nsone_zone = DummyZone([])
        load_mock.side_effect = [nsone_zone]
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(1, len(zone.records))
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

        # Existing zone w/records
        load_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records)
        load_mock.side_effect = [nsone_zone]
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(self.expected, zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

        # Test skipping unsupported record type
        load_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records +
                               [{
                                   'type': 'UNSUPPORTED',
                                   'ttl': 42,
                                   'short_answers': ['unsupported'],
                                   'domain': 'unsupported.unit.tests.',
                               }])
        load_mock.side_effect = [nsone_zone]
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(self.expected, zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])
    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)
    def test_sync(self, load_mock, create_mock):
        provider = Ns1Provider('test', 'api-key')

        desired = Zone('unit.tests.', [])
        for r in self.expected:
            desired.add_record(r)

        plan = provider.plan(desired)
        # everything except the root NS
        expected_n = len(self.expected) - 1
        self.assertEquals(expected_n, len(plan.changes))
        self.assertTrue(plan.exists)

        # Fails, general error
        load_mock.reset_mock()
        create_mock.reset_mock()
        load_mock.side_effect = ResourceException('boom')
        with self.assertRaises(ResourceException) as ctx:
            provider.apply(plan)
        self.assertEquals(load_mock.side_effect, ctx.exception)

        # Fails, bad auth
        load_mock.reset_mock()
        create_mock.reset_mock()
        load_mock.side_effect = \
            ResourceException('server error: zone not found')
        create_mock.side_effect = AuthException('unauthorized')
        with self.assertRaises(AuthException) as ctx:
            provider.apply(plan)
        self.assertEquals(create_mock.side_effect, ctx.exception)

        # non-existent zone, create
        load_mock.reset_mock()
        create_mock.reset_mock()
        load_mock.side_effect = \
            ResourceException('server error: zone not found')
        # ugh, need a mock zone with a mock prop since we're using getattr, we
        # can actually control side effects on `meth` with that.
        mock_zone = Mock()
        mock_zone.add_SRV = Mock()
        mock_zone.add_SRV.side_effect = [
            RateLimitException('boo', period=0),
            None,
        ]
        create_mock.side_effect = [mock_zone]
        got_n = provider.apply(plan)
        self.assertEquals(expected_n, got_n)

        # Update & delete
        load_mock.reset_mock()
        create_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records +
                               [{
                                   'type': 'A',
                                   'ttl': 42,
                                   'short_answers': ['9.9.9.9'],
                                   'domain': 'delete-me.unit.tests.',
                               }])
        nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2'
        nsone_zone.loadRecord = Mock()
        zone_search = Mock()
        zone_search.return_value = [
            {
                "domain":
                "geo.unit.tests",
                "zone":
                "unit.tests",
                "type":
                "A",
                "answers": [
                    {
                        'answer': ['1.1.1.1'],
                        'meta': {}
                    },
                    {
                        'answer': ['1.2.3.4'],
                        'meta': {
                            'ca_province': ['ON']
                        }
                    },
                    {
                        'answer': ['2.3.4.5'],
                        'meta': {
                            'us_state': ['NY']
                        }
                    },
                    {
                        'answer': ['3.4.5.6'],
                        'meta': {
                            'country': ['US']
                        }
                    },
                    {
                        'answer': ['4.5.6.7'],
                        'meta': {
                            'iso_region_code': ['NA-US-WA']
                        }
                    },
                ],
                'ttl':
                34,
            },
        ]
        nsone_zone.search = zone_search
        load_mock.side_effect = [nsone_zone, nsone_zone]
        plan = provider.plan(desired)
        self.assertEquals(3, len(plan.changes))
        self.assertIsInstance(plan.changes[0], Update)
        self.assertIsInstance(plan.changes[2], Delete)
        # ugh, we need a mock record that can be returned from loadRecord for
        # the update and delete targets, we can add our side effects to that to
        # trigger rate limit handling
        mock_record = Mock()
        mock_record.update.side_effect = [
            RateLimitException('one', period=0),
            None,
            None,
        ]
        mock_record.delete.side_effect = [
            RateLimitException('two', period=0),
            None,
            None,
        ]
        nsone_zone.loadRecord.side_effect = [
            mock_record, mock_record, mock_record
        ]
        got_n = provider.apply(plan)
        self.assertEquals(3, got_n)
        nsone_zone.loadRecord.assert_has_calls([
            call('unit.tests', u'A'),
            call('geo', u'A'),
            call('delete-me', u'A'),
        ])
        mock_record.assert_has_calls([
            call.update(answers=[{
                'answer': [u'1.2.3.4'],
                'meta': {}
            }],
                        filters=[],
                        ttl=32),
            call.update(answers=[{
                u'answer': [u'1.2.3.4'],
                u'meta': {}
            }],
                        filters=[],
                        ttl=32),
            call.update(answers=[
                {
                    u'answer': [u'101.102.103.104'],
                    u'meta': {}
                },
                {
                    u'answer': [u'101.102.103.105'],
                    u'meta': {}
                },
                {
                    u'answer': [u'201.202.203.204'],
                    u'meta': {
                        u'iso_region_code': [u'NA-US-NY']
                    },
                },
            ],
                        filters=[
                            {
                                u'filter': u'shuffle',
                                u'config': {}
                            },
                            {
                                u'filter': u'geotarget_country',
                                u'config': {}
                            },
                            {
                                u'filter': u'select_first_n',
                                u'config': {
                                    u'N': 1
                                }
                            },
                        ],
                        ttl=34),
            call.delete(),
            call.delete()
        ])
Ejemplo n.º 20
0
 def make_expected(self):
     expected = Zone('unit.tests.', [])
     source = YamlProvider('test', join(dirname(__file__), 'config'))
     source.populate(expected)
     return expected
Ejemplo n.º 21
0
 def test_lowering(self):
     zone = Zone('UniT.TEsTs.', [])
     self.assertEquals('unit.tests.', zone.name)
Ejemplo n.º 22
0
    def test_populate(self):
        _expected = self.make_expected()

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

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

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

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

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

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

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

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

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

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

        return
Ejemplo n.º 23
0
    def test_cname_coexisting(self):
        zone = Zone('unit.tests.', [])
        a = Record.new(zone, 'www', {
            'ttl': 60,
            'type': 'A',
            'value': '9.9.9.9',
        })
        cname = Record.new(zone, 'www', {
            'ttl': 60,
            'type': 'CNAME',
            'value': 'foo.bar.com.',
        })

        # add cname to a
        zone.add_record(a)
        with self.assertRaises(InvalidNodeException):
            zone.add_record(cname)

        # add a to cname
        zone = Zone('unit.tests.', [])
        zone.add_record(cname)
        with self.assertRaises(InvalidNodeException):
            zone.add_record(a)
Ejemplo n.º 24
0
from octodns.record import Create, Delete, Record
from octodns.provider.azuredns import _AzureRecord, AzureProvider, \
    _check_endswith_dot, _parse_azure_type
from octodns.zone import Zone
from octodns.provider.base import Plan

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

from unittest import TestCase
from mock import Mock, patch


zone = Zone(name='unit.tests.', sub_zones=[])
octo_records = []
octo_records.append(Record.new(zone, '', {
    'ttl': 0,
    'type': 'A',
    'values': ['1.2.3.4', '10.10.10.10']}))
octo_records.append(Record.new(zone, 'a', {
    'ttl': 1,
    'type': 'A',
    'values': ['1.2.3.4', '1.1.1.1']}))
octo_records.append(Record.new(zone, 'aa', {
    'ttl': 9001,
    'type': 'A',
    'values': ['1.2.4.3']}))
octo_records.append(Record.new(zone, 'aaa', {
    'ttl': 2,
Ejemplo n.º 25
0
    def test_provider(self):
        source = SplitYamlProvider('test',
                                   join(dirname(__file__), 'config/split'))

        zone = Zone('unit.tests.', [])
        dynamic_zone = Zone('dynamic.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))

        source.populate(dynamic_zone)
        self.assertEquals(5, len(dynamic_zone.records))

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

            # We add everything
            plan = target.plan(zone)
            self.assertEquals(
                15, len([c for c in plan.changes if isinstance(c, Create)]))
            self.assertFalse(isdir(zone_dir))

            # Now actually do it
            self.assertEquals(15, target.apply(plan))

            # Dynamic plan
            plan = target.plan(dynamic_zone)
            self.assertEquals(
                5, len([c for c in plan.changes if isinstance(c, Create)]))
            self.assertFalse(isdir(dynamic_zone_dir))
            # Apply it
            self.assertEquals(5, target.apply(plan))
            self.assertTrue(isdir(dynamic_zone_dir))

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

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

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

            yaml_file = join(zone_dir, '$unit.tests.yaml')
            self.assertTrue(isfile(yaml_file))
            with open(yaml_file) as fh:
                data = safe_load(fh.read())
                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 records are stored as plural "values." Check each file to
            # ensure correctness.
            for record_name in ('_srv._tcp', 'mx', 'naptr', 'sub', 'txt'):
                yaml_file = join(zone_dir, '{}.yaml'.format(record_name))
                self.assertTrue(isfile(yaml_file))
                with open(yaml_file) as fh:
                    data = safe_load(fh.read())
                    self.assertTrue('values' in data.pop(record_name))

            # These are stored as singular "value." Again, check each file.
            for record_name in ('aaaa', 'cname', 'included', 'ptr', 'spf',
                                'www.sub', 'www'):
                yaml_file = join(zone_dir, '{}.yaml'.format(record_name))
                self.assertTrue(isfile(yaml_file))
                with open(yaml_file) as fh:
                    data = safe_load(fh.read())
                    self.assertTrue('value' in data.pop(record_name))

            # Again with the plural, this time checking dynamic.tests.
            for record_name in ('a', 'aaaa', 'real-ish-a'):
                yaml_file = join(dynamic_zone_dir,
                                 '{}.yaml'.format(record_name))
                self.assertTrue(isfile(yaml_file))
                with open(yaml_file) as fh:
                    data = safe_load(fh.read())
                    dyna = data.pop(record_name)
                    self.assertTrue('values' in dyna)
                    self.assertTrue('dynamic' in dyna)

            # Singular again.
            for record_name in ('cname', 'simple-weighted'):
                yaml_file = join(dynamic_zone_dir,
                                 '{}.yaml'.format(record_name))
                self.assertTrue(isfile(yaml_file))
                with open(yaml_file) as fh:
                    data = safe_load(fh.read())
                    dyna = data.pop(record_name)
                    self.assertTrue('value' in dyna)
                    self.assertTrue('dynamic' in dyna)
Ejemplo n.º 26
0
def main():
    parser = ArgumentParser(description=__doc__.split('\n')[1])

    parser.add_argument('--config-file', required=True,
                        help='The Manager configuration file to use')
    parser.add_argument('--zone', required=True, help='Zone to dump')
    parser.add_argument('--source', required=True, default=[], action='append',
                        help='Source(s) to pull data from')
    parser.add_argument('--num-workers', default=4,
                        help='Number of background workers')
    parser.add_argument('--timeout', default=1,
                        help='Number seconds to wait for an answer')
    parser.add_argument('server', nargs='+', help='Servers to query')

    args = parser.parse_args()

    manager = Manager(args.config_file)

    log = getLogger('report')

    try:
        sources = [manager.providers[source] for source in args.source]
    except KeyError as e:
        raise Exception('Unknown source: {}'.format(e.args[0]))

    zone = Zone(args.zone, manager.configured_sub_zones(args.zone))
    for source in sources:
        source.populate(zone)

    print('name,type,ttl,{},consistent'.format(','.join(args.server)))
    resolvers = []
    ip_addr_re = re.compile(r'^[\d\.]+$')
    for server in args.server:
        resolver = AsyncResolver(configure=False,
                                 num_workers=int(args.num_workers))
        if not ip_addr_re.match(server):
            server = str(query(server, 'A')[0])
        log.info('server=%s', server)
        resolver.nameservers = [server]
        resolver.lifetime = int(args.timeout)
        resolvers.append(resolver)

    queries = {}
    for record in sorted(zone.records):
        queries[record] = [r.query(record.fqdn, record._type)
                           for r in resolvers]

    for record, futures in sorted(queries.items(), key=lambda d: d[0]):
        stdout.write(record.fqdn)
        stdout.write(',')
        stdout.write(record._type)
        stdout.write(',')
        stdout.write(str(record.ttl))
        compare = {}
        for future in futures:
            stdout.write(',')
            try:
                answers = [str(r) for r in future.result()]
            except (NoAnswer, NoNameservers):
                answers = ['*no answer*']
            except NXDOMAIN:
                answers = ['*does not exist*']
            except Timeout:
                answers = ['*timeout*']
            stdout.write(' '.join(answers))
            # sorting to ignore order
            answers = '*:*'.join(sorted(answers)).lower()
            compare[answers] = True
        stdout.write(',True\n' if len(compare) == 1 else ',False\n')
Ejemplo n.º 27
0
#
#
#

from __future__ import absolute_import, division, print_function, \
    unicode_literals

from unittest import TestCase

from octodns.processor.filter import TypeAllowlistFilter, TypeRejectlistFilter
from octodns.record import Record
from octodns.zone import Zone

zone = Zone('unit.tests.', [])
for record in [
    Record.new(zone, 'a', {
        'ttl': 30,
        'type': 'A',
        'value': '1.2.3.4',
    }),
    Record.new(zone, 'aaaa', {
        'ttl': 30,
        'type': 'AAAA',
        'value': '::1',
    }),
    Record.new(zone, 'txt', {
        'ttl': 30,
        'type': 'TXT',
        'value': 'Hello World!',
    }),
    Record.new(zone, 'a2', {
Ejemplo n.º 28
0
    def test_apply(self):
        provider = DigitalOceanProvider('test', 'token')

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

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

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

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

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST',
                 '/domains',
                 data={
                     'ip_address': '192.0.2.1',
                     'name': 'unit.tests'
                 }),
            # get all records in newly created zone
            call('GET', '/domains/unit.tests/records', {'page': 1}),
            # delete the initial A record
            call('DELETE', '/domains/unit.tests/records/11189877'),
            # created at least some of the record with expected data
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.4',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '1.2.3.5',
                     'name': '@',
                     'ttl': 300,
                     'type': 'A'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': 'ca.unit.tests.',
                     'flags': 0,
                     'name': '@',
                     'tag': 'issue',
                     'ttl': 3600,
                     'type': 'CAA'
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'name': '_imap._tcp',
                     'weight': 0,
                     'data': '.',
                     'priority': 0,
                     'ttl': 600,
                     'type': 'SRV',
                     'port': 0
                 }),
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'name': '_pop3._tcp',
                     'weight': 0,
                     'data': '.',
                     'priority': 0,
                     'ttl': 600,
                     'type': 'SRV',
                     'port': 0
                 }),
            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(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',
            'data': '1.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189898,
            'name': 'www',
            'data': '2.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189899,
            'name': 'ttl',
            'data': '3.2.3.4',
            'ttl': 600,
            'type': 'A',
        }])

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

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

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

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

        changes = expected.changes(got, SimpleProvider())
        self.assertEquals([], changes)
Ejemplo n.º 30
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.records.remove(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')
        ])