From: Vijendar Komalla Date: Mon, 1 Jul 2013 17:48:47 +0000 (-0500) Subject: Rackspace cloud database resource provider X-Git-Tag: 2014.1~398^2 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=c882bbe1e6bcca3a70ae5c147c011b77caa9abde;p=openstack-build%2Fheat-build.git Rackspace cloud database resource provider Change-Id: I0bdacb844d5e5999e70918352e58856c1b21692c --- diff --git a/heat/engine/resources/rackspace/clouddatabase.py b/heat/engine/resources/rackspace/clouddatabase.py new file mode 100644 index 00000000..2c7e42bd --- /dev/null +++ b/heat/engine/resources/rackspace/clouddatabase.py @@ -0,0 +1,233 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# +# 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. + +from heat.common import exception +from heat.engine.resources.rackspace import rackspace_resource +from heat.openstack.common import log as logging + +logger = logging.getLogger(__name__) + + +class CloudDBInstance(rackspace_resource.RackspaceResource): + ''' + Rackspace cloud database resource. + ''' + database_schema = { + "Character_set": { + "Type": "String", + "Default": "utf8", + "Required": False + }, + "Collate": { + "Type": "String", + "Default": "utf8_general_ci", + "Required": False + }, + "Name": { + "Type": "String", + "Required": True, + "MaxLength": "64", + "AllowedPattern": "[a-zA-Z0-9_]+[a-zA-Z0-9_@?#\s]*[a-zA-Z0-9_]+" + } + } + + user_schema = { + "Name": { + "Type": "String", + "Required": True, + "MaxLength": "16", + "AllowedPattern": "[a-zA-Z0-9_]+[a-zA-Z0-9_@?#\s]*[a-zA-Z0-9_]+" + }, + "Password": { + "Type": "String", + "Required": True, + "AllowedPattern": "[a-zA-Z0-9_]+[a-zA-Z0-9_@?#\s]*[a-zA-Z0-9_]+" + }, + "Host": { + "Type": "String", + "Default": "%" + }, + "Databases": { + "Type": "List", + "Required": True + } + } + + properties_schema = { + "InstanceName": { + "Type": "String", + "Required": True, + "MaxLength": "255" + }, + + "FlavorRef": { + "Type": "String", + "Required": True + }, + + "VolumeSize": { + "Type": "Number", + "MinValue": 1, + "MaxValue": 150, + "Required": True + }, + + "Databases": { + 'Type': 'List', + 'Required': False, + 'Schema': { + 'Type': 'Map', + 'Schema': database_schema + } + }, + + "Users": { + 'Type': 'List', + 'Required': False, + 'Schema': { + 'Type': 'Map', + 'Schema': user_schema + } + }, + } + + attributes_schema = { + "hostname": "Hostname of the instance", + "href": "Api endpoint reference of the instance" + } + + def __init__(self, name, json_snippet, stack): + super(CloudDBInstance, self).__init__(name, json_snippet, stack) + self.hostname = None + self.href = None + + def handle_create(self): + ''' + Create Rackspace Cloud DB Instance. + ''' + logger.debug("Cloud DB instance handle_create called") + self.sqlinstancename = self.properties['InstanceName'] + self.flavor = self.properties['FlavorRef'] + self.volume = self.properties['VolumeSize'] + self.databases = self.properties.get('Databases', None) + self.users = self.properties.get('Users', None) + + # create db instance + logger.info("Creating Cloud DB instance %s" % self.sqlinstancename) + instance = self.cloud_db().create(self.sqlinstancename, + flavor=self.flavor, + volume=self.volume) + if instance is not None: + self.resource_id_set(instance.id) + + self.hostname = instance.hostname + self.href = instance.links[0]['href'] + return instance + + def check_create_complete(self, instance): + ''' + Check if cloud DB instance creation is complete. + ''' + instance.get() # get updated attributes + if instance.status == 'ERROR': + instance.delete() + raise exception.Error("Cloud DB instance creation failed.") + + if instance.status != 'ACTIVE': + return False + + logger.info("Cloud DB instance %s created (flavor:%s, volume:%s)" % + (self.sqlinstancename, self.flavor, self.volume)) + # create databases + for database in self.databases: + instance.create_database( + database['Name'], + character_set=database['Character_set'], + collate=database['Collate']) + logger.info("Database %s created on cloud DB instance %s" % + (database['Name'], self.sqlinstancename)) + + # add users + dbs = [] + for user in self.users: + if user['Databases']: + dbs = user['Databases'] + instance.create_user(user['Name'], user['Password'], dbs) + logger.info("Cloud database user %s created successfully" % + (user['Name'])) + return True + + def handle_delete(self): + ''' + Delete a Rackspace Cloud DB Instance. + ''' + logger.debug("CloudDBInstance handle_delete called.") + sqlinstancename = self.properties['InstanceName'] + if self.resource_id is None: + logger.debug("resource_id is null and returning without delete.") + raise exception.ResourceNotFound(resource_name=sqlinstancename, + stack_name=self.stack.name) + instances = self.cloud_db().delete(self.resource_id) + self.resource_id = None + + def validate(self): + ''' + Validate any of the provided params + ''' + res = super(CloudDBInstance, self).validate() + if res: + return res + + # check validity of user and databases + users = self.properties.get('Users', None) + if not users: + return + + databases = self.properties.get('Databases', None) + if not databases: + return {'Error': + 'Databases property is required if Users property' + ' is provided'} + + for user in users: + if not user['Databases']: + return {'Error': + 'Must provide access to at least one database for ' + 'user %s' % user['Name']} + + missing_db = [db_name for db_name in user['Databases'] + if db_name not in [db['Name'] for db in databases]] + if missing_db: + return {'Error': + 'Database %s specified for user does not exist in ' + 'databases.' % missing_db} + return + + def _resolve_attribute(self, name): + if name == 'hostname': + return self.hostname + elif name == 'href': + return self.href + + +# pyrax module is required to work with Rackspace cloud database provider. +# If it is not installed, don't register clouddatabase provider +def resource_mapping(): + if rackspace_resource.PYRAX_INSTALLED: + return { + 'Rackspace::Cloud::DBInstance': CloudDBInstance, + } + else: + return {} diff --git a/heat/tests/test_clouddatabase.py b/heat/tests/test_clouddatabase.py new file mode 100644 index 00000000..510d244c --- /dev/null +++ b/heat/tests/test_clouddatabase.py @@ -0,0 +1,175 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + + +from heat.common import exception +from heat.common import template_format +from heat.engine import parser +from heat.engine import environment +from heat.engine import resource +from heat.engine.resources.rackspace import clouddatabase +from heat.openstack.common import uuidutils +from heat.tests.common import HeatTestCase +from heat.tests.utils import setup_dummy_db + + +wp_template = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "MYSQL instance running on Rackspace cloud", + "Parameters" : { + "FlavorRef": { + "Description" : "Flavor reference", + "Type": "String" + }, + "VolumeSize": { + "Description" : "The volume size", + "Type": "Number", + "MinValue" : "1", + "MaxValue" : "1024" + }, + "InstanceName": { + "Description" : "The database instance name", + "Type": "String" + } + }, + "Resources" : { + "MySqlCloudDB": { + "Type": "Rackspace::Cloud::DBInstance", + "Properties" : { + "InstanceName" : {"testsqlinstance"}, + "FlavorRef" : {"test-flavor"}, + "VolumeSize" : {"test-volume-size"}, + "Users" : [{"name":"testuser", "password":"testpass123"}] , + "Databases" : [{"name":"testdbonetwo"}] + } + } + } + +} +''' + + +class FakeDBInstance(object): + def __init__(self): + self.id = 12345 + self.hostname = "testhost" + self.links = \ + [{"href": "https://adga23dd432a.rackspacecloud.com/132345245"}] + self.resource_id = 12345 + + +class CloudDBInstanceTest(HeatTestCase): + def setUp(self): + super(CloudDBInstanceTest, self).setUp() + 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::DBInstance", + clouddatabase.CloudDBInstance) + + def _setup_test_clouddbinstance(self, name, inject_property_error=False): + stack_name = '%s_stack' % name + t = template_format.parse(wp_template) + template = parser.Template(t) + stack = parser.Stack(None, + stack_name, + template, + environment.Environment({'InstanceName': 'test'}), + stack_id=uuidutils.generate_uuid()) + + t['Resources']['MySqlCloudDB']['Properties']['InstanceName'] = 'Test' + t['Resources']['MySqlCloudDB']['Properties']['FlavorRef'] = '1GB' + t['Resources']['MySqlCloudDB']['Properties']['VolumeSize'] = '30' + + if inject_property_error: + # database name given in users list is not a valid database + t['Resources']['MySqlCloudDB']['Properties']['Databases'] = \ + [{"Name": "onedb"}] + t['Resources']['MySqlCloudDB']['Properties']['Users'] = \ + [{"Name": "testuser", + "Password": "pass", + "Databases": ["invaliddb"]}] + else: + t['Resources']['MySqlCloudDB']['Properties']['Databases'] = \ + [{"Name": "validdb"}] + t['Resources']['MySqlCloudDB']['Properties']['Users'] = \ + [{"Name": "testuser", + "Password": "pass", + "Databases": ["validdb"]}] + + instance = clouddatabase.CloudDBInstance( + '%s_name' % name, + t['Resources']['MySqlCloudDB'], + stack) + instance.resource_id = 1234 + self.m.StubOutWithMock(instance, 'cloud_db') + return instance + + def test_clouddbinstance(self): + instance = self._setup_test_clouddbinstance('dbinstance') + self.assertEqual(instance.hostname, None) + self.assertEqual(instance.href, None) + + def test_clouddbinstance_create(self): + instance = self._setup_test_clouddbinstance('dbinstance_create') + fake_client = self.m.CreateMockAnything() + instance.cloud_db().AndReturn(fake_client) + fakedbinstance = FakeDBInstance() + fake_client.create('Test', + flavor='1GB', + volume='30').AndReturn(fakedbinstance) + self.m.ReplayAll() + instance.handle_create() + expected_hostname = fakedbinstance.hostname + expected_href = fakedbinstance.links[0]['href'] + self.assertEqual(instance._resolve_attribute('hostname'), + expected_hostname) + self.assertEqual(instance._resolve_attribute('href'), expected_href) + self.m.VerifyAll() + + def test_clouddbinstance_delete_resource_notfound(self): + instance = self._setup_test_clouddbinstance('dbinstance_delete') + instance.resource_id = None + self.m.ReplayAll() + self.assertRaises(exception.ResourceNotFound, instance.handle_delete) + self.m.VerifyAll() + + def test_clouddbinstance_delete(self): + instance = self._setup_test_clouddbinstance('dbinstance_delete') + fake_client = self.m.CreateMockAnything() + cloud_db = instance.cloud_db().AndReturn(fake_client) + fakedbinstance = FakeDBInstance() + fake_client.delete(1234).AndReturn(None) + self.m.ReplayAll() + instance.handle_delete() + self.m.VerifyAll() + + def test_clouddbinstance_param_validation_success(self): + instance = self._setup_test_clouddbinstance( + 'dbinstance_params', + inject_property_error=False) + self.m.ReplayAll() + ret = instance.validate() + self.assertEqual(ret, None) + self.m.VerifyAll() + + def test_clouddbinstance_param_validation_fail(self): + instance = self._setup_test_clouddbinstance('dbinstance_params', + inject_property_error=True) + self.m.ReplayAll() + ret = instance.validate() + self.assertTrue('Error' in ret) + self.m.VerifyAll()