Adding designate dns support to trove

Implements: blueprint designate-dnsaas-support

Change-Id: I69a8455595b519a6459f70efa8eef8a4013bd9f6
This commit is contained in:
Steve Leon 2013-10-29 13:05:51 -07:00
parent d488e37a69
commit 4d7be83905
9 changed files with 453 additions and 4 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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.

View File

@ -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)

View File

@ -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):

View File

@ -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.

View File

@ -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)