From 6d17b4671ab964d1dada7319e77f4de12438de02 Mon Sep 17 00:00:00 2001 From: Jonathan Leroy Date: Tue, 27 Oct 2020 11:23:22 +0100 Subject: [PATCH] Handle domains not registred at Gandi or not using Gandi's DNS --- octodns/provider/gandi.py | 32 ++++++++++++++++++++++++++++ tests/fixtures/gandi-zone.json | 7 ++++++ tests/test_octodns_provider_gandi.py | 32 +++++++++++++++++++--------- 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/gandi-zone.json diff --git a/octodns/provider/gandi.py b/octodns/provider/gandi.py index e11e9a3..1f89a80 100644 --- a/octodns/provider/gandi.py +++ b/octodns/provider/gandi.py @@ -41,6 +41,12 @@ class GandiClientNotFound(GandiClientException): super(GandiClientNotFound, self).__init__(r.text) +class GandiClientUnknownDomainName(GandiClientException): + + def __init__(self, msg): + super(GandiClientUnknownDomainName, self).__init__(msg) + + class GandiClient(object): def __init__(self, token): @@ -63,6 +69,16 @@ class GandiClient(object): r.raise_for_status() return r + def zone(self, zone_name): + return self._request('GET', '/livedns/domains/{}' + .format(zone_name)).json() + + def zone_create(self, zone_name): + return self._request('POST', '/livedns/domains', data={ + 'fqdn': zone_name, + 'zone': {} + }).json() + def zone_records(self, zone_name): records = self._request('GET', '/livedns/domains/{}/records' .format(zone_name)).json() @@ -318,9 +334,25 @@ class GandiProvider(BaseProvider): def _apply(self, plan): desired = plan.desired changes = plan.changes + zone = desired.name[:-1] self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, len(changes)) + try: + self._client.zone(zone) + except GandiClientNotFound: + self.log.info('_apply: no existing zone, trying to create it') + try: + self._client.zone_create(zone) + self.log.info('_apply: zone has been successfully created') + except GandiClientNotFound: + raise GandiClientUnknownDomainName('This domain is not ' + 'registred at Gandi. ' + 'Please register or ' + 'transfer it here ' + 'to be able to manage its ' + 'DNS zone.') + # Force records deletion to be done before creation in order to avoid # "CNAME record must be the only record" error when an existing CNAME # record is replaced by an A/AAAA record. diff --git a/tests/fixtures/gandi-zone.json b/tests/fixtures/gandi-zone.json new file mode 100644 index 0000000..e132f4c --- /dev/null +++ b/tests/fixtures/gandi-zone.json @@ -0,0 +1,7 @@ +{ + "domain_keys_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/keys", + "fqdn": "unit.tests", + "automatic_snapshots": true, + "domain_records_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records", + "domain_href": "https://api.gandi.net/v5/livedns/domains/unit.tests" +} \ No newline at end of file diff --git a/tests/test_octodns_provider_gandi.py b/tests/test_octodns_provider_gandi.py index 8152fcf..7448666 100644 --- a/tests/test_octodns_provider_gandi.py +++ b/tests/test_octodns_provider_gandi.py @@ -86,6 +86,18 @@ class TestGandiProvider(TestCase): provider.populate(zone) self.assertIn('"cause":"Forbidden"', text_type(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"', text_type(ctx.exception)) + # General error with requests_mock() as mock: mock.get(ANY, status_code=502, text='Things caught fire') @@ -95,15 +107,6 @@ class TestGandiProvider(TestCase): provider.populate(zone) self.assertEquals(502, ctx.exception.response.status_code) - # Non-existent zone doesn't populate anything - with requests_mock() as mock: - mock.get(ANY, status_code=404, - text='{"message": "Domain `foo.bar` not found"}') - - zone = Zone('unit.tests.', []) - provider.populate(zone) - self.assertEquals(set(), zone.records) - # No diffs == no changes with requests_mock() as mock: base = 'https://api.gandi.net/v5/livedns/domains/unit.tests' \ @@ -147,10 +150,14 @@ class TestGandiProvider(TestCase): 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) @@ -162,6 +169,11 @@ class TestGandiProvider(TestCase): 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, @@ -258,7 +270,7 @@ class TestGandiProvider(TestCase): }) ]) # expected number of total calls - self.assertEquals(14, provider._client._request.call_count) + self.assertEquals(16, provider._client._request.call_count) provider._client._request.reset_mock()