| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import json |
| import sys |
| import unittest |
| |
| from libcloud.utils.py3 import httplib |
| from libcloud.common.gandi_live import ( |
| GandiLiveBaseError, |
| JsonParseError, |
| InvalidRequestError, |
| ) |
| from libcloud.dns.types import ( |
| RecordType, |
| RecordError, |
| ZoneDoesNotExistError, |
| RecordDoesNotExistError, |
| ZoneAlreadyExistsError, |
| RecordAlreadyExistsError, |
| ) |
| from libcloud.dns.drivers.gandi_live import GandiLiveDNSDriver |
| from libcloud.dns.base import Zone, Record |
| from libcloud.test.file_fixtures import DNSFileFixtures |
| from libcloud.test.secrets import DNS_GANDI_LIVE |
| from libcloud.test.common.test_gandi_live import BaseGandiLiveMockHttp |
| |
| |
| class GandiLiveTests(unittest.TestCase): |
| def setUp(self): |
| GandiLiveDNSDriver.connectionCls.conn_class = GandiLiveMockHttp |
| GandiLiveMockHttp.type = None |
| self.driver = GandiLiveDNSDriver(*DNS_GANDI_LIVE) |
| self.test_zone = Zone( |
| id="example.com", |
| type="master", |
| ttl=None, |
| domain="example.com", |
| extra={"zone_uuid": "a53re"}, |
| driver=self, |
| ) |
| self.test_bad_zone = Zone( |
| id="badexample.com", |
| type="master", |
| ttl=None, |
| domain="badexample.com", |
| extra={"zone_uuid": "a53rf"}, |
| driver=self, |
| ) |
| self.test_record = Record( |
| id="A:bob", |
| type=RecordType.A, |
| name="bob", |
| zone=self.test_zone, |
| data="127.0.0.1", |
| driver=self, |
| extra={}, |
| ) |
| self.test_bad_record = Record( |
| id="A:jane", |
| type=RecordType.A, |
| name="jane", |
| zone=self.test_bad_zone, |
| data="127.0.0.1", |
| driver=self, |
| extra={}, |
| ) |
| |
| def test_list_zones(self): |
| zones = self.driver.list_zones() |
| self.assertEqual(len(zones), 2) |
| zone = zones[0] |
| self.assertEqual(zone.id, "example.com") |
| self.assertEqual(zone.type, "master") |
| self.assertEqual(zone.domain, "example.com") |
| self.assertIsNone(zone.ttl) |
| zone = zones[1] |
| self.assertEqual(zone.id, "example.net") |
| self.assertEqual(zone.type, "master") |
| self.assertEqual(zone.domain, "example.net") |
| self.assertIsNone(zone.ttl) |
| |
| def test_create_zone(self): |
| zone = self.driver.create_zone("example.org", extra={"name": "Example"}) |
| self.assertEqual(zone.id, "example.org") |
| self.assertEqual(zone.domain, "example.org") |
| self.assertEqual(zone.extra["zone_uuid"], "54321") |
| |
| def test_create_zone_without_name(self): |
| zone = self.driver.create_zone("example.org") |
| self.assertEqual(zone.id, "example.org") |
| self.assertEqual(zone.domain, "example.org") |
| self.assertEqual(zone.extra["zone_uuid"], "54321") |
| |
| def test_get_zone(self): |
| zone = self.driver.get_zone("example.com") |
| self.assertEqual(zone.id, "example.com") |
| self.assertEqual(zone.type, "master") |
| self.assertEqual(zone.domain, "example.com") |
| self.assertIsNone(zone.ttl) |
| |
| def test_list_records(self): |
| records = self.driver.list_records(self.test_zone) |
| self.assertEqual(len(records), 3) |
| record = records[0] |
| self.assertEqual(record.id, "A:@") |
| self.assertEqual(record.name, "@") |
| self.assertEqual(record.type, RecordType.A) |
| self.assertEqual(record.data, "127.0.0.1") |
| record = records[1] |
| self.assertEqual(record.id, "CNAME:www") |
| self.assertEqual(record.name, "www") |
| self.assertEqual(record.type, RecordType.CNAME) |
| self.assertEqual(record.data, "bob.example.com.") |
| record = records[2] |
| self.assertEqual(record.id, "A:bob") |
| self.assertEqual(record.name, "bob") |
| self.assertEqual(record.type, RecordType.A) |
| self.assertEqual(record.data, "127.0.1.1") |
| |
| def test_get_record(self): |
| record = self.driver.get_record(self.test_zone.id, "A:bob") |
| self.assertEqual(record.id, "A:bob") |
| self.assertEqual(record.name, "bob") |
| self.assertEqual(record.type, RecordType.A) |
| self.assertEqual(record.data, "127.0.1.1") |
| |
| def test_create_record(self): |
| record = self.driver.create_record( |
| "alice", self.test_zone, "AAAA", "::1", extra={"ttl": 400} |
| ) |
| self.assertEqual(record.id, "AAAA:alice") |
| self.assertEqual(record.name, "alice") |
| self.assertEqual(record.type, RecordType.AAAA) |
| self.assertEqual(record.data, "::1") |
| |
| def test_create_record_doesnt_throw_if_ttl_is_not_provided(self): |
| record = self.driver.create_record("alice", self.test_zone, "AAAA", "::1") |
| self.assertEqual(record.id, "AAAA:alice") |
| self.assertEqual(record.name, "alice") |
| self.assertEqual(record.type, RecordType.AAAA) |
| self.assertEqual(record.data, "::1") |
| |
| def test_bad_record_validation(self): |
| with self.assertRaises(RecordError) as ctx: |
| self.driver.create_record( |
| "alice", self.test_zone, "AAAA", "1" * 1025, extra={"ttl": 400} |
| ) |
| self.assertTrue("Record data must be" in str(ctx.exception)) |
| with self.assertRaises(RecordError) as ctx: |
| self.driver.create_record( |
| "alice", self.test_zone, "AAAA", "::1", extra={"ttl": 10} |
| ) |
| self.assertTrue("TTL must be at least" in str(ctx.exception)) |
| with self.assertRaises(RecordError) as ctx: |
| self.driver.create_record( |
| "alice", self.test_zone, "AAAA", "::1", extra={"ttl": 31 * 24 * 60 * 60} |
| ) |
| self.assertTrue("TTL must not exceed" in str(ctx.exception)) |
| |
| def test_update_record(self): |
| record = self.driver.update_record( |
| self.test_record, "bob", RecordType.A, "192.168.0.2", {"ttl": 500} |
| ) |
| self.assertEqual(record.id, "A:bob") |
| self.assertEqual(record.name, "bob") |
| self.assertEqual(record.type, RecordType.A) |
| self.assertEqual(record.data, "192.168.0.2") |
| |
| def test_delete_record(self): |
| success = self.driver.delete_record(self.test_record) |
| self.assertTrue(success) |
| |
| def test_export_bind(self): |
| bind_export = self.driver.export_zone_to_bind_format(self.test_zone) |
| bind_lines = bind_export.decode("utf8").split("\n") |
| self.assertEqual(bind_lines[0], "@ 10800 IN A 127.0.0.1") |
| |
| def test_bad_json_response(self): |
| with self.assertRaises(JsonParseError): |
| self.driver.get_zone("badexample.com") |
| |
| def test_no_record_found(self): |
| with self.assertRaises(RecordDoesNotExistError): |
| self.driver.get_record(self.test_zone.id, "A:none") |
| |
| def test_record_already_exists(self): |
| with self.assertRaises(RecordAlreadyExistsError): |
| self.driver.create_record( |
| "bob", self.test_bad_zone, "A", "127.0.0.1", extra={"ttl": 400} |
| ) |
| |
| def test_no_zone_found(self): |
| with self.assertRaises(ZoneDoesNotExistError): |
| self.driver.get_zone("nosuchzone.com") |
| |
| def test_zone_already_exists(self): |
| with self.assertRaises(ZoneAlreadyExistsError): |
| self.driver.create_zone("badexample.com") |
| |
| # This is somewhat spurious - the driver should be preventing any invalid |
| # request error from arising. |
| def test_suberrors(self): |
| with self.assertRaises(InvalidRequestError) as ctx: |
| self.driver.update_record( |
| self.test_bad_record, "jane", RecordType.A, "192.168.0.2", {"ttl": 500} |
| ) |
| self.assertTrue("is not a foo" in str(ctx.exception)) |
| |
| def test_other_error(self): |
| with self.assertRaises(GandiLiveBaseError): |
| self.driver.list_records(self.test_bad_zone) |
| |
| def test_mx_record(self): |
| record = self.driver.get_record(self.test_zone.id, "MX:lists") |
| self.assertEqual(record.extra["priority"], "10") |
| self.assertTrue("_other_records" in record.extra) |
| other_record = record.extra["_other_records"][0] |
| self.assertEqual(other_record["extra"]["priority"], "20") |
| |
| def test_ex_create_multivalue_record(self): |
| records = self.driver.ex_create_multi_value_record( |
| "alice", self.test_zone, "AAAA", ["::1", "::2"], extra={"ttl": 400} |
| ) |
| self.assertEqual(records[0].id, "AAAA:alice") |
| self.assertEqual(records[0].name, "alice") |
| self.assertEqual(records[0].type, RecordType.AAAA) |
| self.assertEqual(records[0].data, "::1") |
| self.assertEqual(records[1].id, "AAAA:alice") |
| self.assertEqual(records[1].name, "alice") |
| self.assertEqual(records[1].type, RecordType.AAAA) |
| self.assertEqual(records[1].data, "::2") |
| |
| def test_update_multivalue_record(self): |
| record = self.driver.get_record(self.test_zone.id, "MX:lists") |
| updated = self.driver.update_record( |
| record, None, None, "mail1", {"ttl": 400, "priority": 10} |
| ) |
| self.assertEqual(updated.extra["priority"], "10") |
| self.assertEqual(updated.data, "mail1") |
| self.assertTrue("_other_records" in record.extra) |
| other_record = record.extra["_other_records"][0] |
| self.assertEqual(other_record["extra"]["priority"], "20") |
| |
| def test_ex_update_gandi_zone_name(self): |
| updated = self.driver.ex_update_gandi_zone_name("111111", "Foo") |
| self.assertTrue(updated) |
| |
| def test_ex_delete_gandi_zone(self): |
| deleted = self.driver.ex_delete_gandi_zone("111111") |
| self.assertTrue(deleted) |
| |
| |
| class GandiLiveMockHttp(BaseGandiLiveMockHttp): |
| fixtures = DNSFileFixtures("gandi_live") |
| |
| def _json_api_v5_domains_get(self, method, url, body, headers): |
| body = self.fixtures.load("list_zones.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_com_get(self, method, url, body, headers): |
| body = self.fixtures.load("get_zone.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_badexample_com_get(self, method, url, body, headers): |
| body = self.fixtures.load("get_bad_zone.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_nosuchzone_com_get(self, method, url, body, headers): |
| body = self.fixtures.load("get_nonexistent_zone.json") |
| return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.NOT_FOUND]) |
| |
| def _json_api_v5_zones_post(self, method, url, body, headers): |
| input = json.loads(body) |
| if "badexample" in input["name"]: |
| body = self.fixtures.load("create_bad_zone.json") |
| return (httplib.CONFLICT, body, {}, httplib.responses[httplib.CONFLICT]) |
| else: |
| body = self.fixtures.load("create_zone.json") |
| return ( |
| httplib.OK, |
| body, |
| {"Location": "/zones/54321"}, |
| httplib.responses[httplib.OK], |
| ) |
| |
| def _json_api_v5_zones_111111_patch(self, method, url, body, headers): |
| body = self.fixtures.load("update_gandi_zone.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_zones_111111_delete(self, method, url, body, headers): |
| return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_org_patch(self, method, url, body, headers): |
| body = self.fixtures.load("create_domain.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_com_records_get(self, method, url, body, headers): |
| body = self.fixtures.load("list_records.json") |
| resp_headers = {} |
| if ( |
| headers is not None |
| and "Accept" in headers |
| and headers["Accept"] == "text/plain" |
| ): |
| body = self.fixtures.load("list_records_bind.txt") |
| resp_headers["Content-Type"] = "text/plain" |
| return (httplib.OK, body, resp_headers, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_com_records_bob_A_get( |
| self, method, url, body, headers |
| ): |
| body = self.fixtures.load("get_record.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_com_records_none_A_get( |
| self, method, url, body, headers |
| ): |
| body = self.fixtures.load("get_nonexistent_record.json") |
| return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.NOT_FOUND]) |
| |
| def _json_api_v5_domains_example_com_records_lists_MX_get( |
| self, method, url, body, headers |
| ): |
| body = self.fixtures.load("get_mx_record.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_com_records_lists_MX_put( |
| self, method, url, body, headers |
| ): |
| body = self.fixtures.load("update_mx_record.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_com_records_post(self, method, url, body, headers): |
| body = self.fixtures.load("create_record.json") |
| return ( |
| httplib.OK, |
| body, |
| {"Location": "/zones/12345/records/alice/AAAA"}, |
| httplib.responses[httplib.OK], |
| ) |
| |
| def _json_api_v5_domains_badexample_com_records_post( |
| self, method, url, body, headers |
| ): |
| body = self.fixtures.load("create_existing_record.json") |
| return (httplib.CONFLICT, body, {}, httplib.responses[httplib.CONFLICT]) |
| |
| def _json_api_v5_domains_badexample_com_records_get( |
| self, method, url, body, headers |
| ): |
| return ( |
| httplib.INTERNAL_SERVER_ERROR, |
| body, |
| {}, |
| httplib.responses[httplib.INTERNAL_SERVER_ERROR], |
| ) |
| |
| def _json_api_v5_domains_badexample_com_records_jane_A_put( |
| self, method, url, body, headers |
| ): |
| body = self.fixtures.load("update_bad_record.json") |
| return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.BAD_REQUEST]) |
| |
| def _json_api_v5_domains_example_com_records_bob_A_put( |
| self, method, url, body, headers |
| ): |
| body = self.fixtures.load("update_record.json") |
| return (httplib.OK, body, {}, httplib.responses[httplib.OK]) |
| |
| def _json_api_v5_domains_example_com_records_bob_A_delete( |
| self, method, url, body, headers |
| ): |
| return (httplib.NO_CONTENT, "", {}, httplib.responses[httplib.OK]) |
| |
| |
| if __name__ == "__main__": |
| sys.exit(unittest.main()) |