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

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

    def test_populate(self):
        provider = AkamaiProvider("test", "secret", "akam.com", "atok", "ctok")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            try:
                changes = provider.apply(plan)
            except NameError as e:
                expected = "contractId not specified to create zone"
                self.assertEquals(text_type(e), expected)
コード例 #2
0
    def test_apply(self):
        provider = CloudflareProvider('test', 'email', 'token')

        provider._request = Mock()

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

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

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

        provider._request.reset_mock()

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

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

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

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

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

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

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

        # get the list of zones, create a zone, add some records, update
        # something, and delete something
        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call('POST',
                 '/zones/42/dns_records',
                 data={
                     'content': '4.4.4.4',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997654',
                 data={
                     'content': '2.2.2.2',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997653',
                 data={
                     'content': '3.3.3.3',
                     'type': 'A',
                     'name': 'a.unit.tests',
                     'proxied': False,
                     'ttl': 300
                 }),
        ])
コード例 #4
0
    def test_sync(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

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

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

        execute_mock.reset_mock()

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

        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.records.DNSRecord.delete') as delete_mock:
                with patch('dyn.tm.zones.Zone._update') as update_mock:
                    plan = provider.plan(new)
                    provider.apply(plan)
                    update_mock.assert_called()
                # we expect 4 deletes, 2 from actual deletes and 2 from
                # updates which delete and recreate
                self.assertEquals(4, len(delete_mock.call_args_list))
            # the 2 (re)creates
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        self.assertEquals(3, len(plan.changes))
コード例 #5
0
    def test_base_provider(self):
        with self.assertRaises(NotImplementedError) as ctx:
            BaseProvider('base')
        self.assertEquals('Abstract base class, log property missing',
                          text_type(ctx.exception))

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

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

        class HasSupportsGeo(HasLog):
            SUPPORTS_GEO = False

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

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

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

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

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

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

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

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

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

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

        with self.assertRaises(NotImplementedError) as ctx:
            HasPopulate('haspopulate').apply(plan)
        self.assertEquals('Abstract base class, _apply method missing',
                          text_type(ctx.exception))
コード例 #6
0
class TestNs1Provider(TestCase):
    zone = Zone('unit.tests.', [])
    expected = set()
    expected.add(
        Record.new(zone, '', {
            'ttl': 32,
            'type': 'A',
            'value': '1.2.3.4',
        }))
    expected.add(
        Record.new(zone, 'foo', {
            'ttl': 33,
            'type': 'A',
            'values': ['1.2.3.4', '1.2.3.5'],
        }))
    expected.add(
        Record.new(zone, 'cname', {
            'ttl': 34,
            'type': 'CNAME',
            'value': 'foo.unit.tests.',
        }))
    expected.add(
        Record.new(
            zone, '', {
                'ttl':
                35,
                'type':
                'MX',
                'values': [{
                    'preference': 10,
                    'exchange': 'mx1.unit.tests.',
                }, {
                    'preference': 20,
                    'exchange': 'mx2.unit.tests.',
                }]
            }))
    expected.add(
        Record.new(
            zone, 'naptr', {
                'ttl':
                36,
                'type':
                'NAPTR',
                'values': [{
                    'flags': 'U',
                    'order': 100,
                    'preference': 100,
                    'regexp': '!^.*$!sip:[email protected]!',
                    'replacement': '.',
                    'service': 'SIP+D2U',
                }, {
                    'flags': 'S',
                    'order': 10,
                    'preference': 100,
                    'regexp': '!^.*$!sip:[email protected]!',
                    'replacement': '.',
                    'service': 'SIP+D2U',
                }]
            }))
    expected.add(
        Record.new(
            zone, '', {
                'ttl': 37,
                'type': 'NS',
                'values': ['ns1.unit.tests.', 'ns2.unit.tests.'],
            }))
    expected.add(
        Record.new(
            zone, '_srv._tcp', {
                'ttl':
                38,
                'type':
                'SRV',
                'values': [{
                    'priority': 10,
                    'weight': 20,
                    'port': 30,
                    'target': 'foo-1.unit.tests.',
                }, {
                    'priority': 12,
                    'weight': 30,
                    'port': 30,
                    'target': 'foo-2.unit.tests.',
                }]
            }))
    expected.add(
        Record.new(
            zone, 'sub', {
                'ttl': 39,
                'type': 'NS',
                'values': ['ns3.unit.tests.', 'ns4.unit.tests.'],
            }))

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

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

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

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

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

        # Existing zone w/o records
        load_mock.reset_mock()
        nsone_zone = DummyZone([])
        load_mock.side_effect = [nsone_zone]
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(set(), zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

        # Existing zone w/records
        load_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records)
        load_mock.side_effect = [nsone_zone]
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(self.expected, zone.records)
        self.assertEquals(('unit.tests', ), load_mock.call_args[0])

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

        desired = Zone('unit.tests.', [])
        desired.records.update(self.expected)

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

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

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

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

        # Update & delete
        load_mock.reset_mock()
        create_mock.reset_mock()
        nsone_zone = DummyZone(self.nsone_records +
                               [{
                                   'type': 'A',
                                   'ttl': 42,
                                   'short_answers': ['9.9.9.9'],
                                   'domain': 'delete-me.unit.tests.',
                               }])
        nsone_zone.data['records'][0]['short_answers'][0] = '2.2.2.2'
        nsone_zone.loadRecord = Mock()
        load_mock.side_effect = [nsone_zone, nsone_zone]
        plan = provider.plan(desired)
        self.assertEquals(2, len(plan.changes))
        self.assertIsInstance(plan.changes[0], Update)
        self.assertIsInstance(plan.changes[1], Delete)
        # ugh, we need a mock record that can be returned from loadRecord for
        # the update and delete targets, we can add our side effects to that to
        # trigger rate limit handling
        mock_record = Mock()
        mock_record.update.side_effect = [
            RateLimitException('one', period=0),
            None,
        ]
        mock_record.delete.side_effect = [
            RateLimitException('two', period=0),
            None,
        ]
        nsone_zone.loadRecord.side_effect = [mock_record, mock_record]
        got_n = provider.apply(plan)
        self.assertEquals(2, got_n)
        nsone_zone.loadRecord.assert_has_calls([
            call('unit.tests', u'A'),
            call('delete-me', u'A'),
        ])
        mock_record.assert_has_calls(
            [call.update(answers=[u'1.2.3.4'], ttl=32),
             call.delete()])

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

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

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

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

        record = Record.new(zone, 'txt', {
            'ttl': 35,
            'type': 'TXT',
            'value': 'foo\; bar baz\; blip'
        })
        self.assertEquals(['foo; bar baz; blip'],
                          provider._params_for_TXT(record)['answers'])
コード例 #7
0
class TestDynProviderAlias(TestCase):
    expected = Zone('unit.tests.', [])
    for name, data in (('', {
            'type': 'ALIAS',
            'ttl': 300,
            'value': 'www.unit.tests.'
    }), ('www', {
            'type': 'A',
            'ttl': 300,
            'values': ['1.2.3.4']
    })):
        expected.add_record(Record.new(expected, name, data))

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

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

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

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

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

        # No existing records, create all
        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.zones.Zone._update') as update_mock:
                plan = provider.plan(self.expected)
                update_mock.assert_not_called()
                provider.apply(plan)
                update_mock.assert_called()
            add_mock.assert_called()
            # Once for each dyn record
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/Zone/unit.tests/', 'GET', {})
        ])
        self.assertEquals(2, len(plan.changes))
