Add Rackspace Cloud DNS Resource

This will be a temporary solution to have a resource for DNS until
Designate is incubated. Also currently work is in progress for a
new API for Designate.
Until then this will serve as a resource for Rackspace Cloud DNS.

Implements: blueprint rax-dns-resource
Change-Id: Ief0c478b9e40f6f9446b386243400f062a941a36
This commit is contained in:
Vinod Mangalpally 2013-10-31 11:36:34 -05:00
parent bca4b68aef
commit a1301baaa4
2 changed files with 437 additions and 0 deletions

View File

@ -0,0 +1,182 @@
# 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.
try:
from pyrax.exceptions import NotFound
except ImportError:
#Setup fake exception for testing without pyrax
class NotFound(Exception):
pass
from heat.common import exception
from heat.openstack.common import log as logging
from . import rackspace_resource
logger = logging.getLogger(__name__)
class CloudDns(rackspace_resource.RackspaceResource):
record_schema = {
'name': {
'Type': 'String',
'Required': True,
'Description': _('Specifies the name for the domain or subdomain. '
'Must be a valid domain name.'),
'MinLength': 3
},
'type': {
'Type': 'String',
'Required': True,
'Description': _('Specifies the record type.'),
'AllowedValues': [
'A',
'AAAA',
'NS',
'MX',
'CNAME',
'TXT',
'SRV'
]
},
'data': {
'Type': 'String',
'Description': _('Type specific record data'),
'Required': True
},
'ttl': {
'Type': 'Integer',
'Description': _('How long other servers should cache record'
'data.'),
'MinValue': 301,
'Default': 3600
},
'priority': {
'Type': 'Integer',
'Description': _('Required for MX and SRV records, but forbidden '
'for other record types. If specified, must be '
'an integer from 0 to 65535.'),
'MinValue': 0,
'MaxValue': 65535
},
'comment': {
'Type': 'String',
'Description': _('Optional free form text comment'),
'MaxLength': 160
}
}
properties_schema = {
'name': {
'Type': 'String',
'Description': _('Specifies the name for the domain or subdomain. '
'Must be a valid domain name.'),
'Required': True,
'MinLength': 3
},
'emailAddress': {
'Type': 'String',
'UpdateAllowed': True,
'Description': _('Email address to use for contacting the domain '
'administrator.'),
'Required': True
},
'ttl': {
'Type': 'Integer',
'UpdateAllowed': True,
'Description': _('How long other servers should cache record'
'data.'),
'MinValue': 301,
'Default': 3600
},
'comment': {
'Type': 'String',
'UpdateAllowed': True,
'Description': _('Optional free form text comment'),
'MaxLength': 160
},
'records': {
'Type': 'List',
'UpdateAllowed': True,
'Description': _('Domain records'),
'Schema': {
'Type': 'Map',
'Schema': record_schema
}
}
}
update_allowed_keys = ('Properties',)
def handle_create(self):
"""
Create a Rackspace CloudDns Instance.
"""
# There is no check_create_complete as the pyrax create for DNS is
# synchronous.
logger.debug("CloudDns handle_create called.")
args = dict((k, v) for k, v in self.properties.items())
for rec in args['records'] or {}:
# only pop the priority for the correct types
if (rec['type'] != 'MX') and (rec['type'] != 'SRV'):
rec.pop('priority', None)
dom = self.cloud_dns().create(**args)
self.resource_id_set(dom.id)
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
"""
Update a Rackspace CloudDns Instance.
"""
logger.debug("CloudDns handle_update called.")
if not self.resource_id:
raise exception.Error('Update called on a non-existent domain')
if prop_diff:
dom = self.cloud_dns().get(self.resource_id)
# handle records separately
records = prop_diff.pop('records', {})
# Handle top level domain properties
dom.update(**prop_diff)
# handle records
if records:
recs = dom.list_records()
# 1. delete all the current records other than rackspace NS records
[rec.delete() for rec in recs if rec.type != 'NS' or
'stabletransit.com' not in rec.data]
# 2. update with the new records in prop_diff
dom.add_records(records)
def handle_delete(self):
"""
Delete a Rackspace CloudDns Instance.
"""
logger.debug("CloudDns handle_delete called.")
if self.resource_id:
try:
dom = self.cloud_dns().get(self.resource_id)
dom.delete()
except NotFound:
pass
self.resource_id_set(None)
# pyrax module is required to work with Rackspace cloud server provider.
# If it is not installed, don't register cloud server provider
def resource_mapping():
if rackspace_resource.PYRAX_INSTALLED:
return {'Rackspace::Cloud::DNS': CloudDns}
else:
return {}

