diff --git a/etc/trove/trove-taskmanager.conf.sample b/etc/trove/trove-taskmanager.conf.sample index c58daeece9..92445767ae 100644 --- a/etc/trove/trove-taskmanager.conf.sample +++ b/etc/trove/trove-taskmanager.conf.sample @@ -61,6 +61,17 @@ notification_service_id = mysql:2f3ff068-2bfb-4f70-9a9d-a6bb65bc084b # Trove DNS trove_dns_support = False +dns_account_id = 123456 +dns_auth_url = http://127.0.0.1:5000/v2.0 +dns_username = user +dns_passkey = password +dns_ttl = 3600 +dns_domain_name = 'trove.com.' +dns_domain_id = 11111111-1111-1111-1111-111111111111 +dns_driver = trove.dns.designate.driver.DesignateDriver +dns_instance_entry_factory = trove.dns.designate.driver.DesignateInstanceEntryFactory +dns_endpoint_url = http://127.0.0.1/v1/ +dns_service_type = dns # Trove Security Groups for Instances trove_security_groups_support = True diff --git a/etc/trove/trove.conf.sample b/etc/trove/trove.conf.sample index 62a2f8b291..33e942e252 100644 --- a/etc/trove/trove.conf.sample +++ b/etc/trove/trove.conf.sample @@ -69,6 +69,17 @@ http_delete_rate = 200 # Trove DNS trove_dns_support = False +dns_account_id = 123456 +dns_auth_url = http://127.0.0.1:5000/v2.0 +dns_username = user +dns_passkey = password +dns_ttl = 3600 +dns_domain_name = 'trove.com.' +dns_domain_id = 11111111-1111-1111-1111-111111111111 +dns_driver = trove.dns.designate.driver.DesignateDriver +dns_instance_entry_factory = trove.dns.designate.driver.DesignateInstanceEntryFactory +dns_endpoint_url = http://127.0.0.1/v1/ +dns_service_type = dns # Taskmanager queue name taskmanager_queue = taskmanager diff --git a/requirements.txt b/requirements.txt index e1367f1a69..8fdf695760 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ python-novaclient>=2.15.0 python-cinderclient>=1.0.5 python-keystoneclient>=0.3.2 python-swiftclient>=1.5 +python-designateclient>=1.0.0 iso8601>=0.1.4 jsonschema>=1.3.0,!=1.4.0 Jinja2 diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 04d0f92ca0..b4bb5fd88c 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -68,14 +68,17 @@ common_opts = [ cfg.StrOpt('dns_instance_entry_factory', default='trove.dns.driver.DnsInstanceEntryFactory'), cfg.StrOpt('dns_hostname', default=""), - cfg.IntOpt('dns_account_id', default=0), + cfg.StrOpt('dns_account_id', default=""), + cfg.StrOpt('dns_endpoint_url', default="0.0.0.0"), + cfg.StrOpt('dns_service_type', default=""), + cfg.StrOpt('dns_region', default=""), cfg.StrOpt('dns_auth_url', default=""), cfg.StrOpt('dns_domain_name', default=""), cfg.StrOpt('dns_username', default="", secret=True), cfg.StrOpt('dns_passkey', default="", secret=True), cfg.StrOpt('dns_management_base_url', default=""), cfg.IntOpt('dns_ttl', default=300), - cfg.IntOpt('dns_domain_id', default=1), + cfg.StrOpt('dns_domain_id', default=""), cfg.IntOpt('users_page_size', default=20), cfg.IntOpt('databases_page_size', default=20), cfg.IntOpt('instances_page_size', default=20), diff --git a/trove/dns/designate/__init__.py b/trove/dns/designate/__init__.py new file mode 100644 index 0000000000..89ba40d634 --- /dev/null +++ b/trove/dns/designate/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2013 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed 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. diff --git a/trove/dns/designate/driver.py b/trove/dns/designate/driver.py new file mode 100644 index 0000000000..43b3ebfbbf --- /dev/null +++ b/trove/dns/designate/driver.py @@ -0,0 +1,181 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed 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. + +""" +Dns Driver that uses Designate DNSaaS. +""" + +from trove.common import cfg +from trove.common import exception +from trove.dns import driver +from trove.openstack.common import log as logging +from designateclient.v1 import Client +from designateclient.v1.records import Record +import base64 +import hashlib + + +CONF = cfg.CONF + +DNS_TENANT_ID = CONF.dns_account_id +DNS_AUTH_URL = CONF.dns_auth_url +DNS_ENDPOINT_URL = CONF.dns_endpoint_url +DNS_SERVICE_TYPE = CONF.dns_service_type +DNS_REGION = CONF.dns_region +DNS_USERNAME = CONF.dns_username +DNS_PASSKEY = CONF.dns_passkey +DNS_TTL = CONF.dns_ttl +DNS_DOMAIN_ID = CONF.dns_domain_id +DNS_DOMAIN_NAME = CONF.dns_domain_name + + +LOG = logging.getLogger(__name__) + + +class DesignateObjectConverter(object): + + def domain_to_zone(self, domain): + return DesignateDnsZone(id=domain.id, name=domain.name) + + def record_to_entry(self, record, dns_zone): + return driver.DnsEntry(name=record.name, content=record.data, + type=record.type, ttl=record.ttl, + priority=record.priority, dns_zone=dns_zone) + + +def create_designate_client(): + """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 + + +class DesignateDriver(driver.DnsDriver): + + def __init__(self): + self.dns_client = create_designate_client() + self.converter = DesignateObjectConverter() + self.default_dns_zone = DesignateDnsZone(id=DNS_DOMAIN_ID, + name=DNS_DOMAIN_NAME) + + def create_entry(self, entry): + """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 + record = Record(name=entry.name + '.', + type=entry.type, + data=entry.content, + ttl=entry.ttl, + priority=entry.priority) + client.records.create(dns_zone.id, record) + + 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.records.delete(dns_zone.id, matching_record[0].id) + + def get_entries_by_content(self, content, dns_zone=None): + """Retrieves all entries in a dns_zone with a matching content field""" + records = self._get_records(dns_zone) + return [self.converter.record_to_entry(record, dns_zone) + for record in records if record.data == content] + + def get_entries_by_name(self, name, dns_zone): + records = self._get_records(dns_zone) + return [self.converter.record_to_entry(record, dns_zone) + for record in records if record.name == name] + + def get_dns_zones(self, name=None): + """Returns all dns zones (optionally filtered by the name argument.""" + domains = self.dns_client.domains.list() + return [self.converter.domain_to_zone(domain) + for domain in domains if not name or domain.name == name] + + def modify_content(self, name, content, dns_zone): + # We dont need this in trove for now + raise NotImplementedError("Not implemented for Designate DNS.") + + def rename_entry(self, content, name, dns_zone): + # We dont need this in trove for now + raise NotImplementedError("Not implemented for Designate DNS.") + + 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.records.list(dns_zone.id) + + +class DesignateInstanceEntryFactory(driver.DnsInstanceEntryFactory): + """Defines how instance DNS entries are created for instances.""" + + def create_entry(self, instance): + zone = DesignateDnsZone(id=DNS_DOMAIN_ID, name=DNS_DOMAIN_NAME) + # Constructing the hostname by hashing the instance ID. + name = base64.urlsafe_b64encode(hashlib.md5(instance).digest())[:11] + hostname = ("%s.%s" % (name, zone.name)) + #Removing the leading dot if present + if hostname.endswith('.'): + hostname = hostname[:-1] + + return driver.DnsEntry(name=hostname, content=None, type="A", + ttl=DNS_TTL, dns_zone=zone) + + +class DesignateDnsZone(driver.DnsZone): + + def __init__(self, id, name): + self._name = name + self._id = id + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def id(self): + return self._id + + @id.setter + def id(self, value): + self._id = value + + def __eq__(self, other): + return (isinstance(other, DesignateDnsZone) and + self.name == other.name and + self.id == other.id) + + def __str__(self): + return "%s:%s" % (self.id, self.name) diff --git a/trove/dns/manager.py b/trove/dns/manager.py index a2fd4d9813..6e6a791803 100644 --- a/trove/dns/manager.py +++ b/trove/dns/manager.py @@ -35,12 +35,12 @@ class DnsManager(object): *args, **kwargs): if not dns_driver: dns_driver = CONF.dns_driver - dns_driver = utils.import_object(dns_driver) + dns_driver = utils.import_class(dns_driver) self.driver = dns_driver() if not dns_instance_entry_factory: dns_instance_entry_factory = CONF.dns_instance_entry_factory - entry_factory = utils.import_object(dns_instance_entry_factory) + entry_factory = utils.import_class(dns_instance_entry_factory) self.entry_factory = entry_factory() def create_instance_entry(self, instance_id, content): diff --git a/trove/tests/unittests/dns/__init__.py b/trove/tests/unittests/dns/__init__.py new file mode 100644 index 0000000000..f0ec14305d --- /dev/null +++ b/trove/tests/unittests/dns/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed 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. diff --git a/trove/tests/unittests/dns/test_designate_driver.py b/trove/tests/unittests/dns/test_designate_driver.py new file mode 100644 index 0000000000..d94b0fdd72 --- /dev/null +++ b/trove/tests/unittests/dns/test_designate_driver.py @@ -0,0 +1,214 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed 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 testtools +from designateclient.v1.domains import Domain +from designateclient.v1.records import Record +from trove.dns.designate import driver +from mockito import any +from mockito import mock +from mockito import when +import base64 +import hashlib + + +class DesignateObjectConverterTest(testtools.TestCase): + + def setUp(self): + super(DesignateObjectConverterTest, self).setUp() + + def tearDown(self): + super(DesignateObjectConverterTest, self).tearDown() + + def test_convert_domain_to_zone(self): + name = 'www.example.com' + id = '39413651-3b9e-41f1-a4df-e47d5e9f67be' + email = 'john.smith@openstack.com' + domain = Domain(name=name, id=id, email=email) + converter = driver.DesignateObjectConverter() + converted_domain = converter.domain_to_zone(domain) + self.assertEqual(name, converted_domain.name) + self.assertEqual(id, converted_domain.id) + + def test_convert_record_to_entry(self): + name = 'test.example.com' + id = '4f3439ef-fc8b-4098-a1aa-a66ed01102b9' + domain_id = '39413651-3b9e-41f1-a4df-e47d5e9f67be' + domain_name = 'example.com' + type = 'CNAME' + data = '127.0.0.1' + ttl = 3600 + priority = 1 + zone = driver.DesignateDnsZone(domain_id, domain_name) + record = Record(name=name, id=id, domain_id=domain_id, type=type, + data=data, priority=priority, ttl=ttl) + converter = driver.DesignateObjectConverter() + converted_record = converter.record_to_entry(record, zone) + self.assertEqual(name, converted_record.name) + self.assertEqual(data, converted_record.content) + self.assertEqual(type, converted_record.type) + self.assertEqual(priority, converted_record.priority) + self.assertEqual(ttl, converted_record.ttl) + self.assertEqual(zone, converted_record.dns_zone) + + +class DesignateDriverTest(testtools.TestCase): + + def setUp(self): + super(DesignateDriverTest, self).setUp() + self.domains = [Domain(name='www.example.com', + id='11111111-1111-1111-1111-111111111111', + email='test@example.com'), + Domain(name='www.trove.com', + id='22222222-2222-2222-2222-222222222222', + email='test@trove.com'), + Domain(name='www.openstack.com', + id='33333333-3333-3333-3333-333333333333', + email='test@openstack.com')] + self.records = [Record(name='record1', type='A', data='10.0.0.1', + ttl=3600, priority=1), + Record(name='record2', type='CNAME', data='10.0.0.2', + ttl=1800, priority=2), + Record(name='record3', type='A', data='10.0.0.3', + ttl=3600, priority=1)] + + def tearDown(self): + super(DesignateDriverTest, self).tearDown() + + def test_get_entries_by_name(self): + zone = driver.DesignateDnsZone('123', 'www.example.com') + when(driver).create_designate_client().thenReturn(None) + when(driver.DesignateDriver)._get_records(any()).thenReturn( + self.records) + dns_driver = driver.DesignateDriver() + entries = dns_driver.get_entries_by_name('record2', zone) + self.assertTrue(len(entries) == 1, 'More than one record found') + entry = entries[0] + self.assertEqual('record2', entry.name) + self.assertEqual('CNAME', entry.type) + self.assertEqual('10.0.0.2', entry.content) + self.assertEqual(1800, entry.ttl) + self.assertEqual(2, entry.priority) + zone = entry.dns_zone + self.assertEqual('123', zone.id) + self.assertEqual('www.example.com', zone.name) + + def test_get_entries_by_name_not_found(self): + zone = driver.DesignateDnsZone('123', 'www.example.com') + when(driver).create_designate_client().thenReturn(None) + when(driver.DesignateDriver)._get_records(any()).thenReturn( + self.records) + dns_driver = driver.DesignateDriver() + entries = dns_driver.get_entries_by_name('record_not_found', zone) + self.assertTrue(len(entries) == 0, 'Some records were returned') + + def test_get_entries_by_content(self): + zone = driver.DesignateDnsZone('123', 'www.example.com') + when(driver).create_designate_client().thenReturn(None) + when(driver.DesignateDriver)._get_records(any()).thenReturn( + self.records) + dns_driver = driver.DesignateDriver() + entries = dns_driver.get_entries_by_content('10.0.0.1', zone) + self.assertTrue(len(entries) == 1, 'More than one record found') + entry = entries[0] + self.assertEqual('record1', entry.name) + self.assertEqual('A', entry.type) + self.assertEqual('10.0.0.1', entry.content) + self.assertEqual(3600, entry.ttl) + self.assertEqual(1, entry.priority) + zone = entry.dns_zone + self.assertEqual('123', zone.id) + self.assertEqual('www.example.com', zone.name) + + def test_get_entries_by_content_not_found(self): + zone = driver.DesignateDnsZone('123', 'www.example.com') + when(driver).create_designate_client().thenReturn(None) + when(driver.DesignateDriver)._get_records(any()).thenReturn( + self.records) + dns_driver = driver.DesignateDriver() + entries = dns_driver.get_entries_by_content('127.0.0.1', zone) + self.assertTrue(len(entries) == 0, 'Some records were returned') + + def test_get_dnz_zones(self): + + client = mock() + client.domains = mock() + when(driver).create_designate_client().thenReturn(client) + when(client.domains).list().thenReturn(self.domains) + dns_driver = driver.DesignateDriver() + zones = dns_driver.get_dns_zones() + self.assertTrue(len(zones) == 3) + for x in range(0, 3): + self.assertDomainsAreEqual(self.domains[x], zones[x]) + + def test_get_dnz_zones_by_name(self): + client = mock() + client.domains = mock() + when(driver).create_designate_client().thenReturn(client) + when(client.domains).list().thenReturn(self.domains) + dns_driver = driver.DesignateDriver() + zones = dns_driver.get_dns_zones('www.trove.com') + self.assertTrue(len(zones) == 1) + self.assertDomainsAreEqual(self.domains[1], zones[0]) + + def test_get_dnz_zones_not_found(self): + client = mock() + client.domains = mock() + when(driver).create_designate_client().thenReturn(client) + when(client.domains).list().thenReturn(self.domains) + dns_driver = driver.DesignateDriver() + zones = dns_driver.get_dns_zones('www.notfound.com') + self.assertTrue(len(zones) == 0) + + def assertDomainsAreEqual(self, expected, actual): + self.assertEqual(expected.name, actual.name) + self.assertEqual(expected.id, actual.id) + + +class DesignateInstanceEntryFactoryTest(testtools.TestCase): + + def setUp(self): + super(DesignateInstanceEntryFactoryTest, self).setUp() + + def tearDown(self): + super(DesignateInstanceEntryFactoryTest, self).tearDown() + + def test_create_entry(self): + instance_id = '11111111-2222-3333-4444-555555555555' + driver.DNS_DOMAIN_ID = '00000000-0000-0000-0000-000000000000' + driver.DNS_DOMAIN_NAME = 'trove.com' + driver.DNS_TTL = 3600 + hashed_id = base64.urlsafe_b64encode(hashlib.md5(instance_id).digest()) + hashed_id_concat = hashed_id[:11] + exp_hostname = ("%s.%s" % (hashed_id_concat, driver.DNS_DOMAIN_NAME)) + factory = driver.DesignateInstanceEntryFactory() + entry = factory.create_entry(instance_id) + self.assertEqual(exp_hostname, entry.name) + self.assertEqual('A', entry.type) + self.assertEqual(3600, entry.ttl) + zone = entry.dns_zone + self.assertEqual(driver.DNS_DOMAIN_NAME, zone.name) + self.assertEqual(driver.DNS_DOMAIN_ID, zone.id) + + def test_create_entry_ends_with_dot(self): + instance_id = '11111111-2222-3333-4444-555555555555' + driver.DNS_DOMAIN_ID = '00000000-0000-0000-0000-000000000000' + driver.DNS_DOMAIN_NAME = 'trove.com.' + driver.DNS_TTL = 3600 + hashed_id = base64.urlsafe_b64encode(hashlib.md5(instance_id).digest()) + hashed_id_concat = hashed_id[:11] + exp_hostname = ("%s.%s" % + (hashed_id_concat, driver.DNS_DOMAIN_NAME))[:-1] + factory = driver.DesignateInstanceEntryFactory() + entry = factory.create_entry(instance_id) + self.assertEqual(exp_hostname, entry.name)