コード例 #8
0
class TestGandiProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

    def test_populate(self):

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

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

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

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

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

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

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

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

            with self.assertRaises(GandiClientNotFound) as ctx:
                zone = Zone('unit.tests.', [])
                provider._client.zone(zone)
            self.assertIn('"cause": "Not Found"', str(ctx.exception))

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

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

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

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

        del provider._zone_records[zone.name]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        provider._client._request.reset_mock()

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

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

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

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

        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('DELETE', '/livedns/domains/unit.tests/records/www/A'),
            call('DELETE', '/livedns/domains/unit.tests/records/ttl/A'),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'ttl',
                     'rrset_ttl': 300,
                     'rrset_type': 'A',
                     'rrset_values': ['3.2.3.4']
                 })
        ],
                                                   any_order=True)
コード例 #9
0
    def test_SSHFP(self):
        # doesn't blow up
        Record.new(
            self.zone, '', {
                'type': 'SSHFP',
                'ttl': 600,
                'value': {
                    'algorithm': 1,
                    'fingerprint_type': 1,
                    'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73'
                }
            })

        # missing algorithm
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '', {
                    'type': 'SSHFP',
                    'ttl': 600,
                    'value': {
                        'fingerprint_type': 1,
                        'fingerprint':
                        'bf6b6825d2977c511a475bbefb88aad54a92ac73'
                    }
                })
        self.assertEquals(['missing algorithm'], ctx.exception.reasons)

        # invalid algorithm
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '', {
                    'type': 'SSHFP',
                    'ttl': 600,
                    'value': {
                        'algorithm': 'nope',
                        'fingerprint_type': 1,
                        'fingerprint':
                        'bf6b6825d2977c511a475bbefb88aad54a92ac73'
                    }
                })
        self.assertEquals(['invalid algorithm "nope"'], ctx.exception.reasons)

        # missing fingerprint_type
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '', {
                    'type': 'SSHFP',
                    'ttl': 600,
                    'value': {
                        'algorithm': 1,
                        'fingerprint':
                        'bf6b6825d2977c511a475bbefb88aad54a92ac73'
                    }
                })
        self.assertEquals(['missing fingerprint_type'], ctx.exception.reasons)

        # invalid fingerprint_type
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '', {
                    'type': 'SSHFP',
                    'ttl': 600,
                    'value': {
                        'algorithm': 1,
                        'fingerprint_type': 'yeeah',
                        'fingerprint':
                        'bf6b6825d2977c511a475bbefb88aad54a92ac73'
                    }
                })
        self.assertEquals(['invalid fingerprint_type "yeeah"'],
                          ctx.exception.reasons)

        # missing fingerprint
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '', {
                    'type': 'SSHFP',
                    'ttl': 600,
                    'value': {
                        'algorithm': 1,
                        'fingerprint_type': 1,
                    }
                })
        self.assertEquals(['missing fingerprint'], ctx.exception.reasons)
