# 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. import base64 from datetime import datetime from eventlet.support import greenlets as greenlet from heat.engine import event from heat.common import exception from heat.db import api as db_api from heat.common import identifier from heat.engine import timestamp from heat.engine.properties import Properties from heat.openstack.common import log as logging logger = logging.getLogger(__name__) _resource_classes = {} def get_types(): '''Return an iterator over the list of valid resource types''' return iter(_resource_classes) def get_class(resource_type): '''Return the Resource class for a given resource type''' return _resource_classes.get(resource_type) def _register_class(resource_type, resource_class): logger.info(_('Registering resource type %s') % resource_type) if resource_type in _resource_classes: logger.warning(_('Replacing existing resource type %s') % resource_type) _resource_classes[resource_type] = resource_class class Metadata(object): ''' A descriptor for accessing the metadata of a resource while ensuring the most up-to-date data is always obtained from the database. ''' def __get__(self, resource, resource_class): '''Return the metadata for the owning resource.''' if resource is None: return None if resource.id is None: return resource.parsed_template('Metadata') rs = db_api.resource_get(resource.stack.context, resource.id) rs.refresh(attrs=['rsrc_metadata']) return rs.rsrc_metadata def __set__(self, resource, metadata): '''Update the metadata for the owning resource.''' if resource.id is None: raise exception.ResourceNotAvailable(resource_name=resource.name) rs = db_api.resource_get(resource.stack.context, resource.id) rs.update_and_save({'rsrc_metadata': metadata}) class Resource(object): # Status strings CREATE_IN_PROGRESS = 'IN_PROGRESS' CREATE_FAILED = 'CREATE_FAILED' CREATE_COMPLETE = 'CREATE_COMPLETE' DELETE_IN_PROGRESS = 'DELETE_IN_PROGRESS' DELETE_FAILED = 'DELETE_FAILED' DELETE_COMPLETE = 'DELETE_COMPLETE' UPDATE_IN_PROGRESS = 'UPDATE_IN_PROGRESS' UPDATE_FAILED = 'UPDATE_FAILED' UPDATE_COMPLETE = 'UPDATE_COMPLETE' # Status values, returned from subclasses to indicate update method UPDATE_REPLACE = 'UPDATE_REPLACE' UPDATE_INTERRUPTION = 'UPDATE_INTERRUPTION' UPDATE_NO_INTERRUPTION = 'UPDATE_NO_INTERRUPTION' UPDATE_NOT_IMPLEMENTED = 'UPDATE_NOT_IMPLEMENTED' # If True, this resource must be created before it can be referenced. strict_dependency = True created_time = timestamp.Timestamp(db_api.resource_get, 'created_at') updated_time = timestamp.Timestamp(db_api.resource_get, 'updated_at') metadata = Metadata() # Resource implementation set this to the subset of template keys # which are supported for handle_update, used by update_template_diff update_allowed_keys = () def __new__(cls, name, json, stack): '''Create a new Resource of the appropriate class for its type.''' if cls != Resource: # Call is already for a subclass, so pass it through return super(Resource, cls).__new__(cls) # Select the correct subclass to instantiate ResourceClass = get_class(json['Type']) or GenericResource return ResourceClass(name, json, stack) def __init__(self, name, json_snippet, stack): if '/' in name: raise ValueError(_('Resource name may not contain "/"')) self.stack = stack self.context = stack.context self.name = name self.t = stack.resolve_static_data(json_snippet) self.properties = Properties(self.properties_schema, self.t.get('Properties', {}), self.stack.resolve_runtime_data, self.name) resource = db_api.resource_get_by_name_and_stack(self.context, name, stack.id) if resource: self.resource_id = resource.nova_instance self.state = resource.state self.state_description = resource.state_description self.id = resource.id else: self.resource_id = None self.state = None self.state_description = '' self.id = None def __eq__(self, other): '''Allow == comparison of two resources''' # For the purposes of comparison, we declare two resource objects # equal if their names and parsed_templates are the same if isinstance(other, Resource): return (self.name == other.name) and ( self.parsed_template() == other.parsed_template()) return NotImplemented def __ne__(self, other): '''Allow != comparison of two resources''' result = self.__eq__(other) if result is NotImplemented: return result return not result def type(self): return self.t['Type'] def identifier(self): '''Return an identifier for this resource''' return identifier.ResourceIdentifier(resource_name=self.name, **self.stack.identifier()) def parsed_template(self, section=None, default={}): ''' Return the parsed template data for the resource. May be limited to only one section of the data, in which case a default value may also be supplied. ''' if section is None: template = self.t else: template = self.t.get(section, default) return self.stack.resolve_runtime_data(template) def update_template_diff(self, json_snippet=None): ''' Returns the difference between json_template and self.t If something has been removed in json_snippet which exists in self.t we set it to None. If any keys have changed which are not in update_allowed_keys, raises NotImplementedError ''' update_allowed_set = set(self.update_allowed_keys) # Create a set containing the keys in both current and update template current_snippet = self.parsed_template() template_keys = set(current_snippet.keys()) template_keys.update(set(json_snippet.keys())) # Create a set of keys which differ (or are missing/added) changed_keys_set = set([k for k in template_keys if current_snippet.get(k) != json_snippet.get(k)]) if not changed_keys_set.issubset(update_allowed_set): badkeys = changed_keys_set - update_allowed_set raise NotImplementedError("Cannot update keys %s for %s" % (badkeys, self.name)) return dict((k, json_snippet.get(k)) for k in changed_keys_set) def __str__(self): return '%s "%s"' % (self.__class__.__name__, self.name) def _add_dependencies(self, deps, fragment): if isinstance(fragment, dict): for key, value in fragment.items(): if key in ('DependsOn', 'Ref'): target = self.stack.resources[value] if key == 'DependsOn' or target.strict_dependency: deps += (self, target) elif key != 'Fn::GetAtt': self._add_dependencies(deps, value) elif isinstance(fragment, list): for item in fragment: self._add_dependencies(deps, item) def add_dependencies(self, deps): self._add_dependencies(deps, self.t) deps += (self, None) def keystone(self): return self.stack.clients.keystone() def nova(self, service_type='compute'): return self.stack.clients.nova(service_type) def swift(self): return self.stack.clients.swift() def quantum(self): return self.stack.clients.quantum() def create(self): ''' Create the resource. Subclasses should provide a handle_create() method to customise creation. ''' if self.state in (self.CREATE_IN_PROGRESS, self.CREATE_COMPLETE): return 'Resource creation already requested' logger.info('creating %s' % str(self)) try: err = self.properties.validate() if err: return err self.state_set(self.CREATE_IN_PROGRESS) if callable(getattr(self, 'handle_create', None)): self.handle_create() except Exception as ex: # If we get a GreenletExit exception, the create thread has # been killed so we should raise allowing this thread to exit if type(ex) is greenlet.GreenletExit: logger.warning('GreenletExit during create, exiting') raise else: logger.exception('create %s', str(self)) self.state_set(self.CREATE_FAILED, str(ex)) return str(ex) else: self.state_set(self.CREATE_COMPLETE) def update(self, json_snippet=None): ''' update the resource. Subclasses should provide a handle_update() method to customise update, the base-class handle_update will fail by default. ''' if self.state in (self.CREATE_IN_PROGRESS, self.UPDATE_IN_PROGRESS): return 'Resource update already requested' if not json_snippet: return 'Must specify json snippet for resource update!' logger.info('updating %s' % str(self)) result = self.UPDATE_NOT_IMPLEMENTED try: self.state_set(self.UPDATE_IN_PROGRESS) properties = Properties(self.properties_schema, json_snippet.get('Properties', {}), self.stack.resolve_runtime_data, self.name) err = properties.validate() if err: raise ValueError(err) if callable(getattr(self, 'handle_update', None)): result = self.handle_update(json_snippet) except Exception as ex: logger.exception('update %s : %s' % (str(self), str(ex))) self.state_set(self.UPDATE_FAILED, str(ex)) return str(ex) else: # If resource was updated (with or without interruption), # then we set the resource to UPDATE_COMPLETE if not result == self.UPDATE_REPLACE: self.t = self.stack.resolve_static_data(json_snippet) self.state_set(self.UPDATE_COMPLETE) return result def physical_resource_name(self): return '%s.%s' % (self.stack.name, self.name) def physical_resource_name_find(self, resource_name): if resource_name in self.stack: return '%s.%s' % (self.stack.name, resource_name) else: raise IndexError('no such resource') def validate(self): logger.info('Validating %s' % str(self)) return self.properties.validate() def delete(self): ''' Delete the resource. Subclasses should provide a handle_delete() method to customise deletion. ''' if self.state == self.DELETE_COMPLETE: return if self.state == self.DELETE_IN_PROGRESS: return 'Resource deletion already in progress' logger.info('deleting %s (inst:%s db_id:%s)' % (str(self), self.resource_id, str(self.id))) self.state_set(self.DELETE_IN_PROGRESS) try: if callable(getattr(self, 'handle_delete', None)): self.handle_delete() except Exception as ex: logger.exception('Delete %s', str(self)) self.state_set(self.DELETE_FAILED, str(ex)) return str(ex) self.state_set(self.DELETE_COMPLETE) def destroy(self): ''' Delete the resource and remove it from the database. ''' result = self.delete() if result: return result if self.id is None: return try: db_api.resource_get(self.context, self.id).delete() except exception.NotFound: # Don't fail on delete if the db entry has # not been created yet. pass except Exception as ex: logger.exception('Delete %s from DB' % str(self)) return str(ex) self.id = None def resource_id_set(self, inst): self.resource_id = inst if self.id is not None: try: rs = db_api.resource_get(self.context, self.id) rs.update_and_save({'nova_instance': self.resource_id}) except Exception as ex: logger.warn('db error %s' % str(ex)) def _store(self): '''Create the resource in the database''' try: rs = {'state': self.state, 'stack_id': self.stack.id, 'nova_instance': self.resource_id, 'name': self.name, 'rsrc_metadata': self.metadata, 'stack_name': self.stack.name} new_rs = db_api.resource_create(self.context, rs) self.id = new_rs.id self.stack.updated_time = datetime.utcnow() except Exception as ex: logger.error('DB error %s' % str(ex)) def _add_event(self, new_state, reason): '''Add a state change event to the database''' ev = event.Event(self.context, self.stack, self, new_state, reason, self.resource_id, self.properties) try: ev.store() except Exception as ex: logger.error('DB error %s' % str(ex)) def _store_or_update(self, new_state, reason): self.state = new_state self.state_description = reason if self.id is not None: try: rs = db_api.resource_get(self.context, self.id) rs.update_and_save({'state': self.state, 'state_description': reason, 'nova_instance': self.resource_id}) self.stack.updated_time = datetime.utcnow() except Exception as ex: logger.error('DB error %s' % str(ex)) # store resource in DB on transition to CREATE_IN_PROGRESS # all other transistions (other than to DELETE_COMPLETE) # should be handled by the update_and_save above.. elif new_state == self.CREATE_IN_PROGRESS: self._store() def state_set(self, new_state, reason="state changed"): old_state = self.state self._store_or_update(new_state, reason) if new_state != old_state: self._add_event(new_state, reason) def FnGetRefId(self): ''' http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\ intrinsic-function-reference-ref.html ''' if self.resource_id is not None: return unicode(self.resource_id) else: return unicode(self.name) def FnGetAtt(self, key): ''' http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\ intrinsic-function-reference-getatt.html ''' return unicode(self.name) def FnBase64(self, data): ''' http://docs.amazonwebservices.com/AWSCloudFormation/latest/UserGuide/\ intrinsic-function-reference-base64.html ''' return base64.b64encode(data) def handle_update(self, json_snippet=None): raise NotImplementedError("Update not implemented for Resource %s" % type(self)) def metadata_update(self, metadata): ''' No-op for resources which don't explicitly override this method ''' logger.warning("Resource %s does not implement metadata update" % self.name) class GenericResource(Resource): properties_schema = {} def handle_create(self): logger.warning('Creating generic resource (Type "%s")' % self.type()) def handle_update(self, json_snippet=None): logger.warning('Updating generic resource (Type "%s")' % self.type())