466 lines
16 KiB
Python
466 lines
16 KiB
Python
# 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 inspect
|
|
|
|
from oslo_context import context as oslo_context
|
|
from oslo_log import log as logging
|
|
from oslo_utils import timeutils
|
|
from osprofiler import profiler
|
|
import six
|
|
|
|
from senlin.common import consts
|
|
from senlin.common import context
|
|
from senlin.common import exception as exc
|
|
from senlin.common.i18n import _, _LE, _LW
|
|
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
|
|
|
|
@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 = {}
|
|
|
|
profile = None
|
|
try:
|
|
profile = cls(name, spec, metadata=metadata, user=ctx.user,
|
|
project=ctx.project)
|
|
profile.validate(True)
|
|
except (exc.ResourceNotFound, exc.ESchema) as ex:
|
|
error = _("Failed in creating profile %(name)s: %(error)s"
|
|
) % {"name": name, "error": six.text_type(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.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.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.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.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 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 do_create(self, obj):
|
|
"""For subclass to override."""
|
|
raise NotImplementedError
|
|
|
|
def do_delete(self, obj, **params):
|
|
"""For subclass to override."""
|
|
raise NotImplementedError
|
|
|
|
def do_update(self, obj, new_profile, **params):
|
|
"""For subclass to override."""
|
|
LOG.warning(_LW("Update operation not supported."))
|
|
return True
|
|
|
|
def do_check(self, obj):
|
|
"""For subclass to override."""
|
|
LOG.warning(_LW("Check operation not supported."))
|
|
return True
|
|
|
|
def do_get_details(self, obj):
|
|
"""For subclass to override."""
|
|
LOG.warning(_LW("Get_details operation not supported."))
|
|
return {}
|
|
|
|
def do_join(self, obj, cluster_id):
|
|
"""For subclass to override to perform extra operations."""
|
|
LOG.warning(_LW("Join operation not specialized."))
|
|
return True
|
|
|
|
def do_leave(self, obj):
|
|
"""For subclass to override to perform extra operations."""
|
|
LOG.warning(_LW("Leave operation not specialized."))
|
|
return True
|
|
|
|
def do_recover(self, obj, **options):
|
|
"""Default recover operation.
|
|
|
|
:param obj: The node object to operate on.
|
|
:param options: Keyword arguments for the recover operation.
|
|
"""
|
|
operation = options.pop('operation', None)
|
|
# TODO(Qiming): The operation input could be a list of operations.
|
|
if operation and not isinstance(operation, six.string_types):
|
|
operation = operation[0]
|
|
|
|
if operation and operation != consts.RECOVER_RECREATE:
|
|
LOG.error(_LE("Recover operation not supported: %s"), operation)
|
|
return False
|
|
|
|
try:
|
|
self.do_delete(obj, **options)
|
|
except exc.EResourceDeletion as ex:
|
|
raise exc.EResourceOperation(op='recovering', type='node',
|
|
id=obj.id, message=six.text_type(ex))
|
|
res = None
|
|
try:
|
|
res = self.do_create(obj)
|
|
except exc.EResourceCreation as ex:
|
|
raise exc.EResourceOperation(op='recovering', type='node',
|
|
id=obj.id, message=six.text_type(ex))
|
|
return res
|
|
|
|
def do_validate(self, obj):
|
|
"""For subclass to override."""
|
|
LOG.warning(_LW("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(_LE("The following properties are not updatable: %s."
|
|
) % msg)
|
|
return False
|