Source code for ironic.common.glance_service.base_image_service

# Copyright 2010 OpenStack Foundation
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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 os
import sys
import time

from glanceclient import client
from glanceclient import exc as glance_exc
from oslo_config import cfg
from oslo_log import log
import sendfile
import six
import six.moves.urllib.parse as urlparse

from ironic.common import exception
from ironic.common.glance_service import service_utils
from ironic.common.i18n import _LE


LOG = log.getLogger(__name__)
CONF = cfg.CONF


def _translate_image_exception(image_id, exc_value):
    if isinstance(exc_value, (glance_exc.Forbidden,
                              glance_exc.Unauthorized)):
        return exception.ImageNotAuthorized(image_id=image_id)
    if isinstance(exc_value, glance_exc.NotFound):
        return exception.ImageNotFound(image_id=image_id)
    if isinstance(exc_value, glance_exc.BadRequest):
        return exception.Invalid(exc_value)
    return exc_value


def _translate_plain_exception(exc_value):
    if isinstance(exc_value, (glance_exc.Forbidden,
                              glance_exc.Unauthorized)):
        return exception.NotAuthorized(exc_value)
    if isinstance(exc_value, glance_exc.NotFound):
        return exception.NotFound(exc_value)
    if isinstance(exc_value, glance_exc.BadRequest):
        return exception.Invalid(exc_value)
    return exc_value


def check_image_service(func):
    """Creates a glance client if doesn't exists and calls the function."""
    @six.wraps(func)
    def wrapper(self, *args, **kwargs):
        """Wrapper around methods calls.

        :param image_href: href that describes the location of an image
        """

        if self.client:
            return func(self, *args, **kwargs)

        image_href = kwargs.get('image_href')
        (image_id, self.glance_host,
         self.glance_port, use_ssl) = service_utils.parse_image_ref(image_href)

        if use_ssl:
            scheme = 'https'
        else:
            scheme = 'http'
        params = {}
        params['insecure'] = CONF.glance.glance_api_insecure
        if (not params['insecure'] and CONF.glance.glance_cafile
                and use_ssl):
            params['cacert'] = CONF.glance.glance_cafile
        if CONF.glance.auth_strategy == 'keystone':
            params['token'] = self.context.auth_token
        endpoint = '%s://%s:%s' % (scheme, self.glance_host, self.glance_port)
        self.client = client.Client(self.version,
                                    endpoint, **params)
        return func(self, *args, **kwargs)
    return wrapper


[docs]class BaseImageService(object): def __init__(self, client=None, version=1, context=None): self.client = client self.version = version self.context = context
[docs] def call(self, method, *args, **kwargs): """Call a glance client method. If we get a connection error, retry the request according to CONF.glance_num_retries. :param context: The request context, for access checks. :param version: The requested API version.v :param method: The method requested to be called. :param args: A list of positional arguments for the method called :param kwargs: A dict of keyword arguments for the method called :raises: GlanceConnectionFailed """ retry_excs = (glance_exc.ServiceUnavailable, glance_exc.InvalidEndpoint, glance_exc.CommunicationError) image_excs = (glance_exc.Forbidden, glance_exc.Unauthorized, glance_exc.NotFound, glance_exc.BadRequest) num_attempts = 1 + CONF.glance.glance_num_retries for attempt in range(1, num_attempts + 1): try: return getattr(self.client.images, method)(*args, **kwargs) except retry_excs as e: host = self.glance_host port = self.glance_port error_msg = _LE("Error contacting glance server " "'%(host)s:%(port)s' for '%(method)s', attempt" " %(attempt)s of %(num_attempts)s failed.") LOG.exception(error_msg, {'host': host, 'port': port, 'num_attempts': num_attempts, 'attempt': attempt, 'method': method}) if attempt == num_attempts: raise exception.GlanceConnectionFailed(host=host, port=port, reason=str(e)) time.sleep(1) except image_excs as e: exc_type, exc_value, exc_trace = sys.exc_info() if method == 'list': new_exc = _translate_plain_exception( exc_value) else: new_exc = _translate_image_exception( args[0], exc_value) six.reraise(type(new_exc), new_exc, exc_trace)
@check_image_service def _detail(self, method='list', **kwargs): """Calls out to Glance for a list of detailed image information. :returns: A list of dicts containing image metadata. """ LOG.debug("Getting a full list of images metadata from glance.") params = service_utils.extract_query_params(kwargs, self.version) images = self.call(method, **params) _images = [] for image in images: if service_utils.is_image_available(self.context, image): _images.append(service_utils.translate_from_glance(image)) return _images @check_image_service def _show(self, image_href, method='get'): """Returns a dict with image data for the given opaque image id. :param image_id: The opaque image identifier. :returns: A dict containing image metadata. :raises: ImageNotFound """ LOG.debug("Getting image metadata from glance. Image: %s", image_href) (image_id, self.glance_host, self.glance_port, use_ssl) = service_utils.parse_image_ref(image_href) image = self.call(method, image_id) if not service_utils.is_image_available(self.context, image): raise exception.ImageNotFound(image_id=image_id) base_image_meta = service_utils.translate_from_glance(image) return base_image_meta @check_image_service def _download(self, image_id, data=None, method='data'): """Calls out to Glance for data and writes data. :param image_id: The opaque image identifier. :param data: (Optional) File object to write data to. """ (image_id, self.glance_host, self.glance_port, use_ssl) = service_utils.parse_image_ref(image_id) if (self.version == 2 and 'file' in CONF.glance.allowed_direct_url_schemes): location = self._get_location(image_id) url = urlparse.urlparse(location) if url.scheme == "file": with open(url.path, "r") as f: filesize = os.path.getsize(f.name) sendfile.sendfile(data.fileno(), f.fileno(), 0, filesize) return image_chunks = self.call(method, image_id) if data is None: return image_chunks else: for chunk in image_chunks: data.write(chunk) @check_image_service def _create(self, image_meta, data=None, method='create'): """Store the image data and return the new image object. :param image_meta: A dict containing image metadata :param data: (Optional) File object to create image from. :returns: dict -- New created image metadata """ sent_service_image_meta = service_utils.translate_to_glance(image_meta) # TODO(ghe): Allow copy-from or location headers Bug #1199532 if data: sent_service_image_meta['data'] = data recv_service_image_meta = self.call(method, **sent_service_image_meta) return service_utils.translate_from_glance(recv_service_image_meta) @check_image_service def _update(self, image_id, image_meta, data=None, method='update', purge_props=False): """Modify the given image with the new data. :param image_id: The opaque image identifier. :param data: (Optional) File object to update data from. :param purge_props: (Optional=False) Purge existing properties. :returns: dict -- New created image metadata """ (image_id, self.glance_host, self.glance_port, use_ssl) = service_utils.parse_image_ref(image_id) if image_meta: image_meta = service_utils.translate_to_glance(image_meta) else: image_meta = {} if self.version == 1: image_meta['purge_props'] = purge_props if data: image_meta['data'] = data # 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) image_meta = self.call(method, image_id, **image_meta) if self.version == 2 and data: self.call('upload', image_id, data) image_meta = self._show(image_id) return image_meta @check_image_service def _delete(self, image_id, method='delete'): """Delete the given image. :param image_id: The opaque image identifier. :raises: ImageNotFound if the image does not exist. :raises: NotAuthorized if the user is not an owner. :raises: ImageNotAuthorized if the user is not authorized. """ (image_id, glance_host, glance_port, use_ssl) = service_utils.parse_image_ref(image_id) self.call(method, image_id)