From 62a23857909f2023a228aab7e6fe37c2a1c298e2 Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Wed, 12 Jun 2019 08:50:53 +1000 Subject: [PATCH] Add a designate V2 API dns driver Change-Id: Iafb36333a37146787c57eded139b4c2e071d69b0 --- ...ignate-v2-dns-driver-8d1be56ab2c71b83.yaml | 5 ++ trove/common/cfg.py | 4 ++ trove/dns/designate/driver.py | 71 +++++++++++++++---- .../test_designate_driver.py | 52 ++++++++++++++ 4 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/add-designate-v2-dns-driver-8d1be56ab2c71b83.yaml diff --git a/releasenotes/notes/add-designate-v2-dns-driver-8d1be56ab2c71b83.yaml b/releasenotes/notes/add-designate-v2-dns-driver-8d1be56ab2c71b83.yaml new file mode 100644 index 0000000000..c2240ca5f6 --- /dev/null +++ b/releasenotes/notes/add-designate-v2-dns-driver-8d1be56ab2c71b83.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added support for designate v2 api with a new dns driver. To use this driver + set dns_driver = trove.dns.designate.driver.DesignateDriverV2 diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 34de8fd396..22dde22e95 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -144,6 +144,10 @@ common_opts = [ help='Region name for DNSaaS.'), cfg.URIOpt('dns_auth_url', default="http://0.0.0.0", help='Authentication URL for DNSaaS.'), + cfg.StrOpt('dns_user_domain_id', default="default", + help='Keystone user domain ID used for auth'), + cfg.StrOpt('dns_project_domain_id', default="default", + help='Keystone project domain ID used for auth'), cfg.StrOpt('dns_domain_name', default="", help='Domain name used for adding DNS entries.'), cfg.StrOpt('dns_username', default="", secret=True, diff --git a/trove/dns/designate/driver.py b/trove/dns/designate/driver.py index c02e1cd568..13e441dd3b 100644 --- a/trove/dns/designate/driver.py +++ b/trove/dns/designate/driver.py @@ -20,8 +20,10 @@ Dns Driver that uses Designate DNSaaS. import base64 import hashlib -from designateclient.v1 import Client +from designateclient import client from designateclient.v1.records import Record +from keystoneauth1 import loading +from keystoneauth1 import session from oslo_log import log as logging from oslo_utils import encodeutils import six @@ -44,14 +46,16 @@ DNS_PASSKEY = CONF.dns_passkey DNS_TTL = CONF.dns_ttl DNS_DOMAIN_ID = CONF.dns_domain_id DNS_DOMAIN_NAME = CONF.dns_domain_name - +DNS_USER_DOMAIN_ID = CONF.dns_user_domain_id +DNS_PROJECT_DOMAIN_ID = CONF.dns_project_domain_id LOG = logging.getLogger(__name__) class DesignateObjectConverter(object): - def domain_to_zone(self, domain): + @staticmethod + def domain_to_zone(domain): return DesignateDnsZone(id=domain.id, name=domain.name) def record_to_entry(self, record, dns_zone): @@ -60,22 +64,23 @@ class DesignateObjectConverter(object): priority=record.priority, dns_zone=dns_zone) -def create_designate_client(): +def create_designate_client(api_version='2'): """Creates a Designate DNSaaS client.""" - client = Client(auth_url=DNS_AUTH_URL, - username=DNS_USERNAME, - password=DNS_PASSKEY, - tenant_id=DNS_TENANT_ID, - endpoint=DNS_ENDPOINT_URL, - service_type=DNS_SERVICE_TYPE, - region_name=DNS_REGION) - return client + loader = loading.get_plugin_loader('password') + auth = loader.load_from_options(auth_url=DNS_AUTH_URL, + username=DNS_USERNAME, + password=DNS_PASSKEY, + project_id=DNS_TENANT_ID, + user_domain_id=DNS_USER_DOMAIN_ID, + project_domain_id=DNS_PROJECT_DOMAIN_ID) + sesh = session.Session(auth=auth) + return client.Client(api_version, session=sesh) class DesignateDriver(driver.DnsDriver): def __init__(self): - self.dns_client = create_designate_client() + self.dns_client = create_designate_client(api_version='1') self.converter = DesignateObjectConverter() self.default_dns_zone = DesignateDnsZone(id=DNS_DOMAIN_ID, name=DNS_DOMAIN_NAME) @@ -140,6 +145,46 @@ class DesignateDriver(driver.DnsDriver): return self.dns_client.records.list(dns_zone.id) +class DesignateDriverV2(driver.DnsDriver): + + def __init__(self): + self.dns_client = create_designate_client() + self.default_dns_zone = DesignateDnsZone(id=DNS_DOMAIN_ID, + name=DNS_DOMAIN_NAME) + + def create_entry(self, entry, content): + """Creates the entry in the driver at the given dns zone.""" + dns_zone = entry.dns_zone or self.default_dns_zone + if not dns_zone.id: + raise TypeError(_("The entry's dns_zone must have an ID " + "specified.")) + name = entry.name + LOG.debug("Creating DNS entry %s.", name) + client = self.dns_client + # Record name has to end with a '.' by dns standard + client.recordsets.create(DNS_DOMAIN_ID, entry.name + '.', entry.type, + records=[content]) + + def delete_entry(self, name, type, dns_zone=None): + """Deletes an entry with the given name and type from a dns zone.""" + dns_zone = dns_zone or self.default_dns_zone + records = self._get_records(dns_zone) + matching_record = [rec for rec in records + if rec['name'] == name + '.' + and rec['type'] == type] + if not matching_record: + raise exception.DnsRecordNotFound(name) + LOG.debug("Deleting DNS entry %s.", name) + self.dns_client.recordsets.delete(dns_zone.id, + matching_record[0]['id']) + + def _get_records(self, dns_zone): + dns_zone = dns_zone or self.default_dns_zone + if not dns_zone: + raise TypeError(_('DNS domain is must be specified')) + return self.dns_client.recordsets.list(dns_zone.id) + + class DesignateInstanceEntryFactory(driver.DnsInstanceEntryFactory): """Defines how instance DNS entries are created for instances.""" diff --git a/trove/tests/unittests/domain-name-service/test_designate_driver.py b/trove/tests/unittests/domain-name-service/test_designate_driver.py index bd0df0664a..3ab617514d 100644 --- a/trove/tests/unittests/domain-name-service/test_designate_driver.py +++ b/trove/tests/unittests/domain-name-service/test_designate_driver.py @@ -20,7 +20,9 @@ from mock import MagicMock from mock import patch import six +from trove.common import exception from trove.dns.designate import driver +from trove.dns import driver as base_driver from trove.tests.unittests import trove_testtools @@ -173,6 +175,56 @@ class DesignateDriverTest(trove_testtools.TestCase): self.assertEqual(expected.id, actual.id) +class DesignateDriverV2Test(trove_testtools.TestCase): + + def setUp(self): + super(DesignateDriverV2Test, self).setUp() + self.records = [dict(name='record1.', type='A', data='10.0.0.1', + ttl=3600, priority=1, + id='11111111-1111-1111-1111-111111111111'), + dict(name='record2.', type='CNAME', data='10.0.0.2', + ttl=1800, priority=2, + id='22222222-2222-2222-2222-222222222222'), + dict(name='record3.', type='A', data='10.0.0.3', + ttl=3600, priority=1, + id='3333333-3333-3333-3333-333333333333')] + self.mock_client = MagicMock() + self.create_des_client_patch = patch.object( + driver, 'create_designate_client', MagicMock( + return_value=self.mock_client)) + self.create_des_client_mock = self.create_des_client_patch.start() + self.addCleanup(self.create_des_client_patch.stop) + + def test_create_entry(self): + dns_driver = driver.DesignateDriverV2() + zone = driver.DesignateDnsZone( + id='22222222-2222-2222-2222-222222222222', name='www.trove.com') + entry = base_driver.DnsEntry(name='www.example.com', content='None', + type='A', ttl=3600, priority=None, + dns_zone=zone) + + dns_driver.create_entry(entry, '1.2.3.4') + self.mock_client.recordsets.create.assert_called_once_with( + driver.DNS_DOMAIN_ID, entry.name + '.', entry.type, + records=['1.2.3.4']) + + def test_delete_entry(self): + with patch.object(driver.DesignateDriverV2, '_get_records', + MagicMock(return_value=self.records)): + dns_driver = driver.DesignateDriverV2() + dns_driver.delete_entry('record1', 'A') + self.mock_client.recordsets.delete(driver.DNS_DOMAIN_ID) + + def test_delete_no_entry(self): + with patch.object(driver.DesignateDriverV2, '_get_records', + MagicMock(return_value=self.records)): + dns_driver = driver.DesignateDriverV2() + self.assertRaises(exception.DnsRecordNotFound, + dns_driver.delete_entry, + 'nothere', 'A') + self.mock_client.recordsets.assert_not_called() + + class DesignateInstanceEntryFactoryTest(trove_testtools.TestCase): def setUp(self):