コード例 #10
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] * 15  # 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
                 }),
        ])
        # expected number of total calls
        self.assertEquals(17, 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')
        ])
コード例 #11
0
    def test_apply(self):
        provider = GandiProvider('test_id', 'token')

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

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

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

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

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

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

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

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

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

        provider._client._request.reset_mock()

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

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

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

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

        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('DELETE', '/livedns/domains/unit.tests/records/www/A'),
            call('DELETE', '/livedns/domains/unit.tests/records/ttl/A'),
            call('POST',
                 '/livedns/domains/unit.tests/records',
                 data={
                     'rrset_name': 'ttl',
                     'rrset_ttl': 300,
                     'rrset_type': 'A',
                     'rrset_values': ['3.2.3.4']
                 })
        ],
                                                   any_order=True)
コード例 #12
0
    def test_apply(self):
        provider = MythicBeastsProvider('test', {'unit.tests.': 'mypassword'})
        zone = Zone('unit.tests.', [])

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

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

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

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

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

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

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

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

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

            plan = provider.plan(wanted)

            # Octo ignores NS records (15-1)
            self.assertEquals(
                1, len([c for c in plan.changes if isinstance(c, Update)]))
            self.assertEquals(
                1, len([c for c in plan.changes if isinstance(c, Delete)]))
            self.assertEquals(
                14, len([c for c in plan.changes if isinstance(c, Create)]))
            self.assertEquals(16, provider.apply(plan))
            self.assertTrue(plan.exists)
コード例 #13
0
    def test_command_generation(self):
        zone = Zone('unit.tests.', [])
        zone.add_record(
            Record.new(zone, 'prawf-alias', {
                'ttl': 60,
                'type': 'ALIAS',
                'value': 'alias.unit.tests.',
            }))
        zone.add_record(
            Record.new(
                zone, 'prawf-ns', {
                    'ttl': 300,
                    'type': 'NS',
                    'values': [
                        'alias.unit.tests.',
                        'alias2.unit.tests.',
                    ],
                }))
        zone.add_record(
            Record.new(zone, 'prawf-a', {
                'ttl': 60,
                'type': 'A',
                'values': [
                    '1.2.3.4',
                    '5.6.7.8',
                ],
            }))
        zone.add_record(
            Record.new(
                zone, 'prawf-aaaa', {
                    'ttl': 60,
                    'type': 'AAAA',
                    'values': [
                        'a:a::a',
                        'b:b::b',
                        'c:c::c:c',
                    ],
                }))
        zone.add_record(
            Record.new(zone, 'prawf-txt', {
                'ttl': 60,
                'type': 'TXT',
                'value': 'prawf prawf dyma prawf',
            }))
        zone.add_record(
            Record.new(zone, 'prawf-txt2', {
                'ttl': 60,
                'type': 'TXT',
                'value': 'v=DKIM1\\; k=rsa\\; p=prawf',
            }))
        with requests_mock() as mock:
            mock.post(ANY, status_code=200, text='')

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

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

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

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

            generated_commands.sort()
            expected_commands.sort()

            self.assertEquals(generated_commands, expected_commands)

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

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

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

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

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

            generated_commands.sort()
            expected_commands.sort()

            self.assertEquals(generated_commands, expected_commands)
