deb-glance/glance/domain/__init__.py
Sabari Kumar Murugesan 93b81315c6 Remove state transition from active to queued
After commit Ieb03aaba887492819f9c58aa67f7acfcea81720e it is no
longer necessary to maintain a state transition from ACTIVE to
QUEUED. The only time this happened was when someone removed
the last location from an image. Since this was removed we
don't need it anymore.

Also, updates the image state transition images and docs to
represent the current state transitions.

Closes-Bug: #1555448

Change-Id: I390ae2a7110f06046f38914e946139b3b729bd2d
2016-03-10 06:22:11 +00:00

676 lines
22 KiB
Python

# 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_log import log as logging
from oslo_utils import excutils
from oslo_utils import importutils
import six
from glance.common import exception
from glance.common import timeutils
from glance.i18n import _, _LE, _LI, _LW
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', '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):
extra_properties = extra_properties or {}
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 or [])
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': ('pending_delete', 'deleted', 'deactivated'),
'killed': ('deleted',),
'pending_delete': ('deleted',),
'deleted': (),
'deactivated': ('active', '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', {})
self.extra_properties = ExtraProperties(extra_properties)
self.tags = kwargs.pop('tags', [])
if kwargs:
message = _("__init__() got unexpected keyword argument '%s'")
raise TypeError(message % list(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 deactivate(self):
if self.status == 'active':
self.status = 'deactivated'
elif self.status == 'deactivated':
# Noop if already deactive
pass
else:
LOG.debug("Not allowed to deactivate image in status '%s'",
self.status)
msg = (_("Not allowed to deactivate image in status '%s'")
% self.status)
raise exception.Forbidden(message=msg)
def reactivate(self):
if self.status == 'deactivated':
self.status = 'active'
elif self.status == 'active':
# Noop if already active
pass
else:
LOG.debug("Not allowed to reactivate image in status '%s'",
self.status)
msg = (_("Not allowed to reactivate image in status '%s'")
% self.status)
raise exception.Forbidden(message=msg)
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):
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.
task_time_to_live = CONF.task.task_time_to_live
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.info(_LI("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})
self._status = new_status
else:
LOG.error(_LE("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})
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_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('result'),
kwargs.get('message')
)
class TaskExecutorFactory(object):
eventlet_deprecation_warned = False
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:
# NOTE(flaper87): Backwards compatibility layer.
# It'll allow us to provide a deprecation path to
# users that are currently consuming the `eventlet`
# executor.
task_executor = CONF.task.task_executor
if task_executor == 'eventlet':
# NOTE(jokke): Making sure we do not log the deprecation
# warning 1000 times or anything crazy like that.
if not TaskExecutorFactory.eventlet_deprecation_warned:
msg = _LW("The `eventlet` executor has been deprecated. "
"Use `taskflow` instead.")
LOG.warn(msg)
TaskExecutorFactory.eventlet_deprecation_warned = True
task_executor = 'taskflow'
executor_cls = ('glance.async.%s_executor.'
'TaskExecutor' % task_executor)
LOG.debug("Loading %s executor", 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
)
class MetadefTag(object):
def __init__(self, namespace, tag_id, name, created_at, updated_at):
self.namespace = namespace
self.tag_id = tag_id
self.name = name
self.created_at = created_at
self.updated_at = updated_at
def delete(self):
if self.namespace.protected:
raise exception.ProtectedMetadefTagDelete(tag_name=self.name)
class MetadefTagFactory(object):
def new_tag(self, namespace, name, **kwargs):
tag_id = str(uuid.uuid4())
created_at = timeutils.utcnow()
updated_at = created_at
return MetadefTag(
namespace,
tag_id,
name,
created_at,
updated_at
)