diff --git a/manila/exception.py b/manila/exception.py index de3a180776..ea1f4600e1 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -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") diff --git a/manila/image/__init__.py b/manila/image/__init__.py deleted file mode 100644 index 7affa08d4c..0000000000 --- a/manila/image/__init__.py +++ /dev/null @@ -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. diff --git a/manila/image/glance.py b/manila/image/glance.py deleted file mode 100644 index 3e6a5b1e1a..0000000000 --- a/manila/image/glance.py +++ /dev/null @@ -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() diff --git a/manila/image/image_utils.py b/manila/image/image_utils.py deleted file mode 100644 index a8d0d5346c..0000000000 --- a/manila/image/image_utils.py +++ /dev/null @@ -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) diff --git a/manila/tests/glance/__init__.py b/manila/tests/glance/__init__.py deleted file mode 100644 index ef9fa05a7c..0000000000 --- a/manila/tests/glance/__init__.py +++ /dev/null @@ -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 -================================= -""" diff --git a/manila/tests/glance/stubs.py b/manila/tests/glance/stubs.py deleted file mode 100644 index 076afeffc9..0000000000 --- a/manila/tests/glance/stubs.py +++ /dev/null @@ -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) diff --git a/manila/tests/image/__init__.py b/manila/tests/image/__init__.py deleted file mode 100644 index a079349176..0000000000 --- a/manila/tests/image/__init__.py +++ /dev/null @@ -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 * diff --git a/manila/tests/image/test_glance.py b/manila/tests/image/test_glance.py deleted file mode 100644 index 919bb2df90..0000000000 --- a/manila/tests/image/test_glance.py +++ /dev/null @@ -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() diff --git a/requirements.txt b/requirements.txt index 6917050c79..305a4ca1bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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