コード例 #14
0
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',
                },
            }))

    ns1_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('ns1.rest.zones.Zones.retrieve')
    def test_populate(self, zone_retrieve_mock):
        provider = Ns1Provider('test', 'api-key')

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

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

        # Non-existent zone doesn't populate anything
        zone_retrieve_mock.reset_mock()
        zone_retrieve_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', ), zone_retrieve_mock.call_args[0])
        self.assertFalse(exists)

        # Existing zone w/o records
        zone_retrieve_mock.reset_mock()
        ns1_zone = {
            'records': [{
                "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,
            }],
        }
        zone_retrieve_mock.side_effect = [ns1_zone]
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(1, len(zone.records))
        self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0])

        # Existing zone w/records
        zone_retrieve_mock.reset_mock()
        ns1_zone = {
            'records':
            self.ns1_records + [{
                "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,
            }],
        }
        zone_retrieve_mock.side_effect = [ns1_zone]
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(self.expected, zone.records)
        self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0])

        # Test skipping unsupported record type
        zone_retrieve_mock.reset_mock()
        ns1_zone = {
            'records':
            self.ns1_records + [{
                'type': 'UNSUPPORTED',
                'ttl': 42,
                'short_answers': ['unsupported'],
                'domain': 'unsupported.unit.tests.',
            }, {
                "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,
            }],
        }
        zone_retrieve_mock.side_effect = [ns1_zone]
        zone = Zone('unit.tests.', [])
        provider.populate(zone)
        self.assertEquals(self.expected, zone.records)
        self.assertEquals(('unit.tests', ), zone_retrieve_mock.call_args[0])

    @patch('ns1.rest.records.Records.delete')
    @patch('ns1.rest.records.Records.update')
    @patch('ns1.rest.records.Records.create')
    @patch('ns1.rest.records.Records.retrieve')
    @patch('ns1.rest.zones.Zones.create')
    @patch('ns1.rest.zones.Zones.retrieve')
    def test_sync(self, zone_retrieve_mock, zone_create_mock,
                  record_retrieve_mock, record_create_mock, record_update_mock,
                  record_delete_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
        zone_retrieve_mock.reset_mock()
        zone_create_mock.reset_mock()
        zone_retrieve_mock.side_effect = ResourceException('boom')
        with self.assertRaises(ResourceException) as ctx:
            provider.apply(plan)
        self.assertEquals(zone_retrieve_mock.side_effect, ctx.exception)

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

        # non-existent zone, create
        zone_retrieve_mock.reset_mock()
        zone_create_mock.reset_mock()
        zone_retrieve_mock.side_effect = \
            ResourceException('server error: zone not found')

        zone_create_mock.side_effect = ['foo']
        # Test out the create rate-limit handling, then 9 successes
        record_create_mock.side_effect = [
            RateLimitException('boo', period=0),
        ] + ([None] * 9)

        got_n = provider.apply(plan)
        self.assertEquals(expected_n, got_n)

        # Zone was created
        zone_create_mock.assert_has_calls([call('unit.tests')])
        # Checking that we got some of the expected records too
        record_create_mock.assert_has_calls([
            call('unit.tests',
                 'unit.tests',
                 'A',
                 answers=[{
                     'answer': ['1.2.3.4'],
                     'meta': {}
                 }],
                 filters=[],
                 ttl=32),
            call('unit.tests',
                 'unit.tests',
                 'CAA',
                 answers=[(0, 'issue', 'ca.unit.tests')],
                 ttl=40),
            call('unit.tests',
                 'unit.tests',
                 'MX',
                 answers=[(10, 'mx1.unit.tests.'), (20, 'mx2.unit.tests.')],
                 ttl=35),
        ])

        # Update & delete
        zone_retrieve_mock.reset_mock()
        zone_create_mock.reset_mock()

        ns1_zone = {
            'records':
            self.ns1_records + [
                {
                    'type': 'A',
                    'ttl': 42,
                    'short_answers': ['9.9.9.9'],
                    'domain': 'delete-me.unit.tests.',
                },
                {
                    "domain":
                    "geo.unit.tests",
                    "zone":
                    "unit.tests",
                    "type":
                    "A",
                    "short_answers": [
                        '1.1.1.1',
                        '1.2.3.4',
                        '2.3.4.5',
                        '3.4.5.6',
                        '4.5.6.7',
                    ],
                    'tier':
                    3,  # This flags it as advacned, full load required
                    'ttl':
                    34,
                }
            ],
        }
        ns1_zone['records'][0]['short_answers'][0] = '2.2.2.2'

        record_retrieve_mock.side_effect = [{
            "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']
                    }
                },
            ],
            'tier':
            3,
            'ttl':
            34,
        }]

        zone_retrieve_mock.side_effect = [ns1_zone, ns1_zone]
        plan = provider.plan(desired)
        self.assertEquals(3, len(plan.changes))
        # Shouldn't rely on order so just count classes
        classes = defaultdict(lambda: 0)
        for change in plan.changes:
            classes[change.__class__] += 1
        self.assertEquals(1, classes[Delete])
        self.assertEquals(2, classes[Update])

        record_update_mock.side_effect = [
            RateLimitException('one', period=0),
            None,
            None,
        ]
        record_delete_mock.side_effect = [
            RateLimitException('two', period=0),
            None,
            None,
        ]

        got_n = provider.apply(plan)
        self.assertEquals(3, got_n)

        record_update_mock.assert_has_calls([
            call('unit.tests',
                 'unit.tests',
                 'A',
                 answers=[{
                     'answer': ['1.2.3.4'],
                     'meta': {}
                 }],
                 filters=[],
                 ttl=32),
            call('unit.tests',
                 'unit.tests',
                 'A',
                 answers=[{
                     'answer': ['1.2.3.4'],
                     'meta': {}
                 }],
                 filters=[],
                 ttl=32),
            call('unit.tests',
                 'geo.unit.tests',
                 'A',
                 answers=[{
                     'answer': ['101.102.103.104'],
                     'meta': {}
                 }, {
                     'answer': ['101.102.103.105'],
                     'meta': {}
                 }, {
                     'answer': ['201.202.203.204'],
                     'meta': {
                         'iso_region_code': ['NA-US-NY']
                     }
                 }],
                 filters=[{
                     'filter': 'shuffle',
                     'config': {}
                 }, {
                     'filter': 'geotarget_country',
                     'config': {}
                 }, {
                     'filter': 'select_first_n',
                     'config': {
                         'N': 1
                     }
                 }],
                 ttl=34)
        ])

    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 ns1
        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 ns1
        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))
