~ubuntu-branches/ubuntu/saucy/nova/saucy-proposed

« back to all changes in this revision

Viewing changes to .pc/upstream/0001-fix-bug-where-nova-ignores-glance-host-in-imageref.patch/nova/image/glance.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2012-05-24 13:12:53 UTC
  • mfrom: (1.1.55)
  • Revision ID: package-import@ubuntu.com-20120524131253-ommql08fg1en06ut
Tags: 2012.2~f1-0ubuntu1
* New upstream release.
* Prepare for quantal:
  - Dropped debian/patches/upstream/0006-Use-project_id-in-ec2.cloud._format_image.patch
  - Dropped debian/patches/upstream/0005-Populate-image-properties-with-project_id-again.patch
  - Dropped debian/patches/upstream/0004-Fixed-bug-962840-added-a-test-case.patch
  - Dropped debian/patches/upstream/0003-Allow-unprivileged-RADOS-users-to-access-rbd-volumes.patch
  - Dropped debian/patches/upstream/0002-Stop-libvirt-test-from-deleting-instances-dir.patch
  - Dropped debian/patches/upstream/0001-fix-bug-where-nova-ignores-glance-host-in-imageref.patch 
  - Dropped debian/patches/0001-fix-useexisting-deprecation-warnings.patch
* debian/control: Add python-keystone as a dependency. (LP: #907197)
* debian/patches/kombu_tests_timeout.patch: Refreshed.
* debian/nova.conf, debian/nova-common.postinst: Convert to new ini
  file configuration
* debian/patches/nova-manage_flagfile_location.patch: Refreshed

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
 
 
3
 
# Copyright 2010 OpenStack LLC.
4
 
# All Rights Reserved.
5
 
#
6
 
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
7
 
#    not use this file except in compliance with the License. You may obtain
8
 
#    a copy of the License at
9
 
#
10
 
#         http://www.apache.org/licenses/LICENSE-2.0
11
 
#
12
 
#    Unless required by applicable law or agreed to in writing, software
13
 
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
 
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
 
#    License for the specific language governing permissions and limitations
16
 
#    under the License.
17
 
 
18
 
"""Implementation of an image service that uses Glance as the backend"""
19
 
 
20
 
from __future__ import absolute_import
21
 
 
22
 
import copy
23
 
import datetime
24
 
import json
25
 
import random
26
 
import sys
27
 
import time
28
 
import urlparse
29
 
 
30
 
from glance.common import exception as glance_exception
31
 
 
32
 
from nova import exception
33
 
from nova import flags
34
 
from nova import log as logging
35
 
from nova import utils
36
 
 
37
 
 
38
 
LOG = logging.getLogger(__name__)
39
 
 
40
 
 
41
 
FLAGS = flags.FLAGS
42
 
 
43
 
 
44
 
GlanceClient = utils.import_class('glance.client.Client')
45
 
 
46
 
 
47
 
def _parse_image_ref(image_href):
48
 
    """Parse an image href into composite parts.
49
 
 
50
 
    :param image_href: href of an image
51
 
    :returns: a tuple of the form (image_id, host, port)
52
 
    :raises ValueError
53
 
 
54
 
    """
55
 
    o = urlparse.urlparse(image_href)
56
 
    port = o.port or 80
57
 
    host = o.netloc.split(':', 1)[0]
58
 
    image_id = o.path.split('/')[-1]
59
 
    return (image_id, host, port)
60
 
 
61
 
 
62
 
def _create_glance_client(context, host, port):
63
 
    if FLAGS.auth_strategy == 'keystone':
64
 
        # NOTE(dprince): Glance client just needs auth_tok right? Should we
65
 
        # add username and tenant to the creds below?
66
 
        creds = {'strategy': 'keystone',
67
 
                 'username': context.user_id,
68
 
                 'tenant': context.project_id}
69
 
        glance_client = GlanceClient(host, port, auth_tok=context.auth_token,
70
 
                                     creds=creds)
71
 
    else:
72
 
        glance_client = GlanceClient(host, port)
73
 
    return glance_client
74
 
 
75
 
 
76
 
def pick_glance_api_server():
77
 
    """Return which Glance API server to use for the request
78
 
 
79
 
    This method provides a very primitive form of load-balancing suitable for
80
 
    testing and sandbox environments. In production, it would be better to use
81
 
    one IP and route that to a real load-balancer.
82
 
 
83
 
        Returns (host, port)
84
 
    """
85
 
    host_port = random.choice(FLAGS.glance_api_servers)
86
 
    host, port_str = host_port.split(':')
87
 
    port = int(port_str)
88
 
    return host, port
89
 
 
90
 
 
91
 
def get_glance_client(context, image_href):
92
 
    """Get the correct glance client and id for the given image_href.
93
 
 
94
 
    The image_href param can be an href of the form
95
 
    http://myglanceserver:9292/images/42, or just an int such as 42. If the
96
 
    image_href is an int, then flags are used to create the default
97
 
    glance client.
98
 
 
99
 
    :param image_href: image ref/id for an image
100
 
    :returns: a tuple of the form (glance_client, image_id)
101
 
 
102
 
    """
103
 
    glance_host, glance_port = pick_glance_api_server()
104
 
 
105
 
    # check if this is an id
106
 
    if '/' not in str(image_href):
107
 
        glance_client = _create_glance_client(context,
108
 
                                              glance_host,
109
 
                                              glance_port)
110
 
        return (glance_client, image_href)
111
 
 
112
 
    else:
113
 
        try:
114
 
            (image_id, host, port) = _parse_image_ref(image_href)
115
 
        except ValueError:
116
 
            raise exception.InvalidImageRef(image_href=image_href)
117
 
 
118
 
        glance_client = _create_glance_client(context,
119
 
                                              glance_host,
120
 
                                              glance_port)
121
 
        return (glance_client, image_id)
122
 
 
123
 
 
124
 
class GlanceImageService(object):
125
 
    """Provides storage and retrieval of disk image objects within Glance."""
126
 
 
127
 
    def __init__(self, client=None):
128
 
        self._client = client
129
 
 
130
 
    def _get_client(self, context):
131
 
        # NOTE(sirp): we want to load balance each request across glance
132
 
        # servers. Since GlanceImageService is a long-lived object, `client`
133
 
        # is made to choose a new server each time via this property.
134
 
        if self._client is not None:
135
 
            return self._client
136
 
        glance_host, glance_port = pick_glance_api_server()
137
 
        return _create_glance_client(context, glance_host, glance_port)
138
 
 
139
 
    def _call_retry(self, context, name, *args, **kwargs):
140
 
        """Retry call to glance server if there is a connection error.
141
 
        Suitable only for idempotent calls."""
142
 
        for i in xrange(FLAGS.glance_num_retries + 1):
143
 
            client = self._get_client(context)
144
 
            try:
145
 
                return getattr(client, name)(*args, **kwargs)
146
 
            except glance_exception.ClientConnectionError as e:
147
 
                LOG.exception(_('Connection error contacting glance'
148
 
                                ' server, retrying'))
149
 
 
150
 
                time.sleep(1)
151
 
 
152
 
        raise exception.GlanceConnectionFailed(
153
 
                reason=_('Maximum attempts reached'))
154
 
 
155
 
    def index(self, context, **kwargs):
156
 
        """Calls out to Glance for a list of images available."""
157
 
        params = self._extract_query_params(kwargs)
158
 
        image_metas = self._get_images(context, **params)
159
 
 
160
 
        images = []
161
 
        for image_meta in image_metas:
162
 
            # NOTE(sirp): We need to use `get_images_detailed` and not
163
 
            # `get_images` here because we need `is_public` and `properties`
164
 
            # included so we can filter by user
165
 
            if self._is_image_available(context, image_meta):
166
 
                meta_subset = utils.subset_dict(image_meta, ('id', 'name'))
167
 
                images.append(meta_subset)
168
 
        return images
169
 
 
170
 
    def detail(self, context, **kwargs):
171
 
        """Calls out to Glance for a list of detailed image information."""
172
 
        params = self._extract_query_params(kwargs)
173
 
        image_metas = self._get_images(context, **params)
174
 
 
175
 
        images = []
176
 
        for image_meta in image_metas:
177
 
            if self._is_image_available(context, image_meta):
178
 
                base_image_meta = self._translate_from_glance(image_meta)
179
 
                images.append(base_image_meta)
180
 
        return images
181
 
 
182
 
    def _extract_query_params(self, params):
183
 
        _params = {}
184
 
        accepted_params = ('filters', 'marker', 'limit',
185
 
                           'sort_key', 'sort_dir')
186
 
        for param in accepted_params:
187
 
            if param in params:
188
 
                _params[param] = params.get(param)
189
 
 
190
 
        return _params
191
 
 
192
 
    def _get_images(self, context, **kwargs):
193
 
        """Get image entitites from images service"""
194
 
 
195
 
        # ensure filters is a dict
196
 
        kwargs['filters'] = kwargs.get('filters') or {}
197
 
        # NOTE(vish): don't filter out private images
198
 
        kwargs['filters'].setdefault('is_public', 'none')
199
 
 
200
 
        client = self._get_client(context)
201
 
        return self._fetch_images(client.get_images_detailed, **kwargs)
202
 
 
203
 
    def _fetch_images(self, fetch_func, **kwargs):
204
 
        """Paginate through results from glance server"""
205
 
        try:
206
 
            images = fetch_func(**kwargs)
207
 
        except Exception:
208
 
            _reraise_translated_exception()
209
 
 
210
 
        if not images:
211
 
            # break out of recursive loop to end pagination
212
 
            return
213
 
 
214
 
        for image in images:
215
 
            yield image
216
 
 
217
 
        try:
218
 
            # attempt to advance the marker in order to fetch next page
219
 
            kwargs['marker'] = images[-1]['id']
220
 
        except KeyError:
221
 
            raise exception.ImagePaginationFailed()
222
 
 
223
 
        try:
224
 
            kwargs['limit'] = kwargs['limit'] - len(images)
225
 
            # break if we have reached a provided limit
226
 
            if kwargs['limit'] <= 0:
227
 
                return
228
 
        except KeyError:
229
 
            # ignore missing limit, just proceed without it
230
 
            pass
231
 
 
232
 
        for image in self._fetch_images(fetch_func, **kwargs):
233
 
            yield image
234
 
 
235
 
    def show(self, context, image_id):
236
 
        """Returns a dict with image data for the given opaque image id."""
237
 
        try:
238
 
            image_meta = self._call_retry(context, 'get_image_meta',
239
 
                                          image_id)
240
 
        except Exception:
241
 
            _reraise_translated_image_exception(image_id)
242
 
 
243
 
        if not self._is_image_available(context, image_meta):
244
 
            raise exception.ImageNotFound(image_id=image_id)
245
 
 
246
 
        base_image_meta = self._translate_from_glance(image_meta)
247
 
        return base_image_meta
248
 
 
249
 
    def show_by_name(self, context, name):
250
 
        """Returns a dict containing image data for the given name."""
251
 
        image_metas = self.detail(context, filters={'name': name})
252
 
        try:
253
 
            return image_metas[0]
254
 
        except (IndexError, TypeError):
255
 
            raise exception.ImageNotFound(image_id=name)
256
 
 
257
 
    def get(self, context, image_id, data):
258
 
        """Calls out to Glance for metadata and data and writes data."""
259
 
        try:
260
 
            image_meta, image_chunks = self._call_retry(context, 'get_image',
261
 
                                                        image_id)
262
 
        except Exception:
263
 
            _reraise_translated_image_exception(image_id)
264
 
 
265
 
        for chunk in image_chunks:
266
 
            data.write(chunk)
267
 
 
268
 
        base_image_meta = self._translate_from_glance(image_meta)
269
 
        return base_image_meta
270
 
 
271
 
    def create(self, context, image_meta, data=None):
272
 
        """Store the image data and return the new image id.
273
 
 
274
 
        :raises: AlreadyExists if the image already exist.
275
 
 
276
 
        """
277
 
        # Translate Base -> Service
278
 
        LOG.debug(_('Creating image in Glance. Metadata passed in %s'),
279
 
                  image_meta)
280
 
        sent_service_image_meta = self._translate_to_glance(image_meta)
281
 
        LOG.debug(_('Metadata after formatting for Glance %s'),
282
 
                  sent_service_image_meta)
283
 
 
284
 
        recv_service_image_meta = self._get_client(context).add_image(
285
 
            sent_service_image_meta, data)
286
 
 
287
 
        # Translate Service -> Base
288
 
        base_image_meta = self._translate_from_glance(recv_service_image_meta)
289
 
        LOG.debug(_('Metadata returned from Glance formatted for Base %s'),
290
 
                  base_image_meta)
291
 
        return base_image_meta
292
 
 
293
 
    def update(self, context, image_id, image_meta, data=None):
294
 
        """Replace the contents of the given image with the new data.
295
 
 
296
 
        :raises: ImageNotFound if the image does not exist.
297
 
 
298
 
        """
299
 
        # NOTE(vish): show is to check if image is available
300
 
        self.show(context, image_id)
301
 
        image_meta = self._translate_to_glance(image_meta)
302
 
        client = self._get_client(context)
303
 
        try:
304
 
            image_meta = client.update_image(image_id, image_meta, data)
305
 
        except Exception:
306
 
            _reraise_translated_image_exception(image_id)
307
 
 
308
 
        base_image_meta = self._translate_from_glance(image_meta)
309
 
        return base_image_meta
310
 
 
311
 
    def delete(self, context, image_id):
312
 
        """Delete the given image.
313
 
 
314
 
        :raises: ImageNotFound if the image does not exist.
315
 
        :raises: NotAuthorized if the user is not an owner.
316
 
 
317
 
        """
318
 
        # NOTE(vish): show is to check if image is available
319
 
        image_meta = self.show(context, image_id)
320
 
 
321
 
        if FLAGS.auth_strategy == 'deprecated':
322
 
            # NOTE(parthi): only allow image deletions if the user
323
 
            # is a member of the project owning the image, in case of
324
 
            # setup without keystone
325
 
            # TODO(parthi): Currently this access control breaks if
326
 
            # 1. Image is not owned by a project
327
 
            # 2. Deleting user is not bound a project
328
 
            properties = image_meta['properties']
329
 
            if (context.project_id and ('project_id' in properties)
330
 
                and (context.project_id != properties['project_id'])):
331
 
                raise exception.NotAuthorized(_("Not the image owner"))
332
 
 
333
 
            if (context.project_id and ('owner_id' in properties)
334
 
                and (context.project_id != properties['owner_id'])):
335
 
                raise exception.NotAuthorized(_("Not the image owner"))
336
 
 
337
 
        try:
338
 
            result = self._get_client(context).delete_image(image_id)
339
 
        except glance_exception.NotFound:
340
 
            raise exception.ImageNotFound(image_id=image_id)
341
 
        return result
342
 
 
343
 
    def delete_all(self):
344
 
        """Clears out all images."""
345
 
        pass
346
 
 
347
 
    @classmethod
348
 
    def _translate_to_glance(cls, image_meta):
349
 
        image_meta = _convert_to_string(image_meta)
350
 
        image_meta = _remove_read_only(image_meta)
351
 
        return image_meta
352
 
 
353
 
    @classmethod
354
 
    def _translate_from_glance(cls, image_meta):
355
 
        image_meta = _limit_attributes(image_meta)
356
 
        image_meta = _convert_timestamps_to_datetimes(image_meta)
357
 
        image_meta = _convert_from_string(image_meta)
358
 
        return image_meta
359
 
 
360
 
    @staticmethod
361
 
    def _is_image_available(context, image_meta):
362
 
        """Check image availability.
363
 
 
364
 
        Under Glance, images are always available if the context has
365
 
        an auth_token.
366
 
 
367
 
        """
368
 
        if hasattr(context, 'auth_token') and context.auth_token:
369
 
            return True
370
 
 
371
 
        if image_meta['is_public'] or context.is_admin:
372
 
            return True
373
 
 
374
 
        properties = image_meta['properties']
375
 
 
376
 
        if context.project_id and ('owner_id' in properties):
377
 
            return str(properties['owner_id']) == str(context.project_id)
378
 
 
379
 
        if context.project_id and ('project_id' in properties):
380
 
            return str(properties['project_id']) == str(context.project_id)
381
 
 
382
 
        try:
383
 
            user_id = properties['user_id']
384
 
        except KeyError:
385
 
            return False
386
 
 
387
 
        return str(user_id) == str(context.user_id)
388
 
 
389
 
 
390
 
# utility functions
391
 
def _convert_timestamps_to_datetimes(image_meta):
392
 
    """Returns image with timestamp fields converted to datetime objects."""
393
 
    for attr in ['created_at', 'updated_at', 'deleted_at']:
394
 
        if image_meta.get(attr):
395
 
            image_meta[attr] = _parse_glance_iso8601_timestamp(
396
 
                image_meta[attr])
397
 
    return image_meta
398
 
 
399
 
 
400
 
def _parse_glance_iso8601_timestamp(timestamp):
401
 
    """Parse a subset of iso8601 timestamps into datetime objects."""
402
 
    iso_formats = ['%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S']
403
 
 
404
 
    for iso_format in iso_formats:
405
 
        try:
406
 
            return datetime.datetime.strptime(timestamp, iso_format)
407
 
        except ValueError:
408
 
            pass
409
 
 
410
 
    raise ValueError(_('%(timestamp)s does not follow any of the '
411
 
                       'signatures: %(iso_formats)s') % locals())
412
 
 
413
 
 
414
 
# TODO(yamahata): use block-device-mapping extension to glance
415
 
def _json_loads(properties, attr):
416
 
    prop = properties[attr]
417
 
    if isinstance(prop, basestring):
418
 
        properties[attr] = json.loads(prop)
419
 
 
420
 
 
421
 
def _json_dumps(properties, attr):
422
 
    prop = properties[attr]
423
 
    if not isinstance(prop, basestring):
424
 
        properties[attr] = json.dumps(prop)
425
 
 
426
 
 
427
 
_CONVERT_PROPS = ('block_device_mapping', 'mappings')
428
 
 
429
 
 
430
 
def _convert(method, metadata):
431
 
    metadata = copy.deepcopy(metadata)  # don't touch original metadata
432
 
    properties = metadata.get('properties')
433
 
    if properties:
434
 
        for attr in _CONVERT_PROPS:
435
 
            if attr in properties:
436
 
                method(properties, attr)
437
 
 
438
 
    return metadata
439
 
 
440
 
 
441
 
def _convert_from_string(metadata):
442
 
    return _convert(_json_loads, metadata)
443
 
 
444
 
 
445
 
def _convert_to_string(metadata):
446
 
    return _convert(_json_dumps, metadata)
447
 
 
448
 
 
449
 
def _limit_attributes(image_meta):
450
 
    IMAGE_ATTRIBUTES = ['size', 'disk_format',
451
 
                        'container_format', 'checksum', 'id',
452
 
                        'name', 'created_at', 'updated_at',
453
 
                        'deleted_at', 'deleted', 'status',
454
 
                        'min_disk', 'min_ram', 'is_public']
455
 
    output = {}
456
 
    for attr in IMAGE_ATTRIBUTES:
457
 
        output[attr] = image_meta.get(attr)
458
 
 
459
 
    output['properties'] = image_meta.get('properties', {})
460
 
 
461
 
    return output
462
 
 
463
 
 
464
 
def _remove_read_only(image_meta):
465
 
    IMAGE_ATTRIBUTES = ['updated_at', 'created_at', 'deleted_at']
466
 
    output = copy.deepcopy(image_meta)
467
 
    for attr in IMAGE_ATTRIBUTES:
468
 
        if attr in output:
469
 
            del output[attr]
470
 
    return output
471
 
 
472
 
 
473
 
def _reraise_translated_image_exception(image_id):
474
 
    """Transform the exception for the image but keep its traceback intact."""
475
 
    exc_type, exc_value, exc_trace = sys.exc_info()
476
 
    new_exc = _translate_image_exception(image_id, exc_type, exc_value)
477
 
    raise new_exc, None, exc_trace
478
 
 
479
 
 
480
 
def _reraise_translated_exception():
481
 
    """Transform the exception but keep its traceback intact."""
482
 
    exc_type, exc_value, exc_trace = sys.exc_info()
483
 
    new_exc = _translate_plain_exception(exc_type, exc_value)
484
 
    raise new_exc, None, exc_trace
485
 
 
486
 
 
487
 
def _translate_image_exception(image_id, exc_type, exc_value):
488
 
    if exc_type in (glance_exception.Forbidden,
489
 
                    glance_exception.NotAuthenticated,
490
 
                    glance_exception.MissingCredentialError):
491
 
        return exception.ImageNotAuthorized(image_id=image_id)
492
 
    if exc_type is glance_exception.NotFound:
493
 
        return exception.ImageNotFound(image_id=image_id)
494
 
    if exc_type is glance_exception.Invalid:
495
 
        return exception.Invalid(exc_value)
496
 
    return exc_value
497
 
 
498
 
 
499
 
def _translate_plain_exception(exc_type, exc_value):
500
 
    if exc_type in (glance_exception.Forbidden,
501
 
                    glance_exception.NotAuthenticated,
502
 
                    glance_exception.MissingCredentialError):
503
 
        return exception.NotAuthorized(exc_value)
504
 
    if exc_type is glance_exception.NotFound:
505
 
        return exception.NotFound(exc_value)
506
 
    if exc_type is glance_exception.Invalid:
507
 
        return exception.Invalid(exc_value)
508
 
    return exc_value