# Copyright 2012 OpenStack Foundation # Copyright 2013 IBM Corp. # All Rights Reserved. # # 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 collections import datetime import uuid from oslo.config import cfg from oslo.utils import excutils from oslo.utils import importutils from oslo.utils import timeutils import six from glance.common import exception from glance import i18n import glance.openstack.common.log as logging _ = i18n._ _LE = i18n._LE LOG = logging.getLogger(__name__) CONF = cfg.CONF CONF.import_opt('task_executor', 'glance.common.config', group='task') _delayed_delete_imported = False def _import_delayed_delete(): # glance_store (indirectly) imports glance.domain therefore we can't put # the CONF.import_opt outside - we have to do it in a convoluted/indirect # way! global _delayed_delete_imported if not _delayed_delete_imported: CONF.import_opt('delayed_delete', 'glance_store') _delayed_delete_imported = True class ImageFactory(object): _readonly_properties = ['created_at', 'updated_at', 'status', 'checksum', 'size', 'virtual_size'] _reserved_properties = ['owner', 'is_public', 'locations', 'deleted', 'deleted_at', 'direct_url', 'self', 'file', 'schema'] def _check_readonly(self, kwargs): for key in self._readonly_properties: if key in kwargs: raise exception.ReadonlyProperty(property=key) def _check_unexpected(self, kwargs): if kwargs: msg = _('new_image() got unexpected keywords %s') raise TypeError(msg % kwargs.keys()) def _check_reserved(self, properties): if properties is not None: for key in self._reserved_properties: if key in properties: raise exception.ReservedProperty(property=key) def new_image(self, image_id=None, name=None, visibility='private', min_disk=0, min_ram=0, protected=False, owner=None, disk_format=None, container_format=None, extra_properties=None, tags=None, **other_args): self._check_readonly(other_args) self._check_unexpected(other_args) self._check_reserved(extra_properties) if image_id is None: image_id = str(uuid.uuid4()) created_at = timeutils.utcnow() updated_at = created_at status = 'queued' return Image(image_id=image_id, name=name, status=status, created_at=created_at, updated_at=updated_at, visibility=visibility, min_disk=min_disk, min_ram=min_ram, protected=protected, owner=owner, disk_format=disk_format, container_format=container_format, extra_properties=extra_properties, tags=tags) class Image(object): valid_state_targets = { # Each key denotes a "current" state for the image. Corresponding # values list the valid states to which we can jump from that "current" # state. # NOTE(flwang): In v2, we are deprecating the 'killed' status, so it's # allowed to restore image from 'saving' to 'queued' so that upload # can be retried. 'queued': ('saving', 'active', 'deleted'), 'saving': ('active', 'killed', 'deleted', 'queued'), 'active': ('queued', 'pending_delete', 'deleted'), 'killed': ('deleted'), 'pending_delete': ('deleted'), 'deleted': (), } def __init__(self, image_id, status, created_at, updated_at, **kwargs): self.image_id = image_id self.status = status self.created_at = created_at self.updated_at = updated_at self.name = kwargs.pop('name', None) self.visibility = kwargs.pop('visibility', 'private') self.min_disk = kwargs.pop('min_disk', 0) self.min_ram = kwargs.pop('min_ram', 0) self.protected = kwargs.pop('protected', False) self.locations = kwargs.pop('locations', []) self.checksum = kwargs.pop('checksum', None) self.owner = kwargs.pop('owner', None) self._disk_format = kwargs.pop('disk_format', None) self._container_format = kwargs.pop('container_format', None) self.size = kwargs.pop('size', None) self.virtual_size = kwargs.pop('virtual_size', None) extra_properties = kwargs.pop('extra_properties', None) or {} self.extra_properties = ExtraProperties(extra_properties) self.tags = kwargs.pop('tags', None) or [] if kwargs: message = _("__init__() got unexpected keyword argument '%s'") raise TypeError(message % kwargs.keys()[0]) @property def status(self): return self._status @status.setter def status(self, status): has_status = hasattr(self, '_status') if has_status: if status not in self.valid_state_targets[self._status]: kw = {'cur_status': self._status, 'new_status': status} e = exception.InvalidImageStatusTransition(**kw) LOG.debug(e) raise e if self._status == 'queued' and status in ('saving', 'active'): missing = [k for k in ['disk_format', 'container_format'] if not getattr(self, k)] if len(missing) > 0: if len(missing) == 1: msg = _('Property %s must be set prior to ' 'saving data.') else: msg = _('Properties %s must be set prior to ' 'saving data.') raise ValueError(msg % ', '.join(missing)) # NOTE(flwang): Image size should be cleared as long as the image # status is updated to 'queued' if status == 'queued': self.size = None self.virtual_size = None self._status = status @property def visibility(self): return self._visibility @visibility.setter def visibility(self, visibility): if visibility not in ('public', 'private'): raise ValueError(_('Visibility must be either "public" ' 'or "private"')) self._visibility = visibility @property def tags(self): return self._tags @tags.setter def tags(self, value): self._tags = set(value) @property def container_format(self): return self._container_format @container_format.setter def container_format(self, value): if hasattr(self, '_container_format') and self.status != 'queued': msg = _("Attribute container_format can be only replaced " "for a queued image.") raise exception.Forbidden(message=msg) self._container_format = value @property def disk_format(self): return self._disk_format @disk_format.setter def disk_format(self, value): if hasattr(self, '_disk_format') and self.status != 'queued': msg = _("Attribute disk_format can be only replaced " "for a queued image.") raise exception.Forbidden(message=msg) self._disk_format = value @property def min_disk(self): return self._min_disk @min_disk.setter def min_disk(self, value): if value and value < 0: extra_msg = _('Cannot be a negative value') raise exception.InvalidParameterValue(value=value, param='min_disk', extra_msg=extra_msg) self._min_disk = value @property def min_ram(self): return self._min_ram @min_ram.setter def min_ram(self, value): if value and value < 0: extra_msg = _('Cannot be a negative value') raise exception.InvalidParameterValue(value=value, param='min_ram', extra_msg=extra_msg) self._min_ram = value def delete(self): if self.protected: raise exception.ProtectedImageDelete(image_id=self.image_id) if CONF.delayed_delete and self.locations: self.status = 'pending_delete' else: self.status = 'deleted' def get_data(self, *args, **kwargs): raise NotImplementedError() def set_data(self, data, size=None): raise NotImplementedError() class ExtraProperties(collections.MutableMapping, dict): def __getitem__(self, key): return dict.__getitem__(self, key) def __setitem__(self, key, value): return dict.__setitem__(self, key, value) def __delitem__(self, key): return dict.__delitem__(self, key) def __eq__(self, other): if isinstance(other, ExtraProperties): return dict(self).__eq__(dict(other)) elif isinstance(other, dict): return dict(self).__eq__(other) else: return False def __len__(self): return dict(self).__len__() def keys(self): return dict(self).keys() class ImageMembership(object): def __init__(self, image_id, member_id, created_at, updated_at, id=None, status=None): self.id = id self.image_id = image_id self.member_id = member_id self.created_at = created_at self.updated_at = updated_at self.status = status @property def status(self): return self._status @status.setter def status(self, status): if status not in ('pending', 'accepted', 'rejected'): msg = _('Status must be "pending", "accepted" or "rejected".') raise ValueError(msg) self._status = status class ImageMemberFactory(object): def new_image_member(self, image, member_id): created_at = timeutils.utcnow() updated_at = created_at return ImageMembership(image_id=image.image_id, member_id=member_id, created_at=created_at, updated_at=updated_at, status='pending') class Task(object): _supported_task_type = ('import',) _supported_task_status = ('pending', 'processing', 'success', 'failure') def __init__(self, task_id, task_type, status, owner, expires_at, created_at, updated_at, task_input, result, message, task_time_to_live=48): if task_type not in self._supported_task_type: raise exception.InvalidTaskType(task_type) if status not in self._supported_task_status: raise exception.InvalidTaskStatus(status) self.task_id = task_id self._status = status self.type = task_type self.owner = owner self.expires_at = expires_at # NOTE(nikhil): We use '_time_to_live' to determine how long a # task should live from the time it succeeds or fails. self._time_to_live = datetime.timedelta(hours=task_time_to_live) self.created_at = created_at self.updated_at = updated_at self.task_input = task_input self.result = result self.message = message @property def status(self): return self._status @property def message(self): return self._message @message.setter def message(self, message): if message: self._message = six.text_type(message) else: self._message = six.text_type('') def _validate_task_status_transition(self, cur_status, new_status): valid_transitions = { 'pending': ['processing', 'failure'], 'processing': ['success', 'failure'], 'success': [], 'failure': [], } if new_status in valid_transitions[cur_status]: return True else: return False def _set_task_status(self, new_status): if self._validate_task_status_transition(self.status, new_status): self._status = new_status log_msg = (_("Task [%(task_id)s] status changing from " "%(cur_status)s to %(new_status)s") % {'task_id': self.task_id, 'cur_status': self.status, 'new_status': new_status}) LOG.info(log_msg) self._status = new_status else: log_msg = (_("Task [%(task_id)s] status failed to change from " "%(cur_status)s to %(new_status)s") % {'task_id': self.task_id, 'cur_status': self.status, 'new_status': new_status}) LOG.error(log_msg) raise exception.InvalidTaskStatusTransition( cur_status=self.status, new_status=new_status ) def begin_processing(self): new_status = 'processing' self._set_task_status(new_status) def succeed(self, result): new_status = 'success' self.result = result self._set_task_status(new_status) self.expires_at = timeutils.utcnow() + self._time_to_live def fail(self, message): new_status = 'failure' self.message = message self._set_task_status(new_status) self.expires_at = timeutils.utcnow() + self._time_to_live def run(self, executor): executor.begin_processing(self.task_id) class TaskStub(object): def __init__(self, task_id, task_type, status, owner, expires_at, created_at, updated_at): self.task_id = task_id self._status = status self.type = task_type self.owner = owner self.expires_at = expires_at self.created_at = created_at self.updated_at = updated_at @property def status(self): return self._status class TaskFactory(object): def new_task(self, task_type, owner, task_time_to_live=48, task_input=None, **kwargs): task_id = str(uuid.uuid4()) status = 'pending' # Note(nikhil): expires_at would be set on the task, only when it # succeeds or fails. expires_at = None created_at = timeutils.utcnow() updated_at = created_at return Task( task_id, task_type, status, owner, expires_at, created_at, updated_at, task_input, kwargs.get('message'), kwargs.get('result'), task_time_to_live ) class TaskExecutorFactory(object): def __init__(self, task_repo, image_repo, image_factory): self.task_repo = task_repo self.image_repo = image_repo self.image_factory = image_factory def new_task_executor(self, context): try: executor_cls = ('glance.async.%s_executor.' 'TaskExecutor' % CONF.task.task_executor) LOG.debug("Loading %s executor" % CONF.task.task_executor) executor = importutils.import_class(executor_cls) return executor(context, self.task_repo, self.image_repo, self.image_factory) except ImportError: with excutils.save_and_reraise_exception(): LOG.exception(_LE("Failed to load the %s executor provided " "in the config.") % CONF.task.task_executor) class MetadefNamespace(object): def __init__(self, namespace_id, namespace, display_name, description, owner, visibility, protected, created_at, updated_at): self.namespace_id = namespace_id self.namespace = namespace self.display_name = display_name self.description = description self.owner = owner self.visibility = visibility or "private" self.protected = protected or False self.created_at = created_at self.updated_at = updated_at def delete(self): if self.protected: raise exception.ProtectedMetadefNamespaceDelete( namespace=self.namespace) class MetadefNamespaceFactory(object): def new_namespace(self, namespace, owner, **kwargs): namespace_id = str(uuid.uuid4()) created_at = timeutils.utcnow() updated_at = created_at return MetadefNamespace( namespace_id, namespace, kwargs.get('display_name'), kwargs.get('description'), owner, kwargs.get('visibility'), kwargs.get('protected'), created_at, updated_at ) class MetadefObject(object): def __init__(self, namespace, object_id, name, created_at, updated_at, required, description, properties): self.namespace = namespace self.object_id = object_id self.name = name self.created_at = created_at self.updated_at = updated_at self.required = required self.description = description self.properties = properties def delete(self): if self.namespace.protected: raise exception.ProtectedMetadefObjectDelete(object_name=self.name) class MetadefObjectFactory(object): def new_object(self, namespace, name, **kwargs): object_id = str(uuid.uuid4()) created_at = timeutils.utcnow() updated_at = created_at return MetadefObject( namespace, object_id, name, created_at, updated_at, kwargs.get('required'), kwargs.get('description'), kwargs.get('properties') ) class MetadefResourceType(object): def __init__(self, namespace, name, prefix, properties_target, created_at, updated_at): self.namespace = namespace self.name = name self.prefix = prefix self.properties_target = properties_target self.created_at = created_at self.updated_at = updated_at def delete(self): if self.namespace.protected: raise exception.ProtectedMetadefResourceTypeAssociationDelete( resource_type=self.name) class MetadefResourceTypeFactory(object): def new_resource_type(self, namespace, name, **kwargs): created_at = timeutils.utcnow() updated_at = created_at return MetadefResourceType( namespace, name, kwargs.get('prefix'), kwargs.get('properties_target'), created_at, updated_at ) class MetadefProperty(object): def __init__(self, namespace, property_id, name, schema): self.namespace = namespace self.property_id = property_id self.name = name self.schema = schema def delete(self): if self.namespace.protected: raise exception.ProtectedMetadefNamespacePropDelete( property_name=self.name) class MetadefPropertyFactory(object): def new_namespace_property(self, namespace, name, schema, **kwargs): property_id = str(uuid.uuid4()) return MetadefProperty( namespace, property_id, name, schema )