glance/glance/domain/__init__.py

608 lines
20 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.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
)