View File

@ -0,0 +1,255 @@
# 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 copy
from heat.engine import environment
from heat.common import template_format
from heat.engine import parser
from heat.engine import resource
from heat.engine import scheduler
from heat.openstack.common import uuidutils
from heat.tests import common
from heat.tests import utils
from heat.openstack.common import log as logging
from ..engine.plugins import cloud_dns
logger = logging.getLogger(__name__)
domain_only_template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Dns instance running on Rackspace cloud",
"Parameters" : {
"UnittestDomain" : {
"Description" : "Domain for unit tests",
"Type" : "String",
"Default" : 'dnsheatunittest.com'
},
"dnsttl" : {
"Description" : "TTL for the domain",
"Type" : "Number",
"MinValue" : '301',
"Default" : '301'
},
"name": {
"Description" : "The cloud dns instance name",
"Type": "String",
"Default": "CloudDNS"
}
},
"Resources" : {
"domain" : {
"Type": "Rackspace::Cloud::DNS",
"Properties" : {
"name" : "dnsheatunittest.com",
"emailAddress" : "admin@dnsheatunittest.com",
"ttl" : 3600,
"comment" : "Testing Cloud DNS integration with Heat"
}
}
}
}
'''
class FakeDnsInstance(object):
def __init__(self):
self.id = 4
self.resource_id = 4
def get(self):
pass
def delete(self):
pass
class RackspaceDnsTest(common.HeatTestCase):
def setUp(self):
super(RackspaceDnsTest, self).setUp()
utils.setup_dummy_db()
# Test environment may not have pyrax client library installed and if
# pyrax is not installed resource class would not be registered.
# So register resource provider class explicitly for unit testing.
resource._register_class("Rackspace::Cloud::DNS", cloud_dns.CloudDns)
self.create_domain_only_args = {
"name": 'dnsheatunittest.com',
"emailAddress": 'admin@dnsheatunittest.com',
"ttl": 3600,
"comment": 'Testing Cloud DNS integration with Heat',
"records": None
}
self.update_domain_only_args = {
"emailAddress": 'updatedEmail@example.com',
"ttl": 5555,
"comment": 'updated comment'
}
def _setup_test_cloud_dns_instance(self, name, parsed_t):
stack_name = '%s_stack' % name
t = parsed_t
template = parser.Template(t)
stack = parser.Stack(None,
stack_name,
template,
environment.Environment({'name': 'test'}),
stack_id=uuidutils.generate_uuid())
instance = cloud_dns.CloudDns(
'%s_name' % name,
t['Resources']['domain'],
stack)
instance.t = instance.stack.resolve_runtime_data(instance.t)
return instance
def _stubout_create(self, instance, fake_dnsinstance, **create_args):
mock_client = self.m.CreateMockAnything()
self.m.StubOutWithMock(instance, 'cloud_dns')
instance.cloud_dns().AndReturn(mock_client)
self.m.StubOutWithMock(mock_client, "create")
mock_client.create(**create_args).AndReturn(fake_dnsinstance)
self.m.ReplayAll()
def _stubout_update(
self,
instance,
fake_dnsinstance,
updateRecords=None,
**update_args):
mock_client = self.m.CreateMockAnything()
self.m.StubOutWithMock(instance, 'cloud_dns')
instance.cloud_dns().AndReturn(mock_client)
self.m.StubOutWithMock(mock_client, "get")
mock_domain = self.m.CreateMockAnything()
mock_client.get(fake_dnsinstance.resource_id).AndReturn(mock_domain)
self.m.StubOutWithMock(mock_domain, "update")
mock_domain.update(**update_args).AndReturn(fake_dnsinstance)
if updateRecords:
fake_records = list()
mock_domain.list_records().AndReturn(fake_records)
mock_domain.add_records([{
'type': 'A',
'name': 'ftp.example.com',
'data': '192.0.2.8',
'ttl': 3600}])
self.m.ReplayAll()
def _get_create_args_with_comments(self, record):
record_with_comment = [dict(record[0])]
record_with_comment[0]["comment"] = None
create_record_args = dict()
create_record_args['records'] = record_with_comment
create_args = dict(
self.create_domain_only_args.items() + create_record_args.items())
return create_args
def test_create_domain_only(self):
"""
Test domain create only without any records.
"""
fake_dns_instance = FakeDnsInstance()
t = template_format.parse(domain_only_template)
instance = self._setup_test_cloud_dns_instance('dnsinstance_create', t)
create_args = self.create_domain_only_args
self._stubout_create(instance, fake_dns_instance, **create_args)
scheduler.TaskRunner(instance.create)()
self.assertEqual((instance.CREATE, instance.COMPLETE), instance.state)
self.m.VerifyAll()
def test_create_domain_with_a_record(self):
"""
Test domain create with an A record. This should not have a
priority field.
"""
fake_dns_instance = FakeDnsInstance()
t = template_format.parse(domain_only_template)
a_record = [{
"type": "A",
"name": "ftp.example.com",
"data": "192.0.2.8",
"ttl": 3600
}]
t['Resources']['domain']['Properties']['records'] = a_record
instance = self._setup_test_cloud_dns_instance('dnsinstance_create', t)
create_args = self._get_create_args_with_comments(a_record)
self._stubout_create(instance, fake_dns_instance, **create_args)
scheduler.TaskRunner(instance.create)()
self.assertEqual((instance.CREATE, instance.COMPLETE), instance.state)
self.m.VerifyAll()
def test_create_domain_with_mx_record(self):
"""
Test domain create with an MX record. This should have a
priority field.
"""
fake_dns_instance = FakeDnsInstance()
t = template_format.parse(domain_only_template)
mx_record = [{
"type": "MX",
"name": "example.com",
"data": "mail.example.com",
"priority": 5,
"ttl": 3600
}]
t['Resources']['domain']['Properties']['records'] = mx_record
instance = self._setup_test_cloud_dns_instance('dnsinstance_create', t)
create_args = self._get_create_args_with_comments(mx_record)
self._stubout_create(instance, fake_dns_instance, **create_args)
scheduler.TaskRunner(instance.create)()
self.assertEqual((instance.CREATE, instance.COMPLETE), instance.state)
self.m.VerifyAll()
def test_update(self, updateRecords=None):
"""
Helper function for testing domain updates.
"""
fake_dns_instance = FakeDnsInstance()
t = template_format.parse(domain_only_template)
instance = self._setup_test_cloud_dns_instance('dnsinstance_update', t)
instance.resource_id = 4
update_args = self.update_domain_only_args
self._stubout_update(
instance,
fake_dns_instance,
updateRecords,
**update_args)
ut = copy.deepcopy(instance.parsed_template())
ut['Properties']['emailAddress'] = 'updatedEmail@example.com'
ut['Properties']['ttl'] = 5555
ut['Properties']['comment'] = 'updated comment'
if updateRecords:
ut['Properties']['records'] = updateRecords
scheduler.TaskRunner(instance.update, ut)()
self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state)
self.m.VerifyAll()
def test_update_domain_only(self):
"""
Test domain update without any records.
"""
self.test_update()
def test_update_domain_with_a_record(self):
"""
Test domain update with an A record.
"""
a_record = [{'type': 'A',
'name': 'ftp.example.com',
'data': '192.0.2.8',
'ttl': 3600}]
self.test_update(updateRecords=a_record)