コード例 #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(12, len(zone.records))
            changes = self.expected.changes(zone, provider)
            self.assertEquals(0, len(changes))

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

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

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

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

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

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

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

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

        provider._client._request.reset_mock()

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

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

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

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/domains/unit.tests/records/11189899'),
            call('DELETE', '/domains/unit.tests/records/11189897'),
            call('DELETE', '/domains/unit.tests/records/11189898')
        ],
                                                   any_order=True)
コード例 #16
0
    def test_SRV(self):
        # doesn't blow up
        Record.new(
            self.zone, '_srv._tcp', {
                'type': 'SRV',
                'ttl': 600,
                'value': {
                    'priority': 1,
                    'weight': 2,
                    'port': 3,
                    'target': 'foo.bar.baz.'
                }
            })

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

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

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

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

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

        # missing target
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 2,
                        'port': 3,
                    }
                })
        self.assertEquals(['missing target'], ctx.exception.reasons)
        # invalid target
        with self.assertRaises(ValidationError) as ctx:
            Record.new(
                self.zone, '_srv._tcp', {
                    'type': 'SRV',
                    'ttl': 600,
                    'value': {
                        'priority': 1,
                        'weight': 2,
                        'port': 3,
                        'target': 'foo.bar.baz'
                    }
                })
        self.assertEquals(['missing trailing .'], ctx.exception.reasons)
