93b81315c6
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
676 lines
22 KiB
Python
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
|
|
)
|