~zulcss/nova/nova-precise-g3

« 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, Adam Gandelman, Chuck Short
  • Date: 2012-04-12 14:14:29 UTC
  • Revision ID: package-import@ubuntu.com-20120412141429-dt69y6cd5e0uqbmk
Tags: 2012.1-0ubuntu2
[ Adam Gandelman ]
* debian/rules: Properly create empty doc/build/man dir for builds that
  skip doc building
* debian/control: Set 'Conflicts: nova-compute-hypervisor' for the various
  nova-compute-$type packages. (LP: #975616)
* debian/control: Set 'Breaks: nova-api' for the various nova-api-$service
  sub-packages. (LP: #966115)

[ Chuck Short ]
* Resynchronize with stable/essex:
  - b1d11b8 Use project_id in ec2.cloud._format_image()
  - 6e988ed Fixes image publication using deprecated auth. (LP: #977765)
  - 6e988ed Populate image properties with project_id again
  - 3b14c74 Fixed bug 962840, added a test case.
  - d4e96fe Allow unprivileged RADOS users to access rbd volumes.
  - 4acfab6 Stop libvirt test from deleting instances dir
  - 155c7b2 fix bug where nova ignores glance host in imageref
* debian/nova.conf: Enabled ec2_private_dns_show_ip so that juju can
  connect to openstack instances.
* debian/patches/fix-docs-build-without-network.patch: Fix docs build
  when there is no network access.

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