# # 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 import attributes from heat.engine.clients import troveclient from heat.engine import constraints from heat.engine import properties from heat.engine import resource from heat.engine.resources import nova_utils from heat.openstack.common.gettextutils import _ from heat.openstack.common import log as logging LOG = logging.getLogger(__name__) class OSDBInstance(resource.Resource): ''' OpenStack cloud database instance resource. ''' PROPERTIES = ( NAME, FLAVOR, SIZE, DATABASES, USERS, AVAILABILITY_ZONE, RESTORE_POINT, ) = ( 'name', 'flavor', 'size', 'databases', 'users', 'availability_zone', 'restore_point', ) _DATABASE_KEYS = ( DATABASE_CHARACTER_SET, DATABASE_COLLATE, DATABASE_NAME, ) = ( 'character_set', 'collate', 'name', ) _USER_KEYS = ( USER_NAME, USER_PASSWORD, USER_HOST, USER_DATABASES, ) = ( 'name', 'password', 'host', 'databases', ) ATTRIBUTES = ( HOSTNAME, HREF, ) = ( 'hostname', 'href', ) properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _('Name of the DB instance to create.'), required=True, constraints=[ constraints.Length(max=255), ] ), FLAVOR: properties.Schema( properties.Schema.STRING, _('Reference to a flavor for creating DB instance.'), required=True ), SIZE: properties.Schema( properties.Schema.INTEGER, _('Database volume size in GB.'), required=True, constraints=[ constraints.Range(1, 150), ] ), DATABASES: properties.Schema( properties.Schema.LIST, _('List of databases to be created on DB instance creation.'), default=[], schema=properties.Schema( properties.Schema.MAP, schema={ DATABASE_CHARACTER_SET: properties.Schema( properties.Schema.STRING, _('Set of symbols and encodings.'), default='utf8' ), DATABASE_COLLATE: properties.Schema( properties.Schema.STRING, _('Set of rules for comparing characters in a ' 'character set.'), default='utf8_general_ci' ), DATABASE_NAME: properties.Schema( properties.Schema.STRING, _('Specifies database names for creating ' 'databases on instance creation.'), required=True, constraints=[ constraints.Length(max=64), constraints.AllowedPattern(r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ] ), }, ) ), USERS: properties.Schema( properties.Schema.LIST, _('List of users to be created on DB instance creation.'), default=[], schema=properties.Schema( properties.Schema.MAP, schema={ USER_NAME: properties.Schema( properties.Schema.STRING, _('User name to create a user on instance ' 'creation.'), required=True, constraints=[ constraints.Length(max=16), constraints.AllowedPattern(r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ] ), USER_PASSWORD: properties.Schema( properties.Schema.STRING, _('Password for those users on instance ' 'creation.'), required=True, constraints=[ constraints.AllowedPattern(r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ] ), USER_HOST: properties.Schema( properties.Schema.STRING, _('The host from which a user is allowed to ' 'connect to the database.'), default='%' ), USER_DATABASES: properties.Schema( properties.Schema.LIST, _('Names of databases that those users can ' 'access on instance creation.'), schema=properties.Schema( properties.Schema.STRING, ), required=True ), }, ) ), AVAILABILITY_ZONE: properties.Schema( properties.Schema.STRING, _('Name of the availability zone for DB instance.') ), RESTORE_POINT: properties.Schema( properties.Schema.STRING, _('DB instance restore point.') ), } attributes_schema = { HOSTNAME: attributes.Schema( _("Hostname of the instance") ), HREF: attributes.Schema( _("Api endpoint reference of the instance") ), } def __init__(self, name, json_snippet, stack): super(OSDBInstance, self).__init__(name, json_snippet, stack) self._href = None self._dbinstance = None @property def dbinstance(self): """Get the trove dbinstance.""" if not self._dbinstance and self.resource_id: self._dbinstance = self.trove().instances.get(self.resource_id) return self._dbinstance def physical_resource_name(self): name = self.properties.get(self.NAME) if name: return name return super(OSDBInstance, self).physical_resource_name() def handle_create(self): ''' Create cloud database instance. ''' self.dbinstancename = self.physical_resource_name() self.flavor = nova_utils.get_flavor_id(self.trove(), self.properties[self.FLAVOR]) self.volume = {'size': self.properties[self.SIZE]} self.databases = self.properties.get(self.DATABASES) self.users = self.properties.get(self.USERS) restore_point = self.properties.get(self.RESTORE_POINT) zone = self.properties.get(self.AVAILABILITY_ZONE) # convert user databases to format required for troveclient. # that is, list of database dictionaries for user in self.users: dbs = [{'name': db} for db in user.get(self.USER_DATABASES, [])] user[self.USER_DATABASES] = dbs # create db instance instance = self.trove().instances.create( self.dbinstancename, self.flavor, volume=self.volume, databases=self.databases, users=self.users, restorePoint=restore_point, availability_zone=zone) self.resource_id_set(instance.id) return instance def _refresh_instance(self, instance): try: instance.get() except troveclient.exceptions.RequestEntityTooLarge as exc: msg = _("Stack %(name)s (%(id)s) received an OverLimit " "response during instance.get(): %(exception)s") LOG.warning(msg % {'name': self.stack.name, 'id': self.stack.id, 'exception': exc}) def check_create_complete(self, instance): ''' Check if cloud DB instance creation is complete. ''' self._refresh_instance(instance) if instance.status == 'ERROR': raise exception.Error(_("Database instance creation failed.")) if instance.status != 'ACTIVE': return False msg = _("Database instance %(database)s created (flavor:%(flavor)s, " "volume:%(volume)s)") LOG.info(msg % ({'database': self.dbinstancename, 'flavor': self.flavor, 'volume': self.volume})) return True def handle_delete(self): ''' Delete a cloud database instance. ''' if not self.resource_id: return instance = None try: instance = self.trove().instances.get(self.resource_id) except troveclient.exceptions.NotFound: LOG.debug("Database instance %s not found." % self.resource_id) self.resource_id_set(None) else: instance.delete() return instance def check_delete_complete(self, instance): ''' Check for completion of cloud DB instance delettion ''' if not instance: return True try: self._refresh_instance(instance) except troveclient.exceptions.NotFound: self.resource_id_set(None) return True return False def validate(self): ''' Validate any of the provided params ''' res = super(OSDBInstance, self).validate() if res: return res # check validity of user and databases users = self.properties.get(self.USERS) if not users: return databases = self.properties.get(self.DATABASES) if not databases: msg = _('Databases property is required if users property' ' is provided') raise exception.StackValidationFailed(message=msg) db_names = set([db[self.DATABASE_NAME] for db in databases]) for user in users: if not user.get(self.USER_DATABASES, []): msg = _('Must provide access to at least one database for ' 'user %s') % user[self.USER_NAME] raise exception.StackValidationFailed(message=msg) missing_db = [db_name for db_name in user[self.USER_DATABASES] if db_name not in db_names] if missing_db: msg = _('Database %s specified for user does not exist in ' 'databases.') % missing_db raise exception.StackValidationFailed(message=msg) def href(self): if not self._href and self.dbinstance: if not self.dbinstance.links: self._href = None else: for link in self.dbinstance.links: if link['rel'] == 'self': self._href = link[self.HREF] break return self._href def _resolve_attribute(self, name): if name == self.HOSTNAME: return self.dbinstance.hostname elif name == self.HREF: return self.href() def resource_mapping(): return { 'OS::Trove::Instance': OSDBInstance, }