コード例 #17
0
    def test_existing_nameservers(self):
        ns_values = ['8.8.8.8.', '9.9.9.9.']
        provider = PowerDnsProvider('test',
                                    'non.existent',
                                    'api-key',
                                    nameserver_values=ns_values)

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

        # no changes
        with requests_mock() as mock:
            data = {
                'rrsets': [{
                    'comments': [],
                    'name':
                    'unit.tests.',
                    'records': [{
                        'content': '8.8.8.8.',
                        'disabled': False
                    }, {
                        'content': '9.9.9.9.',
                        'disabled': False
                    }],
                    'ttl':
                    600,
                    'type':
                    'NS'
                }, {
                    'comments': [],
                    'name':
                    'unit.tests.',
                    'records': [{
                        'content': '1.2.3.4',
                        'disabled': False,
                    }],
                    'ttl':
                    60,
                    'type':
                    'A'
                }]
            }
            mock.get(ANY, status_code=200, json=data)

            unrelated_record = Record.new(expected, '', {
                'type': 'A',
                'ttl': 60,
                'value': '1.2.3.4'
            })
            expected.add_record(unrelated_record)
            plan = provider.plan(expected)
            self.assertFalse(plan)
            # remove it now that we don't need the unrelated change any longer
            expected._remove_record(unrelated_record)

        # ttl diff
        with requests_mock() as mock:
            data = {
                'rrsets': [{
                    'comments': [],
                    'name':
                    'unit.tests.',
                    'records': [
                        {
                            'content': '8.8.8.8.',
                            'disabled': False
                        },
                        {
                            'content': '9.9.9.9.',
                            'disabled': False
                        },
                    ],
                    'ttl':
                    3600,
                    'type':
                    'NS'
                }]
            }
            mock.get(ANY, status_code=200, json=data)

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

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

            plan = provider.plan(expected)
            self.assertEquals(1, len(plan.changes))
コード例 #18
0
    def test_A_and_values_mixin(self):
        # doesn't blow up
        Record.new(self.zone, '', {
            'type': 'A',
            'ttl': 600,
            'value': '1.2.3.4',
        })
        Record.new(self.zone, '', {
            'type': 'A',
            'ttl': 600,
            'values': [
                '1.2.3.4',
                '1.2.3.5',
            ]
        })

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

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

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

        # invalid & valid ip addresses, no ttl
        with self.assertRaises(ValidationError) as ctx:
            Record.new(self.zone, '', {
                'type': 'A',
                'values': ['1.2.3.4', 'hello', '5.6.7.8']
            })
        self.assertEquals([
            'missing ttl',
            'invalid ip address "hello"',
        ], ctx.exception.reasons)
コード例 #19
0
from octodns.provider.base import Plan

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

from unittest import TestCase
from mock import Mock, patch

zone = Zone(name='unit.tests.', sub_zones=[])
octo_records = []
octo_records.append(
    Record.new(zone, '', {
        'ttl': 0,
        'type': 'A',
        'values': ['1.2.3.4', '10.10.10.10']
    }))
octo_records.append(
    Record.new(zone, 'a', {
        'ttl': 1,
        'type': 'A',
        'values': ['1.2.3.4', '1.1.1.1']
    }))
octo_records.append(
    Record.new(zone, 'aa', {
        'ttl': 9001,
        'type': 'A',
        'values': ['1.2.4.3']
    }))
octo_records.append(
コード例 #20
0
    def test_apply(self):
        provider = DnsimpleProvider('test', 'token', 42)

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

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

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

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST', '/domains', data={'name': 'unit.tests'}),
            # created at least one of the record with expected data
            call('POST',
                 '/zones/unit.tests/records',
                 data={
                     'content': '20 30 foo-1.unit.tests.',
                     'priority': 10,
                     'type': 'SRV',
                     'name': '_srv._tcp',
                     'ttl': 600
                 }),
        ])
        # expected number of total calls
        self.assertEquals(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',
            'content': '1.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189898,
            'name': 'www',
            'content': '2.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189899,
            'name': 'ttl',
            'content': '3.2.3.4',
            'ttl': 600,
            'type': 'A',
        }])
        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

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

        plan = provider.plan(wanted)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/zones/unit.tests/records',
                 data={
                     'content': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/zones/unit.tests/records/11189899'),
            call('DELETE', '/zones/unit.tests/records/11189897'),
            call('DELETE', '/zones/unit.tests/records/11189898')
        ],
                                                   any_order=True)
