Removed redundant code for glance
This code left as artifact after port from cinder Remove it as unused/redundant. Change-Id: Ie4a1a646c63ee29e26379484bb50cd5b3b402cbe
This commit is contained in:
parent
ff7757cf26
commit
809d8bcd8e
@ -169,10 +169,6 @@ class PolicyNotAuthorized(NotAuthorized):
|
||||
message = _("Policy doesn't allow %(action)s to be performed.")
|
||||
|
||||
|
||||
class ImageNotAuthorized(ManilaException):
|
||||
message = _("Not authorized for image %(image_id)s.")
|
||||
|
||||
|
||||
class Invalid(ManilaException):
|
||||
message = _("Unacceptable parameters.")
|
||||
code = 400
|
||||
@ -213,10 +209,6 @@ class ServiceUnavailable(Invalid):
|
||||
message = _("Service is unavailable at this time.")
|
||||
|
||||
|
||||
class ImageUnacceptable(Invalid):
|
||||
message = _("Image %(image_id)s is unacceptable: %(reason)s")
|
||||
|
||||
|
||||
class InvalidUUID(Invalid):
|
||||
message = _("Expected a uuid but received %(uuid).")
|
||||
|
||||
@ -247,14 +239,6 @@ class ShareServerNotCreated(ManilaException):
|
||||
message = _("Share Server %(share_server_id)s failed on creation.")
|
||||
|
||||
|
||||
class InvalidImageRef(Invalid):
|
||||
message = _("Invalid image href %(image_href)s.")
|
||||
|
||||
|
||||
class ImageNotFound(NotFound):
|
||||
message = _("Image %(image_id)s could not be found.")
|
||||
|
||||
|
||||
class ServiceNotFound(NotFound):
|
||||
message = _("Service %(service_id)s could not be found.")
|
||||
|
||||
@ -466,10 +450,6 @@ class GlusterfsNoSuitableShareFound(NotFound):
|
||||
message = _("There is no share which can host %(share_size)sG")
|
||||
|
||||
|
||||
class ImageCopyFailure(Invalid):
|
||||
message = _("Failed to copy image to share")
|
||||
|
||||
|
||||
class InvalidShare(ManilaException):
|
||||
message = _("Invalid share: %(reason)s")
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 OpenStack, LLC.
|
||||
# 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.
|
@ -1,461 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
"""Implementation of an image service that uses Glance as the backend"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
import glanceclient
|
||||
import glanceclient.exc
|
||||
|
||||
from manila import exception
|
||||
|
||||
from manila.openstack.common import jsonutils
|
||||
from manila.openstack.common import log as logging
|
||||
from manila.openstack.common import timeutils
|
||||
from oslo.config import cfg
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def _parse_image_ref(image_href):
|
||||
"""Parse an image href into composite parts.
|
||||
|
||||
:param image_href: href of an image
|
||||
:returns: a tuple of the form (image_id, host, port)
|
||||
:raises ValueError
|
||||
|
||||
"""
|
||||
url = urlparse.urlparse(image_href)
|
||||
port = url.port or 80
|
||||
host = url.netloc.split(':', 1)[0]
|
||||
image_id = url.path.split('/')[-1]
|
||||
use_ssl = (url.scheme == 'https')
|
||||
return (image_id, host, port, use_ssl)
|
||||
|
||||
|
||||
def _create_glance_client(context, host, port, use_ssl,
|
||||
version=CONF.glance_api_version):
|
||||
"""Instantiate a new glanceclient.Client object"""
|
||||
if version is None:
|
||||
version = CONF.glance_api_version
|
||||
if use_ssl:
|
||||
scheme = 'https'
|
||||
else:
|
||||
scheme = 'http'
|
||||
params = {}
|
||||
params['insecure'] = CONF.glance_api_insecure
|
||||
if CONF.auth_strategy == 'keystone':
|
||||
params['token'] = context.auth_token
|
||||
endpoint = '%s://%s:%s' % (scheme, host, port)
|
||||
return glanceclient.Client(str(version), endpoint, **params)
|
||||
|
||||
|
||||
def get_api_servers():
|
||||
"""
|
||||
Shuffle a list of CONF.glance_api_servers and return an iterator
|
||||
that will cycle through the list, looping around to the beginning
|
||||
if necessary.
|
||||
"""
|
||||
api_servers = []
|
||||
for api_server in CONF.glance_api_servers:
|
||||
if '//' not in api_server:
|
||||
api_server = 'http://' + api_server
|
||||
url = urlparse.urlparse(api_server)
|
||||
port = url.port or 80
|
||||
host = url.netloc.split(':', 1)[0]
|
||||
use_ssl = (url.scheme == 'https')
|
||||
api_servers.append((host, port, use_ssl))
|
||||
random.shuffle(api_servers)
|
||||
return itertools.cycle(api_servers)
|
||||
|
||||
|
||||
class GlanceClientWrapper(object):
|
||||
"""Glance client wrapper class that implements retries."""
|
||||
|
||||
def __init__(self, context=None, host=None, port=None, use_ssl=False,
|
||||
version=None):
|
||||
if host is not None:
|
||||
self.client = self._create_static_client(context,
|
||||
host, port,
|
||||
use_ssl, version)
|
||||
else:
|
||||
self.client = None
|
||||
self.api_servers = None
|
||||
self.version = version
|
||||
|
||||
def _create_static_client(self, context, host, port, use_ssl, version):
|
||||
"""Create a client that we'll use for every call."""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.use_ssl = use_ssl
|
||||
self.version = version
|
||||
return _create_glance_client(context,
|
||||
self.host, self.port,
|
||||
self.use_ssl, self.version)
|
||||
|
||||
def _create_onetime_client(self, context, version):
|
||||
"""Create a client that will be used for one call."""
|
||||
if self.api_servers is None:
|
||||
self.api_servers = get_api_servers()
|
||||
self.host, self.port, self.use_ssl = self.api_servers.next()
|
||||
return _create_glance_client(context,
|
||||
self.host, self.port,
|
||||
self.use_ssl, version)
|
||||
|
||||
def call(self, context, method, *args, **kwargs):
|
||||
"""
|
||||
Call a glance client method. If we get a connection error,
|
||||
retry the request according to CONF.glance_num_retries.
|
||||
"""
|
||||
version = self.version
|
||||
if version in kwargs:
|
||||
version = kwargs['version']
|
||||
|
||||
retry_excs = (glanceclient.exc.ServiceUnavailable,
|
||||
glanceclient.exc.InvalidEndpoint,
|
||||
glanceclient.exc.CommunicationError)
|
||||
num_attempts = 1 + CONF.glance_num_retries
|
||||
|
||||
for attempt in xrange(1, num_attempts + 1):
|
||||
client = self.client or self._create_onetime_client(context,
|
||||
version)
|
||||
try:
|
||||
return getattr(client.images, method)(*args, **kwargs)
|
||||
except retry_excs as e:
|
||||
host = self.host
|
||||
port = self.port
|
||||
extra = "retrying"
|
||||
error_msg = _("Error contacting glance server "
|
||||
"'%(host)s:%(port)s' for '%(method)s', "
|
||||
"%(extra)s.")
|
||||
if attempt == num_attempts:
|
||||
extra = 'done trying'
|
||||
LOG.exception(error_msg, locals())
|
||||
raise exception.GlanceConnectionFailed(host=host,
|
||||
port=port,
|
||||
reason=str(e))
|
||||
LOG.exception(error_msg, locals())
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
class GlanceImageService(object):
|
||||
"""Provides storage and retrieval of disk image objects within Glance."""
|
||||
|
||||
def __init__(self, client=None):
|
||||
self._client = client or GlanceClientWrapper()
|
||||
|
||||
def detail(self, context, **kwargs):
|
||||
"""Calls out to Glance for a list of detailed image information."""
|
||||
params = self._extract_query_params(kwargs)
|
||||
try:
|
||||
images = self._client.call(context, 'list', **params)
|
||||
except Exception:
|
||||
_reraise_translated_exception()
|
||||
|
||||
_images = []
|
||||
for image in images:
|
||||
if self._is_image_available(context, image):
|
||||
_images.append(self._translate_from_glance(image))
|
||||
|
||||
return _images
|
||||
|
||||
def _extract_query_params(self, params):
|
||||
_params = {}
|
||||
accepted_params = ('filters', 'marker', 'limit',
|
||||
'sort_key', 'sort_dir')
|
||||
for param in accepted_params:
|
||||
if param in params:
|
||||
_params[param] = params.get(param)
|
||||
|
||||
# ensure filters is a dict
|
||||
_params.setdefault('filters', {})
|
||||
# NOTE(vish): don't filter out private images
|
||||
_params['filters'].setdefault('is_public', 'none')
|
||||
|
||||
return _params
|
||||
|
||||
def show(self, context, image_id):
|
||||
"""Returns a dict with image data for the given opaque image id."""
|
||||
try:
|
||||
image = self._client.call(context, 'get', image_id)
|
||||
except Exception:
|
||||
_reraise_translated_image_exception(image_id)
|
||||
|
||||
if not self._is_image_available(context, image):
|
||||
raise exception.ImageNotFound(image_id=image_id)
|
||||
|
||||
base_image_meta = self._translate_from_glance(image)
|
||||
return base_image_meta
|
||||
|
||||
def get_location(self, context, image_id):
|
||||
"""Returns the direct url representing the backend storage location,
|
||||
or None if this attribute is not shown by Glance."""
|
||||
try:
|
||||
client = GlanceClientWrapper()
|
||||
image_meta = client.call(context, 'get', image_id)
|
||||
except Exception:
|
||||
_reraise_translated_image_exception(image_id)
|
||||
|
||||
if not self._is_image_available(context, image_meta):
|
||||
raise exception.ImageNotFound(image_id=image_id)
|
||||
|
||||
return getattr(image_meta, 'direct_url', None)
|
||||
|
||||
def download(self, context, image_id, data):
|
||||
"""Calls out to Glance for metadata and data and writes data."""
|
||||
try:
|
||||
image_chunks = self._client.call(context, 'data', image_id)
|
||||
except Exception:
|
||||
_reraise_translated_image_exception(image_id)
|
||||
|
||||
for chunk in image_chunks:
|
||||
data.write(chunk)
|
||||
|
||||
def create(self, context, image_meta, data=None):
|
||||
"""Store the image data and return the new image object."""
|
||||
sent_service_image_meta = self._translate_to_glance(image_meta)
|
||||
|
||||
if data:
|
||||
sent_service_image_meta['data'] = data
|
||||
|
||||
recv_service_image_meta = self._client.call(context, 'create',
|
||||
**sent_service_image_meta)
|
||||
|
||||
return self._translate_from_glance(recv_service_image_meta)
|
||||
|
||||
def update(self, context, image_id,
|
||||
image_meta, data=None, purge_props=True):
|
||||
"""Modify the given image with the new data."""
|
||||
image_meta = self._translate_to_glance(image_meta)
|
||||
image_meta['purge_props'] = purge_props
|
||||
#NOTE(bcwaldon): id is not an editable field, but it is likely to be
|
||||
# passed in by calling code. Let's be nice and ignore it.
|
||||
image_meta.pop('id', None)
|
||||
if data:
|
||||
image_meta['data'] = data
|
||||
try:
|
||||
image_meta = self._client.call(context, 'update', image_id,
|
||||
**image_meta)
|
||||
except Exception:
|
||||
_reraise_translated_image_exception(image_id)
|
||||
else:
|
||||
return self._translate_from_glance(image_meta)
|
||||
|
||||
def delete(self, context, image_id):
|
||||
"""Delete the given image.
|
||||
|
||||
:raises: ImageNotFound if the image does not exist.
|
||||
:raises: NotAuthorized if the user is not an owner.
|
||||
|
||||
"""
|
||||
try:
|
||||
self._client.call(context, 'delete', image_id)
|
||||
except glanceclient.exc.NotFound:
|
||||
raise exception.ImageNotFound(image_id=image_id)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _translate_to_glance(image_meta):
|
||||
image_meta = _convert_to_string(image_meta)
|
||||
image_meta = _remove_read_only(image_meta)
|
||||
return image_meta
|
||||
|
||||
@staticmethod
|
||||
def _translate_from_glance(image):
|
||||
image_meta = _extract_attributes(image)
|
||||
image_meta = _convert_timestamps_to_datetimes(image_meta)
|
||||
image_meta = _convert_from_string(image_meta)
|
||||
return image_meta
|
||||
|
||||
@staticmethod
|
||||
def _is_image_available(context, image):
|
||||
"""Check image availability.
|
||||
|
||||
This check is needed in case Nova and Glance are deployed
|
||||
without authentication turned on.
|
||||
"""
|
||||
# The presence of an auth token implies this is an authenticated
|
||||
# request and we need not handle the noauth use-case.
|
||||
if hasattr(context, 'auth_token') and context.auth_token:
|
||||
return True
|
||||
|
||||
if image.is_public or context.is_admin:
|
||||
return True
|
||||
|
||||
properties = image.properties
|
||||
|
||||
if context.project_id and ('owner_id' in properties):
|
||||
return str(properties['owner_id']) == str(context.project_id)
|
||||
|
||||
if context.project_id and ('project_id' in properties):
|
||||
return str(properties['project_id']) == str(context.project_id)
|
||||
|
||||
try:
|
||||
user_id = properties['user_id']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return str(user_id) == str(context.user_id)
|
||||
|
||||
|
||||
def _convert_timestamps_to_datetimes(image_meta):
|
||||
"""Returns image with timestamp fields converted to datetime objects."""
|
||||
for attr in ['created_at', 'updated_at', 'deleted_at']:
|
||||
if image_meta.get(attr):
|
||||
image_meta[attr] = timeutils.parse_isotime(image_meta[attr])
|
||||
return image_meta
|
||||
|
||||
|
||||
# NOTE(bcwaldon): used to store non-string data in glance metadata
|
||||
def _json_loads(properties, attr):
|
||||
prop = properties[attr]
|
||||
if isinstance(prop, basestring):
|
||||
properties[attr] = jsonutils.loads(prop)
|
||||
|
||||
|
||||
def _json_dumps(properties, attr):
|
||||
prop = properties[attr]
|
||||
if not isinstance(prop, basestring):
|
||||
properties[attr] = jsonutils.dumps(prop)
|
||||
|
||||
|
||||
_CONVERT_PROPS = ('block_device_mapping', 'mappings')
|
||||
|
||||
|
||||
def _convert(method, metadata):
|
||||
metadata = copy.deepcopy(metadata)
|
||||
properties = metadata.get('properties')
|
||||
if properties:
|
||||
for attr in _CONVERT_PROPS:
|
||||
if attr in properties:
|
||||
method(properties, attr)
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def _convert_from_string(metadata):
|
||||
return _convert(_json_loads, metadata)
|
||||
|
||||
|
||||
def _convert_to_string(metadata):
|
||||
return _convert(_json_dumps, metadata)
|
||||
|
||||
|
||||
def _extract_attributes(image):
|
||||
IMAGE_ATTRIBUTES = ['size', 'disk_format', 'owner',
|
||||
'container_format', 'checksum', 'id',
|
||||
'name', 'created_at', 'updated_at',
|
||||
'deleted_at', 'deleted', 'status',
|
||||
'min_disk', 'min_ram', 'is_public']
|
||||
output = {}
|
||||
for attr in IMAGE_ATTRIBUTES:
|
||||
output[attr] = getattr(image, attr, None)
|
||||
|
||||
output['properties'] = getattr(image, 'properties', {})
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _remove_read_only(image_meta):
|
||||
IMAGE_ATTRIBUTES = ['status', 'updated_at', 'created_at', 'deleted_at']
|
||||
output = copy.deepcopy(image_meta)
|
||||
for attr in IMAGE_ATTRIBUTES:
|
||||
if attr in output:
|
||||
del output[attr]
|
||||
return output
|
||||
|
||||
|
||||
def _reraise_translated_image_exception(image_id):
|
||||
"""Transform the exception for the image but keep its traceback intact."""
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
new_exc = _translate_image_exception(image_id, exc_value)
|
||||
raise new_exc, None, exc_trace
|
||||
|
||||
|
||||
def _reraise_translated_exception():
|
||||
"""Transform the exception but keep its traceback intact."""
|
||||
exc_type, exc_value, exc_trace = sys.exc_info()
|
||||
new_exc = _translate_plain_exception(exc_value)
|
||||
raise new_exc, None, exc_trace
|
||||
|
||||
|
||||
def _translate_image_exception(image_id, exc_value):
|
||||
if isinstance(exc_value, (glanceclient.exc.Forbidden,
|
||||
glanceclient.exc.Unauthorized)):
|
||||
return exception.ImageNotAuthorized(image_id=image_id)
|
||||
if isinstance(exc_value, glanceclient.exc.NotFound):
|
||||
return exception.ImageNotFound(image_id=image_id)
|
||||
if isinstance(exc_value, glanceclient.exc.BadRequest):
|
||||
return exception.Invalid(exc_value)
|
||||
return exc_value
|
||||
|
||||
|
||||
def _translate_plain_exception(exc_value):
|
||||
if isinstance(exc_value, (glanceclient.exc.Forbidden,
|
||||
glanceclient.exc.Unauthorized)):
|
||||
return exception.NotAuthorized(exc_value)
|
||||
if isinstance(exc_value, glanceclient.exc.NotFound):
|
||||
return exception.NotFound(exc_value)
|
||||
if isinstance(exc_value, glanceclient.exc.BadRequest):
|
||||
return exception.Invalid(exc_value)
|
||||
return exc_value
|
||||
|
||||
|
||||
def get_remote_image_service(context, image_href):
|
||||
"""Create an image_service and parse the id from the given image_href.
|
||||
|
||||
The image_href param can be an href of the form
|
||||
'http://example.com:9292/v1/images/b8b2c6f7-7345-4e2f-afa2-eedaba9cbbe3',
|
||||
or just an id such as 'b8b2c6f7-7345-4e2f-afa2-eedaba9cbbe3'. If the
|
||||
image_href is a standalone id, then the default image service is returned.
|
||||
|
||||
:param image_href: href that describes the location of an image
|
||||
:returns: a tuple of the form (image_service, image_id)
|
||||
|
||||
"""
|
||||
#NOTE(bcwaldon): If image_href doesn't look like a URI, assume its a
|
||||
# standalone image ID
|
||||
if '/' not in str(image_href):
|
||||
image_service = get_default_image_service()
|
||||
return image_service, image_href
|
||||
|
||||
try:
|
||||
(image_id, glance_host, glance_port, use_ssl) = \
|
||||
_parse_image_ref(image_href)
|
||||
glance_client = GlanceClientWrapper(context=context,
|
||||
host=glance_host,
|
||||
port=glance_port,
|
||||
use_ssl=use_ssl)
|
||||
except ValueError:
|
||||
raise exception.InvalidImageRef(image_href=image_href)
|
||||
|
||||
image_service = GlanceImageService(client=glance_client)
|
||||
return image_service, image_id
|
||||
|
||||
|
||||
def get_default_image_service():
|
||||
return GlanceImageService()
|
@ -1,250 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
# Copyright (c) 2010 Citrix Systems, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Helper methods to deal with images.
|
||||
|
||||
This is essentially a copy from nova.virt.images.py
|
||||
Some slight modifications, but at some point
|
||||
we should look at maybe pushign this up to OSLO
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from manila import exception
|
||||
|
||||
from manila.openstack.common import log as logging
|
||||
from manila import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
image_helper_opt = [cfg.StrOpt('image_conversion_dir',
|
||||
default='/tmp',
|
||||
help='parent dir for tempdir used for image conversion'), ]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(image_helper_opt)
|
||||
|
||||
|
||||
class QemuImgInfo(object):
|
||||
BACKING_FILE_RE = re.compile((r"^(.*?)\s*\(actual\s+path\s*:"
|
||||
r"\s+(.*?)\)\s*$"), re.I)
|
||||
TOP_LEVEL_RE = re.compile(r"^([\w\d\s\_\-]+):(.*)$")
|
||||
SIZE_RE = re.compile(r"\(\s*(\d+)\s+bytes\s*\)", re.I)
|
||||
|
||||
def __init__(self, cmd_output):
|
||||
details = self._parse(cmd_output)
|
||||
self.image = details.get('image')
|
||||
self.backing_file = details.get('backing_file')
|
||||
self.file_format = details.get('file_format')
|
||||
self.virtual_size = details.get('virtual_size')
|
||||
self.cluster_size = details.get('cluster_size')
|
||||
self.disk_size = details.get('disk_size')
|
||||
self.snapshots = details.get('snapshot_list', [])
|
||||
self.encryption = details.get('encryption')
|
||||
|
||||
def __str__(self):
|
||||
lines = [
|
||||
'image: %s' % self.image,
|
||||
'file_format: %s' % self.file_format,
|
||||
'virtual_size: %s' % self.virtual_size,
|
||||
'disk_size: %s' % self.disk_size,
|
||||
'cluster_size: %s' % self.cluster_size,
|
||||
'backing_file: %s' % self.backing_file,
|
||||
]
|
||||
if self.snapshots:
|
||||
lines.append("snapshots: %s" % self.snapshots)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _canonicalize(self, field):
|
||||
# Standardize on underscores/lc/no dash and no spaces
|
||||
# since qemu seems to have mixed outputs here... and
|
||||
# this format allows for better integration with python
|
||||
# - ie for usage in kwargs and such...
|
||||
field = field.lower().strip()
|
||||
for c in (" ", "-"):
|
||||
field = field.replace(c, '_')
|
||||
return field
|
||||
|
||||
def _extract_bytes(self, details):
|
||||
# Replace it with the byte amount
|
||||
real_size = self.SIZE_RE.search(details)
|
||||
if real_size:
|
||||
details = real_size.group(1)
|
||||
try:
|
||||
details = utils.to_bytes(details)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return details
|
||||
|
||||
def _extract_details(self, root_cmd, root_details, lines_after):
|
||||
consumed_lines = 0
|
||||
real_details = root_details
|
||||
if root_cmd == 'backing_file':
|
||||
# Replace it with the real backing file
|
||||
backing_match = self.BACKING_FILE_RE.match(root_details)
|
||||
if backing_match:
|
||||
real_details = backing_match.group(2).strip()
|
||||
elif root_cmd in ['virtual_size', 'cluster_size', 'disk_size']:
|
||||
# Replace it with the byte amount (if we can convert it)
|
||||
real_details = self._extract_bytes(root_details)
|
||||
elif root_cmd == 'file_format':
|
||||
real_details = real_details.strip().lower()
|
||||
elif root_cmd == 'snapshot_list':
|
||||
# Next line should be a header, starting with 'ID'
|
||||
if not lines_after or not lines_after[0].startswith("ID"):
|
||||
msg = _("Snapshot list encountered but no header found!")
|
||||
raise ValueError(msg)
|
||||
consumed_lines += 1
|
||||
possible_contents = lines_after[1:]
|
||||
real_details = []
|
||||
# This is the sprintf pattern we will try to match
|
||||
# "%-10s%-20s%7s%20s%15s"
|
||||
# ID TAG VM SIZE DATE VM CLOCK (current header)
|
||||
for line in possible_contents:
|
||||
line_pieces = line.split(None)
|
||||
if len(line_pieces) != 6:
|
||||
break
|
||||
else:
|
||||
# Check against this pattern occuring in the final position
|
||||
# "%02d:%02d:%02d.%03d"
|
||||
date_pieces = line_pieces[5].split(":")
|
||||
if len(date_pieces) != 3:
|
||||
break
|
||||
real_details.append({
|
||||
'id': line_pieces[0],
|
||||
'tag': line_pieces[1],
|
||||
'vm_size': line_pieces[2],
|
||||
'date': line_pieces[3],
|
||||
'vm_clock': line_pieces[4] + " " + line_pieces[5],
|
||||
})
|
||||
consumed_lines += 1
|
||||
return (real_details, consumed_lines)
|
||||
|
||||
def _parse(self, cmd_output):
|
||||
# Analysis done of qemu-img.c to figure out what is going on here
|
||||
# Find all points start with some chars and then a ':' then a newline
|
||||
# and then handle the results of those 'top level' items in a separate
|
||||
# function.
|
||||
#
|
||||
# TODO(harlowja): newer versions might have a json output format
|
||||
# we should switch to that whenever possible.
|
||||
# see: http://bit.ly/XLJXDX
|
||||
if not cmd_output:
|
||||
cmd_output = ''
|
||||
contents = {}
|
||||
lines = cmd_output.splitlines()
|
||||
i = 0
|
||||
line_am = len(lines)
|
||||
while i < line_am:
|
||||
line = lines[i]
|
||||
if not line.strip():
|
||||
i += 1
|
||||
continue
|
||||
consumed_lines = 0
|
||||
top_level = self.TOP_LEVEL_RE.match(line)
|
||||
if top_level:
|
||||
root = self._canonicalize(top_level.group(1))
|
||||
if not root:
|
||||
i += 1
|
||||
continue
|
||||
root_details = top_level.group(2).strip()
|
||||
details, consumed_lines = self._extract_details(root,
|
||||
root_details,
|
||||
lines[i + 1:])
|
||||
contents[root] = details
|
||||
i += consumed_lines + 1
|
||||
return contents
|
||||
|
||||
|
||||
def qemu_img_info(path):
|
||||
"""Return a object containing the parsed output from qemu-img info."""
|
||||
out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
run_as_root=True)
|
||||
return QemuImgInfo(out)
|
||||
|
||||
|
||||
def convert_image(source, dest, out_format):
|
||||
"""Convert image to other format"""
|
||||
cmd = ('qemu-img', 'convert', '-O', out_format, source, dest)
|
||||
utils.execute(*cmd, run_as_root=True)
|
||||
|
||||
|
||||
def fetch(context, image_service, image_id, path, _user_id, _project_id):
|
||||
# TODO(vish): Improve context handling and add owner and auth data
|
||||
# when it is added to glance. Right now there is no
|
||||
# auth checking in glance, so we assume that access was
|
||||
# checked before we got here.
|
||||
with utils.remove_path_on_error(path):
|
||||
with open(path, "wb") as image_file:
|
||||
image_service.download(context, image_id, image_file)
|
||||
|
||||
|
||||
def fetch_to_raw(context, image_service,
|
||||
image_id, dest,
|
||||
user_id=None, project_id=None):
|
||||
if (CONF.image_conversion_dir and not
|
||||
os.path.exists(CONF.image_conversion_dir)):
|
||||
os.makedirs(CONF.image_conversion_dir)
|
||||
|
||||
# NOTE(avishay): I'm not crazy about creating temp files which may be
|
||||
# large and cause disk full errors which would confuse users.
|
||||
# Unfortunately it seems that you can't pipe to 'qemu-img convert' because
|
||||
# it seeks. Maybe we can think of something for a future version.
|
||||
fd, tmp = tempfile.mkstemp(dir=CONF.image_conversion_dir)
|
||||
os.close(fd)
|
||||
with utils.remove_path_on_error(tmp):
|
||||
fetch(context, image_service, image_id, tmp, user_id, project_id)
|
||||
|
||||
data = qemu_img_info(tmp)
|
||||
fmt = data.file_format
|
||||
if fmt is None:
|
||||
raise exception.ImageUnacceptable(
|
||||
reason=_("'qemu-img info' parsing failed."),
|
||||
image_id=image_id)
|
||||
|
||||
backing_file = data.backing_file
|
||||
if backing_file is not None:
|
||||
raise exception.ImageUnacceptable(
|
||||
image_id=image_id,
|
||||
reason=_("fmt=%(fmt)s backed by:"
|
||||
"%(backing_file)s") % locals())
|
||||
|
||||
# NOTE(jdg): I'm using qemu-img convert to write
|
||||
# to the volume regardless if it *needs* conversion or not
|
||||
# TODO(avishay): We can speed this up by checking if the image is raw
|
||||
# and if so, writing directly to the device. However, we need to keep
|
||||
# check via 'qemu-img info' that what we copied was in fact a raw
|
||||
# image and not a different format with a backing file, which may be
|
||||
# malicious.
|
||||
LOG.debug("%s was %s, converting to raw" % (image_id, fmt))
|
||||
convert_image(tmp, dest, 'raw')
|
||||
|
||||
data = qemu_img_info(dest)
|
||||
if data.file_format != "raw":
|
||||
raise exception.ImageUnacceptable(
|
||||
image_id=image_id,
|
||||
reason=_("Converted to raw, but format is now %s") %
|
||||
data.file_format)
|
||||
os.unlink(tmp)
|
@ -1,20 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Citrix Systems, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
:mod:`glance` -- Stubs for Glance
|
||||
=================================
|
||||
"""
|
@ -1,112 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Citrix Systems, Inc.
|
||||
#
|
||||
# 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 glanceclient.exc
|
||||
|
||||
|
||||
NOW_GLANCE_FORMAT = "2010-10-11T10:30:22"
|
||||
|
||||
|
||||
class StubGlanceClient(object):
|
||||
|
||||
def __init__(self, images=None):
|
||||
self._images = []
|
||||
_images = images or []
|
||||
map(lambda image: self.create(**image), _images)
|
||||
|
||||
#NOTE(bcwaldon): HACK to get client.images.* to work
|
||||
self.images = lambda: None
|
||||
for fn in ('list', 'get', 'data', 'create', 'update', 'delete'):
|
||||
setattr(self.images, fn, getattr(self, fn))
|
||||
|
||||
#TODO(bcwaldon): implement filters
|
||||
def list(self, filters=None, marker=None, limit=30):
|
||||
if marker is None:
|
||||
index = 0
|
||||
else:
|
||||
for index, image in enumerate(self._images):
|
||||
if image.id == str(marker):
|
||||
index += 1
|
||||
break
|
||||
else:
|
||||
raise glanceclient.exc.BadRequest('Marker not found')
|
||||
|
||||
return self._images[index:index + limit]
|
||||
|
||||
def get(self, image_id):
|
||||
for image in self._images:
|
||||
if image.id == str(image_id):
|
||||
return image
|
||||
raise glanceclient.exc.NotFound(image_id)
|
||||
|
||||
def data(self, image_id):
|
||||
self.get(image_id)
|
||||
return []
|
||||
|
||||
def create(self, **metadata):
|
||||
metadata['created_at'] = NOW_GLANCE_FORMAT
|
||||
metadata['updated_at'] = NOW_GLANCE_FORMAT
|
||||
|
||||
self._images.append(FakeImage(metadata))
|
||||
|
||||
try:
|
||||
image_id = str(metadata['id'])
|
||||
except KeyError:
|
||||
# auto-generate an id if one wasn't provided
|
||||
image_id = str(len(self._images))
|
||||
|
||||
self._images[-1].id = image_id
|
||||
|
||||
return self._images[-1]
|
||||
|
||||
def update(self, image_id, **metadata):
|
||||
for i, image in enumerate(self._images):
|
||||
if image.id == str(image_id):
|
||||
for k, v in metadata.items():
|
||||
setattr(self._images[i], k, v)
|
||||
return self._images[i]
|
||||
raise glanceclient.exc.NotFound(image_id)
|
||||
|
||||
def delete(self, image_id):
|
||||
for i, image in enumerate(self._images):
|
||||
if image.id == image_id:
|
||||
del self._images[i]
|
||||
return
|
||||
raise glanceclient.exc.NotFound(image_id)
|
||||
|
||||
|
||||
class FakeImage(object):
|
||||
def __init__(self, metadata):
|
||||
IMAGE_ATTRIBUTES = ['size', 'disk_format', 'owner',
|
||||
'container_format', 'checksum', 'id',
|
||||
'name', 'created_at', 'updated_at',
|
||||
'deleted', 'status',
|
||||
'min_disk', 'min_ram', 'is_public']
|
||||
raw = dict.fromkeys(IMAGE_ATTRIBUTES)
|
||||
raw.update(metadata)
|
||||
self.__dict__['raw'] = raw
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self.__dict__['raw'][key]
|
||||
except KeyError:
|
||||
raise AttributeError(key)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
try:
|
||||
self.__dict__['raw'][key] = value
|
||||
except KeyError:
|
||||
raise AttributeError(key)
|
@ -1,20 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work
|
||||
|
||||
from manila.tests import *
|
@ -1,593 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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 datetime
|
||||
import random
|
||||
import time
|
||||
|
||||
import glanceclient.exc
|
||||
from glanceclient.v2.client import Client as glanceclient_v2
|
||||
|
||||
from manila import context
|
||||
from manila import exception
|
||||
|
||||
from manila.image import glance
|
||||
from manila import test
|
||||
from manila.tests.glance import stubs as glance_stubs
|
||||
from oslo.config import cfg
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class NullWriter(object):
|
||||
"""Used to test ImageService.get which takes a writer object."""
|
||||
|
||||
def write(self, *arg, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestGlanceSerializer(test.TestCase):
|
||||
def test_serialize(self):
|
||||
metadata = {'name': 'image1',
|
||||
'is_public': True,
|
||||
'foo': 'bar',
|
||||
'properties': {
|
||||
'prop1': 'propvalue1',
|
||||
'mappings': [
|
||||
{'virtual': 'aaa',
|
||||
'device': 'bbb'},
|
||||
{'virtual': 'xxx',
|
||||
'device': 'yyy'}],
|
||||
'block_device_mapping': [
|
||||
{'virtual_device': 'fake',
|
||||
'device_name': '/dev/fake'},
|
||||
{'virtual_device': 'ephemeral0',
|
||||
'device_name': '/dev/fake0'}]}}
|
||||
|
||||
converted_expected = {
|
||||
'name': 'image1',
|
||||
'is_public': True,
|
||||
'foo': 'bar',
|
||||
'properties': {
|
||||
'prop1': 'propvalue1',
|
||||
'mappings':
|
||||
'[{"device": "bbb", "virtual": "aaa"}, '
|
||||
'{"device": "yyy", "virtual": "xxx"}]',
|
||||
'block_device_mapping':
|
||||
'[{"virtual_device": "fake", "device_name": "/dev/fake"}, '
|
||||
'{"virtual_device": "ephemeral0", '
|
||||
'"device_name": "/dev/fake0"}]'}}
|
||||
converted = glance._convert_to_string(metadata)
|
||||
self.assertEqual(converted, converted_expected)
|
||||
self.assertEqual(glance._convert_from_string(converted), metadata)
|
||||
|
||||
|
||||
class TestGlanceImageService(test.TestCase):
|
||||
"""
|
||||
Tests the Glance image service.
|
||||
|
||||
At a high level, the translations involved are:
|
||||
|
||||
1. Glance -> ImageService - This is needed so we can support
|
||||
multple ImageServices (Glance, Local, etc)
|
||||
|
||||
2. ImageService -> API - This is needed so we can support multple
|
||||
APIs (OpenStack, EC2)
|
||||
|
||||
"""
|
||||
NOW_GLANCE_OLD_FORMAT = "2010-10-11T10:30:22"
|
||||
NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000"
|
||||
|
||||
class tzinfo(datetime.tzinfo):
|
||||
@staticmethod
|
||||
def utcoffset(*args, **kwargs):
|
||||
return datetime.timedelta()
|
||||
|
||||
NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22, tzinfo=tzinfo())
|
||||
|
||||
def setUp(self):
|
||||
super(TestGlanceImageService, self).setUp()
|
||||
#fakes.stub_out_compute_api_snapshot(self.stubs)
|
||||
|
||||
client = glance_stubs.StubGlanceClient()
|
||||
self.service = self._create_image_service(client)
|
||||
self.context = context.RequestContext('fake', 'fake', auth_token=True)
|
||||
self.stubs.Set(glance.time, 'sleep', lambda s: None)
|
||||
|
||||
def _create_image_service(self, client):
|
||||
def _fake_create_glance_client(context, host, port, use_ssl, version):
|
||||
return client
|
||||
|
||||
self.stubs.Set(glance,
|
||||
'_create_glance_client',
|
||||
_fake_create_glance_client)
|
||||
|
||||
client_wrapper = glance.GlanceClientWrapper('fake', 'fake_host', 9292)
|
||||
return glance.GlanceImageService(client=client_wrapper)
|
||||
|
||||
@staticmethod
|
||||
def _make_fixture(**kwargs):
|
||||
fixture = {'name': None,
|
||||
'properties': {},
|
||||
'status': None,
|
||||
'is_public': None}
|
||||
fixture.update(kwargs)
|
||||
return fixture
|
||||
|
||||
def _make_datetime_fixture(self):
|
||||
return self._make_fixture(created_at=self.NOW_GLANCE_FORMAT,
|
||||
updated_at=self.NOW_GLANCE_FORMAT,
|
||||
deleted_at=self.NOW_GLANCE_FORMAT)
|
||||
|
||||
def test_create_with_instance_id(self):
|
||||
"""Ensure instance_id is persisted as an image-property."""
|
||||
fixture = {'name': 'test image',
|
||||
'is_public': False,
|
||||
'properties': {'instance_id': '42', 'user_id': 'fake'}}
|
||||
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
image_meta = self.service.show(self.context, image_id)
|
||||
expected = {
|
||||
'id': image_id,
|
||||
'name': 'test image',
|
||||
'is_public': False,
|
||||
'size': None,
|
||||
'min_disk': None,
|
||||
'min_ram': None,
|
||||
'disk_format': None,
|
||||
'container_format': None,
|
||||
'checksum': None,
|
||||
'created_at': self.NOW_DATETIME,
|
||||
'updated_at': self.NOW_DATETIME,
|
||||
'deleted_at': None,
|
||||
'deleted': None,
|
||||
'status': None,
|
||||
'properties': {'instance_id': '42', 'user_id': 'fake'},
|
||||
'owner': None,
|
||||
}
|
||||
self.assertDictMatch(image_meta, expected)
|
||||
|
||||
image_metas = self.service.detail(self.context)
|
||||
self.assertDictMatch(image_metas[0], expected)
|
||||
|
||||
def test_create_without_instance_id(self):
|
||||
"""
|
||||
Ensure we can create an image without having to specify an
|
||||
instance_id. Public images are an example of an image not tied to an
|
||||
instance.
|
||||
"""
|
||||
fixture = {'name': 'test image', 'is_public': False}
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
|
||||
expected = {
|
||||
'id': image_id,
|
||||
'name': 'test image',
|
||||
'is_public': False,
|
||||
'size': None,
|
||||
'min_disk': None,
|
||||
'min_ram': None,
|
||||
'disk_format': None,
|
||||
'container_format': None,
|
||||
'checksum': None,
|
||||
'created_at': self.NOW_DATETIME,
|
||||
'updated_at': self.NOW_DATETIME,
|
||||
'deleted_at': None,
|
||||
'deleted': None,
|
||||
'status': None,
|
||||
'properties': {},
|
||||
'owner': None,
|
||||
}
|
||||
actual = self.service.show(self.context, image_id)
|
||||
self.assertDictMatch(actual, expected)
|
||||
|
||||
def test_create(self):
|
||||
fixture = self._make_fixture(name='test image')
|
||||
num_images = len(self.service.detail(self.context))
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
|
||||
self.assertNotEqual(None, image_id)
|
||||
self.assertEqual(num_images + 1,
|
||||
len(self.service.detail(self.context)))
|
||||
|
||||
def test_create_and_show_non_existing_image(self):
|
||||
fixture = self._make_fixture(name='test image')
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
|
||||
self.assertNotEqual(None, image_id)
|
||||
self.assertRaises(exception.ImageNotFound,
|
||||
self.service.show,
|
||||
self.context,
|
||||
'bad image id')
|
||||
|
||||
def test_detail_private_image(self):
|
||||
fixture = self._make_fixture(name='test image')
|
||||
fixture['is_public'] = False
|
||||
properties = {'owner_id': 'proj1'}
|
||||
fixture['properties'] = properties
|
||||
|
||||
self.service.create(self.context, fixture)['id']
|
||||
|
||||
proj = self.context.project_id
|
||||
self.context.project_id = 'proj1'
|
||||
|
||||
image_metas = self.service.detail(self.context)
|
||||
|
||||
self.context.project_id = proj
|
||||
|
||||
self.assertEqual(1, len(image_metas))
|
||||
self.assertEqual(image_metas[0]['name'], 'test image')
|
||||
self.assertEqual(image_metas[0]['is_public'], False)
|
||||
|
||||
def test_detail_marker(self):
|
||||
fixtures = []
|
||||
ids = []
|
||||
for i in range(10):
|
||||
fixture = self._make_fixture(name='TestImage %d' % (i))
|
||||
fixtures.append(fixture)
|
||||
ids.append(self.service.create(self.context, fixture)['id'])
|
||||
|
||||
image_metas = self.service.detail(self.context, marker=ids[1])
|
||||
self.assertEqual(len(image_metas), 8)
|
||||
i = 2
|
||||
for meta in image_metas:
|
||||
expected = {
|
||||
'id': ids[i],
|
||||
'status': None,
|
||||
'is_public': None,
|
||||
'name': 'TestImage %d' % (i),
|
||||
'properties': {},
|
||||
'size': None,
|
||||
'min_disk': None,
|
||||
'min_ram': None,
|
||||
'disk_format': None,
|
||||
'container_format': None,
|
||||
'checksum': None,
|
||||
'created_at': self.NOW_DATETIME,
|
||||
'updated_at': self.NOW_DATETIME,
|
||||
'deleted_at': None,
|
||||
'deleted': None,
|
||||
'owner': None,
|
||||
}
|
||||
|
||||
self.assertDictMatch(meta, expected)
|
||||
i = i + 1
|
||||
|
||||
def test_detail_limit(self):
|
||||
fixtures = []
|
||||
ids = []
|
||||
for i in range(10):
|
||||
fixture = self._make_fixture(name='TestImage %d' % (i))
|
||||
fixtures.append(fixture)
|
||||
ids.append(self.service.create(self.context, fixture)['id'])
|
||||
|
||||
image_metas = self.service.detail(self.context, limit=5)
|
||||
self.assertEqual(len(image_metas), 5)
|
||||
|
||||
def test_detail_default_limit(self):
|
||||
fixtures = []
|
||||
ids = []
|
||||
for i in range(10):
|
||||
fixture = self._make_fixture(name='TestImage %d' % (i))
|
||||
fixtures.append(fixture)
|
||||
ids.append(self.service.create(self.context, fixture)['id'])
|
||||
|
||||
image_metas = self.service.detail(self.context)
|
||||
for i, meta in enumerate(image_metas):
|
||||
self.assertEqual(meta['name'], 'TestImage %d' % (i))
|
||||
|
||||
def test_detail_marker_and_limit(self):
|
||||
fixtures = []
|
||||
ids = []
|
||||
for i in range(10):
|
||||
fixture = self._make_fixture(name='TestImage %d' % (i))
|
||||
fixtures.append(fixture)
|
||||
ids.append(self.service.create(self.context, fixture)['id'])
|
||||
|
||||
image_metas = self.service.detail(self.context, marker=ids[3], limit=5)
|
||||
self.assertEqual(len(image_metas), 5)
|
||||
i = 4
|
||||
for meta in image_metas:
|
||||
expected = {
|
||||
'id': ids[i],
|
||||
'status': None,
|
||||
'is_public': None,
|
||||
'name': 'TestImage %d' % (i),
|
||||
'properties': {},
|
||||
'size': None,
|
||||
'min_disk': None,
|
||||
'min_ram': None,
|
||||
'disk_format': None,
|
||||
'container_format': None,
|
||||
'checksum': None,
|
||||
'created_at': self.NOW_DATETIME,
|
||||
'updated_at': self.NOW_DATETIME,
|
||||
'deleted_at': None,
|
||||
'deleted': None,
|
||||
'owner': None,
|
||||
}
|
||||
self.assertDictMatch(meta, expected)
|
||||
i = i + 1
|
||||
|
||||
def test_detail_invalid_marker(self):
|
||||
fixtures = []
|
||||
ids = []
|
||||
for i in range(10):
|
||||
fixture = self._make_fixture(name='TestImage %d' % (i))
|
||||
fixtures.append(fixture)
|
||||
ids.append(self.service.create(self.context, fixture)['id'])
|
||||
|
||||
self.assertRaises(exception.Invalid, self.service.detail,
|
||||
self.context, marker='invalidmarker')
|
||||
|
||||
def test_update(self):
|
||||
fixture = self._make_fixture(name='test image')
|
||||
image = self.service.create(self.context, fixture)
|
||||
print image
|
||||
image_id = image['id']
|
||||
fixture['name'] = 'new image name'
|
||||
self.service.update(self.context, image_id, fixture)
|
||||
|
||||
new_image_data = self.service.show(self.context, image_id)
|
||||
self.assertEqual('new image name', new_image_data['name'])
|
||||
|
||||
def test_delete(self):
|
||||
fixture1 = self._make_fixture(name='test image 1')
|
||||
fixture2 = self._make_fixture(name='test image 2')
|
||||
fixtures = [fixture1, fixture2]
|
||||
|
||||
num_images = len(self.service.detail(self.context))
|
||||
self.assertEqual(0, num_images)
|
||||
|
||||
ids = []
|
||||
for fixture in fixtures:
|
||||
new_id = self.service.create(self.context, fixture)['id']
|
||||
ids.append(new_id)
|
||||
|
||||
num_images = len(self.service.detail(self.context))
|
||||
self.assertEqual(2, num_images)
|
||||
|
||||
self.service.delete(self.context, ids[0])
|
||||
|
||||
num_images = len(self.service.detail(self.context))
|
||||
self.assertEqual(1, num_images)
|
||||
|
||||
def test_show_passes_through_to_client(self):
|
||||
fixture = self._make_fixture(name='image1', is_public=True)
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
|
||||
image_meta = self.service.show(self.context, image_id)
|
||||
expected = {
|
||||
'id': image_id,
|
||||
'name': 'image1',
|
||||
'is_public': True,
|
||||
'size': None,
|
||||
'min_disk': None,
|
||||
'min_ram': None,
|
||||
'disk_format': None,
|
||||
'container_format': None,
|
||||
'checksum': None,
|
||||
'created_at': self.NOW_DATETIME,
|
||||
'updated_at': self.NOW_DATETIME,
|
||||
'deleted_at': None,
|
||||
'deleted': None,
|
||||
'status': None,
|
||||
'properties': {},
|
||||
'owner': None,
|
||||
}
|
||||
self.assertEqual(image_meta, expected)
|
||||
|
||||
def test_show_raises_when_no_authtoken_in_the_context(self):
|
||||
fixture = self._make_fixture(name='image1',
|
||||
is_public=False,
|
||||
properties={'one': 'two'})
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
self.context.auth_token = False
|
||||
self.assertRaises(exception.ImageNotFound,
|
||||
self.service.show,
|
||||
self.context,
|
||||
image_id)
|
||||
|
||||
def test_detail_passes_through_to_client(self):
|
||||
fixture = self._make_fixture(name='image10', is_public=True)
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
image_metas = self.service.detail(self.context)
|
||||
expected = [
|
||||
{
|
||||
'id': image_id,
|
||||
'name': 'image10',
|
||||
'is_public': True,
|
||||
'size': None,
|
||||
'min_disk': None,
|
||||
'min_ram': None,
|
||||
'disk_format': None,
|
||||
'container_format': None,
|
||||
'checksum': None,
|
||||
'created_at': self.NOW_DATETIME,
|
||||
'updated_at': self.NOW_DATETIME,
|
||||
'deleted_at': None,
|
||||
'deleted': None,
|
||||
'status': None,
|
||||
'properties': {},
|
||||
'owner': None,
|
||||
},
|
||||
]
|
||||
self.assertEqual(image_metas, expected)
|
||||
|
||||
def test_show_makes_datetimes(self):
|
||||
fixture = self._make_datetime_fixture()
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
image_meta = self.service.show(self.context, image_id)
|
||||
self.assertEqual(image_meta['created_at'], self.NOW_DATETIME)
|
||||
self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME)
|
||||
|
||||
def test_detail_makes_datetimes(self):
|
||||
fixture = self._make_datetime_fixture()
|
||||
self.service.create(self.context, fixture)
|
||||
image_meta = self.service.detail(self.context)[0]
|
||||
self.assertEqual(image_meta['created_at'], self.NOW_DATETIME)
|
||||
self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME)
|
||||
|
||||
def test_download_with_retries(self):
|
||||
tries = [0]
|
||||
|
||||
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
||||
"""A client that fails the first time, then succeeds."""
|
||||
def get(self, image_id):
|
||||
if tries[0] == 0:
|
||||
tries[0] = 1
|
||||
raise glanceclient.exc.ServiceUnavailable('')
|
||||
else:
|
||||
return {}
|
||||
|
||||
client = MyGlanceStubClient()
|
||||
service = self._create_image_service(client)
|
||||
image_id = 1 # doesn't matter
|
||||
writer = NullWriter()
|
||||
|
||||
# When retries are disabled, we should get an exception
|
||||
self.flags(glance_num_retries=0)
|
||||
self.assertRaises(exception.GlanceConnectionFailed,
|
||||
service.download,
|
||||
self.context,
|
||||
image_id,
|
||||
writer)
|
||||
|
||||
# Now lets enable retries. No exception should happen now.
|
||||
tries = [0]
|
||||
self.flags(glance_num_retries=1)
|
||||
service.download(self.context, image_id, writer)
|
||||
|
||||
def test_client_forbidden_converts_to_imagenotauthed(self):
|
||||
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
||||
"""A client that raises a Forbidden exception."""
|
||||
def get(self, image_id):
|
||||
raise glanceclient.exc.Forbidden(image_id)
|
||||
|
||||
client = MyGlanceStubClient()
|
||||
service = self._create_image_service(client)
|
||||
image_id = 1 # doesn't matter
|
||||
writer = NullWriter()
|
||||
self.assertRaises(exception.ImageNotAuthorized, service.download,
|
||||
self.context, image_id, writer)
|
||||
|
||||
def test_client_httpforbidden_converts_to_imagenotauthed(self):
|
||||
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
||||
"""A client that raises a HTTPForbidden exception."""
|
||||
def get(self, image_id):
|
||||
raise glanceclient.exc.HTTPForbidden(image_id)
|
||||
|
||||
client = MyGlanceStubClient()
|
||||
service = self._create_image_service(client)
|
||||
image_id = 1 # doesn't matter
|
||||
writer = NullWriter()
|
||||
self.assertRaises(exception.ImageNotAuthorized, service.download,
|
||||
self.context, image_id, writer)
|
||||
|
||||
def test_client_notfound_converts_to_imagenotfound(self):
|
||||
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
||||
"""A client that raises a NotFound exception."""
|
||||
def get(self, image_id):
|
||||
raise glanceclient.exc.NotFound(image_id)
|
||||
|
||||
client = MyGlanceStubClient()
|
||||
service = self._create_image_service(client)
|
||||
image_id = 1 # doesn't matter
|
||||
writer = NullWriter()
|
||||
self.assertRaises(exception.ImageNotFound, service.download,
|
||||
self.context, image_id, writer)
|
||||
|
||||
def test_client_httpnotfound_converts_to_imagenotfound(self):
|
||||
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
||||
"""A client that raises a HTTPNotFound exception."""
|
||||
def get(self, image_id):
|
||||
raise glanceclient.exc.HTTPNotFound(image_id)
|
||||
|
||||
client = MyGlanceStubClient()
|
||||
service = self._create_image_service(client)
|
||||
image_id = 1 # doesn't matter
|
||||
writer = NullWriter()
|
||||
self.assertRaises(exception.ImageNotFound, service.download,
|
||||
self.context, image_id, writer)
|
||||
|
||||
def test_glance_client_image_id(self):
|
||||
fixture = self._make_fixture(name='test image')
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
(service, same_id) = glance.get_remote_image_service(self.context,
|
||||
image_id)
|
||||
self.assertEqual(same_id, image_id)
|
||||
|
||||
def test_glance_client_image_ref(self):
|
||||
fixture = self._make_fixture(name='test image')
|
||||
image_id = self.service.create(self.context, fixture)['id']
|
||||
image_url = 'http://something-less-likely/%s' % image_id
|
||||
(service, same_id) = glance.get_remote_image_service(self.context,
|
||||
image_url)
|
||||
self.assertEqual(same_id, image_id)
|
||||
self.assertEqual(service._client.host,
|
||||
'something-less-likely')
|
||||
|
||||
|
||||
class TestGlanceClientVersion(test.TestCase):
|
||||
"""Tests the version of the glance client generated"""
|
||||
def setUp(self):
|
||||
super(TestGlanceClientVersion, self).setUp()
|
||||
|
||||
def fake_get_image_model(self):
|
||||
return
|
||||
|
||||
self.stubs.Set(glanceclient_v2, '_get_image_model',
|
||||
fake_get_image_model)
|
||||
self.stubs.Set(glanceclient_v2, '_get_member_model',
|
||||
fake_get_image_model)
|
||||
|
||||
def test_glance_version_by_flag(self):
|
||||
"""Test glance version set by flag is honoured"""
|
||||
client_wrapper_v1 = glance.GlanceClientWrapper('fake', 'fake_host',
|
||||
9292)
|
||||
self.assertEqual(client_wrapper_v1.client.__module__,
|
||||
'glanceclient.v1.client')
|
||||
self.flags(glance_api_version=2)
|
||||
client_wrapper_v2 = glance.GlanceClientWrapper('fake', 'fake_host',
|
||||
9292)
|
||||
self.assertEqual(client_wrapper_v2.client.__module__,
|
||||
'glanceclient.v2.client')
|
||||
CONF.reset()
|
||||
|
||||
def test_glance_version_by_arg(self):
|
||||
"""Test glance version set by arg to GlanceClientWrapper"""
|
||||
client_wrapper_v1 = glance.GlanceClientWrapper('fake', 'fake_host',
|
||||
9292, version=1)
|
||||
self.assertEqual(client_wrapper_v1.client.__module__,
|
||||
'glanceclient.v1.client')
|
||||
client_wrapper_v2 = glance.GlanceClientWrapper('fake', 'fake_host',
|
||||
9292, version=2)
|
||||
self.assertEqual(client_wrapper_v2.client.__module__,
|
||||
'glanceclient.v2.client')
|
||||
|
||||
|
||||
def _create_failing_glance_client(info):
|
||||
class MyGlanceStubClient(glance_stubs.StubGlanceClient):
|
||||
"""A client that fails the first time, then succeeds."""
|
||||
def get(self, image_id):
|
||||
info['num_calls'] += 1
|
||||
if info['num_calls'] == 1:
|
||||
raise glanceclient.exc.ServiceUnavailable('')
|
||||
return {}
|
||||
|
||||
return MyGlanceStubClient()
|
@ -14,7 +14,6 @@ Paste
|
||||
PasteDeploy>=1.5.0
|
||||
posix_ipc
|
||||
python-neutronclient>=2.3.0,<3
|
||||
python-glanceclient>=0.9.0
|
||||
python-keystoneclient>=0.3.2
|
||||
Routes>=1.12.3
|
||||
six>=1.6.0
|
||||
|
Loading…
Reference in New Issue
Block a user