# 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 import eventlet import inspect from oslo_config import cfg from oslo_context import context as oslo_context from oslo_log import log as logging from oslo_utils import timeutils from osprofiler import profiler from senlin.common import consts from senlin.common import context from senlin.common import exception as exc from senlin.common.i18n import _ from senlin.common import schema from senlin.common import utils from senlin.drivers import base as driver_base from senlin.engine import environment from senlin.objects import credential as co from senlin.objects import profile as po LOG = logging.getLogger(__name__) class Profile(object): """Base class for profiles.""" VERSIONS = {} KEYS = ( TYPE, VERSION, PROPERTIES, ) = ( 'type', 'version', 'properties', ) spec_schema = { TYPE: schema.String( _('Name of the profile type.'), required=True, ), VERSION: schema.String( _('Version number of the profile type.'), required=True, ), PROPERTIES: schema.Map( _('Properties for the profile.'), required=True, ) } properties_schema = {} OPERATIONS = {} def __new__(cls, name, spec, **kwargs): """Create a new profile of the appropriate class. :param name: The name for the profile. :param spec: A dictionary containing the spec for the profile. :param kwargs: Keyword arguments for profile creation. :returns: An instance of a specific sub-class of Profile. """ type_name, version = schema.get_spec_version(spec) type_str = "-".join([type_name, version]) if cls != Profile: ProfileClass = cls else: ProfileClass = environment.global_env().get_profile(type_str) return super(Profile, cls).__new__(ProfileClass) def __init__(self, name, spec, **kwargs): """Initialize a profile instance. :param name: A string that specifies the name for the profile. :param spec: A dictionary containing the detailed profile spec. :param kwargs: Keyword arguments for initializing the profile. :returns: An instance of a specific sub-class of Profile. """ type_name, version = schema.get_spec_version(spec) self.type_name = type_name self.version = version type_str = "-".join([type_name, version]) self.name = name self.spec = spec self.id = kwargs.get('id', None) self.type = kwargs.get('type', type_str) self.user = kwargs.get('user') self.project = kwargs.get('project') self.domain = kwargs.get('domain') self.metadata = kwargs.get('metadata', {}) self.created_at = kwargs.get('created_at', None) self.updated_at = kwargs.get('updated_at', None) self.spec_data = schema.Spec(self.spec_schema, self.spec) self.properties = schema.Spec( self.properties_schema, self.spec.get(self.PROPERTIES, {}), version) if not self.id: # new object needs a context dict self.context = self._init_context() else: self.context = kwargs.get('context') # initialize clients self._computeclient = None self._networkclient = None self._orchestrationclient = None self._workflowclient = None self._block_storageclient = None self._glanceclient = None @classmethod def _from_object(cls, profile): """Construct a profile from profile object. :param profile: a profile object that contains all required fields. """ kwargs = { 'id': profile.id, 'type': profile.type, 'context': profile.context, 'user': profile.user, 'project': profile.project, 'domain': profile.domain, 'metadata': profile.metadata, 'created_at': profile.created_at, 'updated_at': profile.updated_at, } return cls(profile.name, profile.spec, **kwargs) @classmethod def load(cls, ctx, profile=None, profile_id=None, project_safe=True): """Retrieve a profile object from database.""" if profile is None: profile = po.Profile.get(ctx, profile_id, project_safe=project_safe) if profile is None: raise exc.ResourceNotFound(type='profile', id=profile_id) return cls._from_object(profile) @classmethod def create(cls, ctx, name, spec, metadata=None): """Create a profile object and validate it. :param ctx: The requesting context. :param name: The name for the profile object. :param spec: A dict containing the detailed spec. :param metadata: An optional dictionary specifying key-value pairs to be associated with the profile. :returns: An instance of Profile. """ if metadata is None: metadata = {} try: profile = cls(name, spec, metadata=metadata, user=ctx.user_id, project=ctx.project_id) profile.validate(True) except (exc.ResourceNotFound, exc.ESchema) as ex: error = _("Failed in creating profile %(name)s: %(error)s" ) % {"name": name, "error": str(ex)} raise exc.InvalidSpec(message=error) profile.store(ctx) return profile @classmethod def delete(cls, ctx, profile_id): po.Profile.delete(ctx, profile_id) def store(self, ctx): """Store the profile into database and return its ID.""" timestamp = timeutils.utcnow(True) values = { 'name': self.name, 'type': self.type, 'context': self.context, 'spec': self.spec, 'user': self.user, 'project': self.project, 'domain': self.domain, 'meta_data': self.metadata, } if self.id: self.updated_at = timestamp values['updated_at'] = timestamp po.Profile.update(ctx, self.id, values) else: self.created_at = timestamp values['created_at'] = timestamp profile = po.Profile.create(ctx, values) self.id = profile.id return self.id @classmethod @profiler.trace('Profile.create_object', hide_args=False) def create_object(cls, ctx, obj): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_create(obj) @classmethod @profiler.trace('Profile.create_cluster_object', hide_args=False) def create_cluster_object(cls, ctx, obj): profile = cls.load(ctx, profile_id=obj.profile_id) try: ret = profile.do_cluster_create(obj) except NotImplementedError: return None return ret @classmethod @profiler.trace('Profile.delete_object', hide_args=False) def delete_object(cls, ctx, obj, **params): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_delete(obj, **params) @classmethod @profiler.trace('Profile.delete_cluster_object', hide_args=False) def delete_cluster_object(cls, ctx, obj, **params): profile = cls.load(ctx, profile_id=obj.profile_id) try: ret = profile.do_cluster_delete(obj, **params) except NotImplementedError: return None return ret @classmethod @profiler.trace('Profile.update_object', hide_args=False) def update_object(cls, ctx, obj, new_profile_id=None, **params): profile = cls.load(ctx, profile_id=obj.profile_id) new_profile = None if new_profile_id: new_profile = cls.load(ctx, profile_id=new_profile_id) return profile.do_update(obj, new_profile, **params) @classmethod @profiler.trace('Profile.get_details', hide_args=False) def get_details(cls, ctx, obj): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_get_details(obj) @classmethod @profiler.trace('Profile.adopt_node', hide_args=False) def adopt_node(cls, ctx, obj, type_name, overrides=None, snapshot=False): """Adopt a node. :param ctx: Request context. :param obj: A temporary node object. :param overrides: An optional parameter that specifies the set of properties to be overridden. :param snapshot: A boolean flag indicating whether a snapshot should be created before adopting the node. :returns: A dictionary containing the profile spec created from the specific node, or a dictionary containing error message. """ parts = type_name.split("-") tmpspec = {"type": parts[0], "version": parts[1]} profile = cls("name", tmpspec) return profile.do_adopt(obj, overrides=overrides, snapshot=snapshot) @classmethod @profiler.trace('Profile.join_cluster', hide_args=False) def join_cluster(cls, ctx, obj, cluster_id): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_join(obj, cluster_id) @classmethod @profiler.trace('Profile.leave_cluster', hide_args=False) def leave_cluster(cls, ctx, obj): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_leave(obj) @classmethod @profiler.trace('Profile.check_object', hide_args=False) def check_object(cls, ctx, obj): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_check(obj) @classmethod @profiler.trace('Profile.check_object', hide_args=False) def healthcheck_object(cls, ctx, obj, health_check_type): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_healthcheck(obj, health_check_type) @classmethod @profiler.trace('Profile.recover_object', hide_args=False) def recover_object(cls, ctx, obj, **options): profile = cls.load(ctx, profile_id=obj.profile_id) return profile.do_recover(obj, **options) def validate(self, validate_props=False): """Validate the schema and the data provided.""" # general validation self.spec_data.validate() self.properties.validate() ctx_dict = self.properties.get('context', {}) if ctx_dict: argspec = inspect.getargspec(context.RequestContext.__init__) valid_keys = argspec.args bad_keys = [k for k in ctx_dict if k not in valid_keys] if bad_keys: msg = _("Some keys in 'context' are invalid: %s") % bad_keys raise exc.ESchema(message=msg) if validate_props: self.do_validate(obj=self) @classmethod def get_schema(cls): return dict((name, dict(schema)) for name, schema in cls.properties_schema.items()) @classmethod def get_ops(cls): return dict((name, dict(schema)) for name, schema in cls.OPERATIONS.items()) def _init_context(self): profile_context = {} if self.CONTEXT in self.properties: profile_context = self.properties[self.CONTEXT] or {} ctx_dict = context.get_service_credentials(**profile_context) ctx_dict.pop('project_name', None) ctx_dict.pop('project_domain_name', None) return ctx_dict def _build_conn_params(self, user, project): """Build connection params for specific user and project. :param user: The ID of the user for which a trust will be used. :param project: The ID of the project for which a trust will be used. :returns: A dict containing the required parameters for connection creation. """ cred = co.Credential.get(oslo_context.get_current(), user, project) if cred is None: raise exc.TrustNotFound(trustor=user) trust_id = cred.cred['openstack']['trust'] # This is supposed to be trust-based authentication params = copy.deepcopy(self.context) params['trust_id'] = trust_id return params def compute(self, obj): """Construct compute client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. """ if self._computeclient is not None: return self._computeclient params = self._build_conn_params(obj.user, obj.project) self._computeclient = driver_base.SenlinDriver().compute(params) return self._computeclient def glance(self, obj): """Construct glance client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. """ if self._glanceclient is not None: return self._glanceclient params = self._build_conn_params(obj.user, obj.project) self._glanceclient = driver_base.SenlinDriver().glance(params) return self._glanceclient def network(self, obj): """Construct network client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. """ if self._networkclient is not None: return self._networkclient params = self._build_conn_params(obj.user, obj.project) self._networkclient = driver_base.SenlinDriver().network(params) return self._networkclient def orchestration(self, obj): """Construct orchestration client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. """ if self._orchestrationclient is not None: return self._orchestrationclient params = self._build_conn_params(obj.user, obj.project) oc = driver_base.SenlinDriver().orchestration(params) self._orchestrationclient = oc return oc def workflow(self, obj): if self._workflowclient is not None: return self._workflowclient params = self._build_conn_params(obj.user, obj.project) self._workflowclient = driver_base.SenlinDriver().workflow(params) return self._workflowclient def block_storage(self, obj): """Construct cinder client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. """ if self._block_storageclient is not None: return self._block_storageclient params = self._build_conn_params(obj.user, obj.project) self._block_storageclient = driver_base.SenlinDriver().block_storage( params) return self._block_storageclient def do_create(self, obj): """For subclass to override.""" raise NotImplementedError def do_cluster_create(self, obj): """For subclass to override.""" raise NotImplementedError def do_delete(self, obj, **params): """For subclass to override.""" raise NotImplementedError def do_cluster_delete(self, obj): """For subclass to override.""" raise NotImplementedError def do_update(self, obj, new_profile, **params): """For subclass to override.""" LOG.warning("Update operation not supported.") return True def do_check(self, obj): """For subclass to override.""" LOG.warning("Check operation not supported.") return True def do_healthcheck(self, obj): """Default healthcheck operation. This is provided as a fallback if a specific profile type does not override this method. :param obj: The node object to operate on. :return status: True indicates node is healthy, False indicates it is unhealthy. """ return self.do_check(obj) def do_get_details(self, obj): """For subclass to override.""" LOG.warning("Get_details operation not supported.") return {} def do_adopt(self, obj, overrides=None, snapshot=False): """For subclass to override.""" LOG.warning("Adopt operation not supported.") return {} def do_join(self, obj, cluster_id): """For subclass to override to perform extra operations.""" LOG.warning("Join operation not specialized.") return True def do_leave(self, obj): """For subclass to override to perform extra operations.""" LOG.warning("Leave operation not specialized.") return True def do_recover(self, obj, **options): """Default recover operation. This is provided as a fallback if a specific profile type does not override this method. :param obj: The node object to operate on. :param options: Keyword arguments for the recover operation. :return id: New id of the recovered resource or None if recovery failed. :return status: True indicates successful recovery, False indicates failure. """ operation = options.get('operation', None) force_recreate = options.get('force_recreate', None) delete_timeout = options.get('delete_timeout', None) if operation.upper() != consts.RECOVER_RECREATE: LOG.error("Recover operation not supported: %s", operation) return None, False extra_params = options.get('operation_params', None) fence_compute = False if extra_params: fence_compute = extra_params.get('fence_compute', False) try: self.do_delete(obj, force=fence_compute, timeout=delete_timeout) except exc.EResourceDeletion as ex: if force_recreate: # log error and continue on to creating the node LOG.warning('Failed to delete node during recovery action: %s', ex) else: raise exc.EResourceOperation(op='recovering', type='node', id=obj.id, message=str(ex)) # pause to allow deleted resource to get reclaimed by nova # this is needed to avoid a problem when the compute resources are # at their quota limit. The deleted resource has to become available # so that the new node can be created. eventlet.sleep(cfg.CONF.batch_interval) res = None try: res = self.do_create(obj) except exc.EResourceCreation as ex: raise exc.EResourceOperation(op='recovering', type='node', id=obj.id, message=str(ex), resource_id=ex.resource_id) return res, True def do_validate(self, obj): """For subclass to override.""" LOG.warning("Validate operation not supported.") return True def to_dict(self): pb_dict = { 'id': self.id, 'name': self.name, 'type': self.type, 'user': self.user, 'project': self.project, 'domain': self.domain, 'spec': self.spec, 'metadata': self.metadata, 'created_at': utils.isotime(self.created_at), 'updated_at': utils.isotime(self.updated_at), } return pb_dict def validate_for_update(self, new_profile): non_updatables = [] for (k, v) in new_profile.properties.items(): if self.properties.get(k, None) != v: if not self.properties_schema[k].updatable: non_updatables.append(k) if not non_updatables: return True msg = ", ".join(non_updatables) LOG.error("The following properties are not updatable: %s.", msg) return False