mogan/mogan/image/glance.py

260 lines
9.2 KiB
Python

# Copyright 2010 OpenStack Foundation
# 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."""
import copy
import inspect
import itertools
import random
import sys
import time
import glanceclient
import glanceclient.exc
from glanceclient.v2 import schemas
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import timeutils
import six
from six.moves import range
from mogan.common import exception
from mogan import conf
LOG = logging.getLogger(__name__)
CONF = conf.CONF
def generate_identity_headers(context, status='Confirmed'):
return {
'X-Auth-Token': getattr(context, 'auth_token', None),
'X-User-Id': getattr(context, 'user', None),
'X-Tenant-Id': getattr(context, 'tenant', None),
'X-Roles': ','.join(getattr(context, 'roles', [])),
'X-Identity-Status': status,
}
def _glanceclient_from_endpoint(context, endpoint, version=2):
"""Instantiate a new glanceclient.Client object."""
params = {}
# NOTE(sdague): even if we aren't using keystone, it doesn't
# hurt to send these headers.
params['identity_headers'] = generate_identity_headers(context)
if endpoint.startswith('https://'):
# https specific params
params['insecure'] = CONF.glance.glance_api_insecure
params['ssl_compression'] = False
if CONF.glance.glance_cafile:
params['cacert'] = CONF.glance.glance_cafile
return glanceclient.Client(str(version), endpoint, **params)
def get_api_servers():
"""Shuffle a list of CONF.glance.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.glance_api_servers:
if '//' not in api_server:
api_server = 'http://' + api_server
api_servers.append(api_server)
random.shuffle(api_servers)
return itertools.cycle(api_servers)
class GlanceClientWrapper(object):
"""Glance client wrapper class that implements retries."""
def __init__(self, context=None, endpoint=None):
if endpoint is not None:
self.client = self._create_static_client(context,
endpoint,
2)
else:
self.client = None
self.api_servers = None
def _create_static_client(self, context, endpoint, version):
"""Create a client that we'll use for every call."""
self.api_server = str(endpoint)
return _glanceclient_from_endpoint(context, endpoint, 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.api_server = next(self.api_servers)
return _glanceclient_from_endpoint(context, self.api_server, version)
def call(self, context, version, method, *args, **kwargs):
"""Call a glance client method. If we get a connection error,
retry the request according to CONF.glance.glance_num_retries.
"""
retry_excs = (glanceclient.exc.ServiceUnavailable,
glanceclient.exc.InvalidEndpoint,
glanceclient.exc.CommunicationError)
num_attempts = 1 + CONF.glance.glance_num_retries
for attempt in range(1, num_attempts + 1):
client = self.client or self._create_onetime_client(context,
version)
try:
controller = getattr(client,
kwargs.pop('controller', 'images'))
result = getattr(controller, method)(*args, **kwargs)
if inspect.isgenerator(result):
# Convert generator results to a list, so that we can
# catch any potential exceptions now and retry the call.
return list(result)
return result
except retry_excs as e:
if attempt < num_attempts:
extra = "retrying"
else:
extra = 'done trying'
LOG.exception("Error contacting glance server "
"'%(server)s' for '%(method)s', "
"%(extra)s.",
{'server': self.api_server,
'method': method, 'extra': extra})
if attempt == num_attempts:
raise exception.GlanceConnectionFailed(
server=str(self.api_server), reason=six.text_type(e))
time.sleep(1)
class GlanceImageServiceV2(object):
"""Provides storage and retrieval of disk image objects within Glance."""
def __init__(self, client=None):
self._client = client or GlanceClientWrapper()
def show(self, context, image_id):
"""Returns a dict with image data for the given opaque image id.
:param context: The context object to pass to image client
:param image_id: The UUID of the image
"""
try:
image = self._client.call(context, 2, 'get', image_id)
except Exception:
_reraise_translated_image_exception(image_id)
image = _translate_from_glance(image)
return image
def _translate_from_glance(image, include_locations=False):
image_meta = _extract_attributes(
image, include_locations=include_locations)
image_meta = _convert_timestamps_to_datetimes(image_meta)
image_meta = _convert_from_string(image_meta)
return image_meta
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, six.string_types):
properties[attr] = jsonutils.loads(prop)
def _json_dumps(properties, attr):
prop = properties[attr]
if not isinstance(prop, six.string_types):
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 _extract_attributes(image, include_locations=False):
include_locations_attrs = ['direct_url', 'locations']
omit_attrs = ['self', 'schema', 'protected', 'virtual_size', 'file',
'tags']
raw_schema = image.schema
schema = schemas.Schema(raw_schema)
output = {'properties': {}, 'deleted': False, 'deleted_at': None,
'disk_format': None, 'container_format': None, 'name': None,
'checksum': None}
for name, value in image.items():
if (name in omit_attrs
or name in include_locations_attrs and not include_locations):
continue
elif name == 'visibility':
output['is_public'] = value == 'public'
elif name == 'size' and value is None:
output['size'] = 0
elif schema.is_base_property(name):
output[name] = value
else:
output['properties'][name] = value
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)
six.reraise(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.ImageBadRequest(image_id=image_id,
response=six.text_type(exc_value))
return exc_value
def get_image_service(context):
"""Create an image_service."""
return GlanceImageServiceV2()