コード例 #21
0
class TestDynProvider(TestCase):
    expected = Zone('unit.tests.', [])
    for name, data in (('', {
            'type': 'A',
            'ttl': 300,
            'values': ['1.2.3.4']
    }), ('cname', {
            'type': 'CNAME',
            'ttl': 301,
            'value': 'unit.tests.'
    }), ('', {
            'type':
            'MX',
            'ttl':
            302,
            'values': [{
                'preference': 10,
                'exchange': 'smtp-1.unit.tests.'
            }, {
                'preference': 20,
                'exchange': 'smtp-2.unit.tests.'
            }]
    }), ('naptr', {
            'type':
            'NAPTR',
            'ttl':
            303,
            'values': [{
                'order': 100,
                'preference': 101,
                'flags': 'U',
                'service': 'SIP+D2U',
                'regexp': '!^.*$!sip:[email protected]!',
                'replacement': '.',
            }, {
                'order': 200,
                'preference': 201,
                'flags': 'U',
                'service': 'SIP+D2U',
                'regexp': '!^.*$!sip:[email protected]!',
                'replacement': '.',
            }]
    }), ('sub', {
            'type': 'NS',
            'ttl': 3600,
            'values': ['ns3.p10.dynect.net.', 'ns3.p10.dynect.net.'],
    }), ('ptr', {
            'type': 'PTR',
            'ttl': 304,
            'value': 'xx.unit.tests.'
    }), ('spf', {
            'type': 'SPF',
            'ttl': 305,
            'values': ['v=spf1 ip4:192.168.0.1/16-all', 'v=spf1 -all'],
    }), ('', {
            'type': 'SSHFP',
            'ttl': 306,
            'value': {
                'algorithm': 1,
                'fingerprint_type': 1,
                'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73',
            }
    }), ('_srv._tcp', {
            'type':
            'SRV',
            'ttl':
            307,
            'values': [{
                'priority': 11,
                'weight': 12,
                'port': 10,
                'target': 'foo-1.unit.tests.'
            }, {
                'priority': 21,
                'weight': 22,
                'port': 20,
                'target': 'foo-2.unit.tests.'
            }]
    })):
        expected.add_record(Record.new(expected, name, data))

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

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

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

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

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

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

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

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

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

        execute_mock.reset_mock()

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

        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.records.DNSRecord.delete') as delete_mock:
                with patch('dyn.tm.zones.Zone._update') as update_mock:
                    plan = provider.plan(new)
                    provider.apply(plan)
                    update_mock.assert_called()
                # we expect 4 deletes, 2 from actual deletes and 2 from
                # updates which delete and recreate
                self.assertEquals(4, len(delete_mock.call_args_list))
            # the 2 (re)creates
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        self.assertEquals(3, len(plan.changes))
コード例 #22
0
class TestDnsimpleProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        provider._client._request.assert_has_calls([
            # created the domain
            call('POST', '/domains', data={'name': 'unit.tests'}),
            # created at least one of the record with expected data
            call('POST',
                 '/zones/unit.tests/records',
                 data={
                     'content': '20 30 foo-1.unit.tests.',
                     'priority': 10,
                     'type': 'SRV',
                     'name': '_srv._tcp',
                     'ttl': 600
                 }),
        ])
        # expected number of total calls
        self.assertEquals(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',
            'content': '1.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189898,
            'name': 'www',
            'content': '2.2.3.4',
            'ttl': 300,
            'type': 'A',
        }, {
            'id': 11189899,
            'name': 'ttl',
            'content': '3.2.3.4',
            'ttl': 600,
            'type': 'A',
        }])
        # Domain exists, we don't care about return
        resp.json.side_effect = ['{}']

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

        plan = provider.plan(wanted)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and deletes for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/zones/unit.tests/records',
                 data={
                     'content': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/zones/unit.tests/records/11189899'),
            call('DELETE', '/zones/unit.tests/records/11189897'),
            call('DELETE', '/zones/unit.tests/records/11189898')
        ],
                                                   any_order=True)
コード例 #23
0
class TestDynProviderGeo(TestCase):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # update of an existing td

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

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

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

        # convert a td to a regular record

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

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

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

        # convert a regular record to a td

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

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

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

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

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

        td = 42

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

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

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

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

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

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

        td_mock.all_response_pools = []

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

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

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

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

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

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

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

        change = Create(self.geo_record)
        provider._mod_rulesets(td_mock, change)
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
        ))
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first geo and it's fallback
            call(1),
            call('default', index=999),
            # 2nd geo and it's fallback
            call(2),
            call('default', index=999),
            # 3nd geo, from existing, and it's fallback
            call(3),
            call('default', index=999),
            # 4th geo and it's 2 levels of fallback
            call(4),
            call(3, index=999),
            call('default', index=999),
        ))
        # unused poll should have been deleted
        self.assertTrue(unused_pool.deleted)
        # old ruleset ruleset should be deleted, it's pool will have been
        # reused
        ruleset_mock.delete.assert_called_once()
コード例 #24
0
    def test_populate(self):
        provider = DnsimpleProvider('test', 'token', 42)

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

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

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

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

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

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

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

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

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

            zone = Zone('unit.tests.', [])
            provider.populate(zone)
            self.assertEquals(
                set([
                    Record.new(zone, '', {
                        'ttl': 3600,
                        'type': 'SSHFP',
                        'values': []
                    }),
                    Record.new(zone, '_srv._tcp', {
                        'ttl': 600,
                        'type': 'SRV',
                        'values': []
                    }),
                    Record.new(zone, 'naptr', {
                        'ttl': 600,
                        'type': 'NAPTR',
                        'values': []
                    }),
                ]), zone.records)
コード例 #25
0
class TestSelectelProvider(TestCase):
    API_URL = 'https://api.selectel.ru/domains/v1'

    api_record = []

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        provider = SelectelProvider(123, 'test_token')

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

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

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

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

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

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

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

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

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

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

        provider = SelectelProvider(123, 'test_token')

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

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

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

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

        provider = SelectelProvider(123, 'test_token')

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

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

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

        provider = SelectelProvider(123, 'test_token')

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

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

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

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

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

        include_change = provider._include_change(change)

        self.assertFalse(include_change)
コード例 #26
0
    def test_apply(self):
        provider = EasyDNSProvider('test', 'token', 'apikey')

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

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

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

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

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

        provider._client._request.reset_mock()

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

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

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

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('PUT',
                 '/zones/records/add/unit.tests/A',
                 data={
                     'rdata': '1.2.3.4',
                     'name': 'test1',
                     'ttl': 300,
                     'type': 'A',
                     'host': 'test1',
                 }),
            call('DELETE', '/zones/records/unit.tests/12342001'),
            call('DELETE', '/zones/records/unit.tests/12342002'),
            call('DELETE', '/zones/records/unit.tests/12342003')
        ],
                                                   any_order=True)
コード例 #27
0
class TestCloudflareProvider(TestCase):
    expected = Zone('unit.tests.', [])
    source = YamlProvider('test', join(dirname(__file__), 'config'))
    source.populate(expected)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # bust zone cache
        provider._zones = None

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

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

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

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

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

            self.assertEquals(0, len(changes))

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

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

        provider._request = Mock()

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

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

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

        provider._request.reset_mock()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        include_change = provider._include_change(change)

        self.assertFalse(include_change)

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

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

        self.assertFalse('proxied' in data)

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

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

        self.assertFalse(data['proxied'])

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

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

        self.assertTrue(data['proxied'])

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

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

        self.assertFalse(extra_changes)

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

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

        self.assertFalse(extra_changes)

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

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

        self.assertFalse(extra_changes)

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

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

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

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

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

        self.assertEquals(1, len(extra_changes))
        self.assertTrue(
            extra_changes[0].existing._octodns['cloudflare']['proxied'])
        self.assertFalse(
            extra_changes[0].new._octodns['cloudflare']['proxied'])
コード例 #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) - 8
        self.assertEquals(n, len(plan.changes))
        self.assertEquals(n, provider.apply(plan))
        self.assertFalse(plan.exists)

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

        provider._client._request.reset_mock()

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

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

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

        plan = provider.plan(wanted)
        self.assertTrue(plan.exists)
        self.assertEquals(2, len(plan.changes))
        self.assertEquals(2, provider.apply(plan))
        # recreate for update, and delete for the 2 parts of the other
        provider._client._request.assert_has_calls([
            call('POST',
                 '/domains/unit.tests/records',
                 data={
                     'data': '3.2.3.4',
                     'type': 'A',
                     'name': 'ttl',
                     'ttl': 300
                 }),
            call('DELETE', '/domains/unit.tests/records/11189899'),
            call('DELETE', '/domains/unit.tests/records/11189897'),
            call('DELETE', '/domains/unit.tests/records/11189898')
        ],
                                                   any_order=True)
コード例 #29
0
    def test_update_delete(self):
        # We need another run so that we can delete, we can't both add and
        # delete in one go b/c of swaps
        provider = CloudflareProvider('test', 'email', 'token')

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

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

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

        # Get zones, create zone, create a record, delete a record
        provider._request.assert_has_calls([
            call('GET', '/zones', params={'page': 1}),
            call('POST',
                 '/zones',
                 data={
                     'jump_start': False,
                     'name': 'unit.tests'
                 }),
            call('PUT', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997654',
                 data={
                     'content': 'ns2.foo.bar.',
                     'type': 'NS',
                     'name': 'unit.tests',
                     'ttl': 300
                 }),
            call('DELETE', '/zones/42/dns_records/'
                 'fc12ab34cd5611334422ab3322997653')
        ])
コード例 #30
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)