~zulcss/nova/nova-precise-g3

« back to all changes in this revision

Viewing changes to .pc/upstream/0005-Populate-image-properties-with-project_id-again.patch/nova/image/s3.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 United States Government as represented by the
 
4
# Administrator of the National Aeronautics and Space Administration.
 
5
# All Rights Reserved.
 
6
#
 
7
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
8
#    not use this file except in compliance with the License. You may obtain
 
9
#    a copy of the License at
 
10
#
 
11
#         http://www.apache.org/licenses/LICENSE-2.0
 
12
#
 
13
#    Unless required by applicable law or agreed to in writing, software
 
14
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
15
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
16
#    License for the specific language governing permissions and limitations
 
17
#    under the License.
 
18
 
 
19
"""Proxy AMI-related calls from cloud controller to objectstore service."""
 
20
 
 
21
import base64
 
22
import binascii
 
23
import os
 
24
import shutil
 
25
import tarfile
 
26
import tempfile
 
27
from xml.etree import ElementTree
 
28
 
 
29
import boto.s3.connection
 
30
import eventlet
 
31
 
 
32
from nova import rpc
 
33
from nova import exception
 
34
from nova import flags
 
35
from nova import image
 
36
from nova import log as logging
 
37
from nova.openstack.common import cfg
 
38
from nova import utils
 
39
from nova.api.ec2 import ec2utils
 
40
 
 
41
 
 
42
LOG = logging.getLogger(__name__)
 
43
 
 
44
s3_opts = [
 
45
    cfg.StrOpt('image_decryption_dir',
 
46
               default='/tmp',
 
47
               help='parent dir for tempdir used for image decryption'),
 
48
    cfg.StrOpt('s3_access_key',
 
49
               default='notchecked',
 
50
               help='access key to use for s3 server for images'),
 
51
    cfg.StrOpt('s3_secret_key',
 
52
               default='notchecked',
 
53
               help='secret key to use for s3 server for images'),
 
54
    cfg.BoolOpt('s3_use_ssl',
 
55
               default=False,
 
56
               help='whether to use ssl when talking to s3'),
 
57
    cfg.BoolOpt('s3_affix_tenant',
 
58
               default=False,
 
59
               help='whether to affix the tenant id to the access key '
 
60
                    'when downloading from s3'),
 
61
    ]
 
62
 
 
63
FLAGS = flags.FLAGS
 
64
FLAGS.register_opts(s3_opts)
 
65
 
 
66
 
 
67
class S3ImageService(object):
 
68
    """Wraps an existing image service to support s3 based register."""
 
69
 
 
70
    def __init__(self, service=None, *args, **kwargs):
 
71
        self.service = service or image.get_default_image_service()
 
72
        self.service.__init__(*args, **kwargs)
 
73
 
 
74
    def _translate_uuids_to_ids(self, context, images):
 
75
        return [self._translate_uuid_to_id(context, img) for img in images]
 
76
 
 
77
    def _translate_uuid_to_id(self, context, image):
 
78
        image_copy = image.copy()
 
79
 
 
80
        try:
 
81
            image_uuid = image_copy['id']
 
82
        except KeyError:
 
83
            pass
 
84
        else:
 
85
            image_copy['id'] = ec2utils.glance_id_to_id(context, image_uuid)
 
86
 
 
87
        for prop in ['kernel_id', 'ramdisk_id']:
 
88
            try:
 
89
                image_uuid = image_copy['properties'][prop]
 
90
            except (KeyError, ValueError):
 
91
                pass
 
92
            else:
 
93
                image_id = ec2utils.glance_id_to_id(context, image_uuid)
 
94
                image_copy['properties'][prop] = image_id
 
95
 
 
96
        return image_copy
 
97
 
 
98
    def _translate_id_to_uuid(self, context, image):
 
99
        image_copy = image.copy()
 
100
 
 
101
        try:
 
102
            image_id = image_copy['id']
 
103
        except KeyError:
 
104
            pass
 
105
        else:
 
106
            image_copy['id'] = ec2utils.id_to_glance_id(context, image_id)
 
107
 
 
108
        for prop in ['kernel_id', 'ramdisk_id']:
 
109
            try:
 
110
                image_id = image_copy['properties'][prop]
 
111
            except (KeyError, ValueError):
 
112
                pass
 
113
            else:
 
114
                image_uuid = ec2utils.id_to_glance_id(context, image_id)
 
115
                image_copy['properties'][prop] = image_uuid
 
116
 
 
117
        return image_copy
 
118
 
 
119
    def create(self, context, metadata, data=None):
 
120
        """Create an image.
 
121
 
 
122
        metadata['properties'] should contain image_location.
 
123
 
 
124
        """
 
125
        image = self._s3_create(context, metadata)
 
126
        return image
 
127
 
 
128
    def delete(self, context, image_id):
 
129
        image_uuid = ec2utils.id_to_glance_id(context, image_id)
 
130
        self.service.delete(context, image_uuid)
 
131
 
 
132
    def update(self, context, image_id, metadata, data=None):
 
133
        image_uuid = ec2utils.id_to_glance_id(context, image_id)
 
134
        metadata = self._translate_id_to_uuid(context, metadata)
 
135
        image = self.service.update(context, image_uuid, metadata, data)
 
136
        return self._translate_uuid_to_id(context, image)
 
137
 
 
138
    def index(self, context):
 
139
        #NOTE(bcwaldon): sort asc to make sure we assign lower ids
 
140
        # to older images
 
141
        images = self.service.index(context, sort_dir='asc')
 
142
        return self._translate_uuids_to_ids(context, images)
 
143
 
 
144
    def detail(self, context):
 
145
        #NOTE(bcwaldon): sort asc to make sure we assign lower ids
 
146
        # to older images
 
147
        images = self.service.detail(context, sort_dir='asc')
 
148
        return self._translate_uuids_to_ids(context, images)
 
149
 
 
150
    def show(self, context, image_id):
 
151
        image_uuid = ec2utils.id_to_glance_id(context, image_id)
 
152
        image = self.service.show(context, image_uuid)
 
153
        return self._translate_uuid_to_id(context, image)
 
154
 
 
155
    def show_by_name(self, context, name):
 
156
        image = self.service.show_by_name(context, name)
 
157
        return self._translate_uuid_to_id(context, image)
 
158
 
 
159
    @staticmethod
 
160
    def _conn(context):
 
161
        # NOTE(vish): access and secret keys for s3 server are not
 
162
        #             checked in nova-objectstore
 
163
        access = FLAGS.s3_access_key
 
164
        if FLAGS.s3_affix_tenant:
 
165
            access = '%s:%s' % (access, context.project_id)
 
166
        secret = FLAGS.s3_secret_key
 
167
        calling = boto.s3.connection.OrdinaryCallingFormat()
 
168
        return boto.s3.connection.S3Connection(aws_access_key_id=access,
 
169
                                               aws_secret_access_key=secret,
 
170
                                               is_secure=FLAGS.s3_use_ssl,
 
171
                                               calling_format=calling,
 
172
                                               port=FLAGS.s3_port,
 
173
                                               host=FLAGS.s3_host)
 
174
 
 
175
    @staticmethod
 
176
    def _download_file(bucket, filename, local_dir):
 
177
        key = bucket.get_key(filename)
 
178
        local_filename = os.path.join(local_dir, os.path.basename(filename))
 
179
        key.get_contents_to_filename(local_filename)
 
180
        return local_filename
 
181
 
 
182
    def _s3_parse_manifest(self, context, metadata, manifest):
 
183
        manifest = ElementTree.fromstring(manifest)
 
184
        image_format = 'ami'
 
185
        image_type = 'machine'
 
186
 
 
187
        try:
 
188
            kernel_id = manifest.find('machine_configuration/kernel_id').text
 
189
            if kernel_id == 'true':
 
190
                image_format = 'aki'
 
191
                image_type = 'kernel'
 
192
                kernel_id = None
 
193
        except Exception:
 
194
            kernel_id = None
 
195
 
 
196
        try:
 
197
            ramdisk_id = manifest.find('machine_configuration/ramdisk_id').text
 
198
            if ramdisk_id == 'true':
 
199
                image_format = 'ari'
 
200
                image_type = 'ramdisk'
 
201
                ramdisk_id = None
 
202
        except Exception:
 
203
            ramdisk_id = None
 
204
 
 
205
        try:
 
206
            arch = manifest.find('machine_configuration/architecture').text
 
207
        except Exception:
 
208
            arch = 'x86_64'
 
209
 
 
210
        # NOTE(yamahata):
 
211
        # EC2 ec2-budlne-image --block-device-mapping accepts
 
212
        # <virtual name>=<device name> where
 
213
        # virtual name = {ami, root, swap, ephemeral<N>}
 
214
        #                where N is no negative integer
 
215
        # device name = the device name seen by guest kernel.
 
216
        # They are converted into
 
217
        # block_device_mapping/mapping/{virtual, device}
 
218
        #
 
219
        # Do NOT confuse this with ec2-register's block device mapping
 
220
        # argument.
 
221
        mappings = []
 
222
        try:
 
223
            block_device_mapping = manifest.findall('machine_configuration/'
 
224
                                                    'block_device_mapping/'
 
225
                                                    'mapping')
 
226
            for bdm in block_device_mapping:
 
227
                mappings.append({'virtual': bdm.find('virtual').text,
 
228
                                 'device': bdm.find('device').text})
 
229
        except Exception:
 
230
            mappings = []
 
231
 
 
232
        properties = metadata['properties']
 
233
        properties['architecture'] = arch
 
234
 
 
235
        def _translate_dependent_image_id(image_key, image_id):
 
236
            image_uuid = ec2utils.ec2_id_to_glance_id(context, image_id)
 
237
            properties[image_key] = image_uuid
 
238
 
 
239
        if kernel_id:
 
240
            _translate_dependent_image_id('kernel_id', kernel_id)
 
241
 
 
242
        if ramdisk_id:
 
243
            _translate_dependent_image_id('ramdisk_id', ramdisk_id)
 
244
 
 
245
        if mappings:
 
246
            properties['mappings'] = mappings
 
247
 
 
248
        metadata.update({'disk_format': image_format,
 
249
                         'container_format': image_format,
 
250
                         'status': 'queued',
 
251
                         'is_public': False,
 
252
                         'properties': properties})
 
253
        metadata['properties']['image_state'] = 'pending'
 
254
 
 
255
        #TODO(bcwaldon): right now, this removes user-defined ids.
 
256
        # We need to re-enable this.
 
257
        image_id = metadata.pop('id', None)
 
258
 
 
259
        image = self.service.create(context, metadata)
 
260
 
 
261
        # extract the new uuid and generate an int id to present back to user
 
262
        image_uuid = image['id']
 
263
        image['id'] = ec2utils.glance_id_to_id(context, image_uuid)
 
264
 
 
265
        # return image_uuid so the caller can still make use of image_service
 
266
        return manifest, image, image_uuid
 
267
 
 
268
    def _s3_create(self, context, metadata):
 
269
        """Gets a manifest from s3 and makes an image."""
 
270
 
 
271
        image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir)
 
272
 
 
273
        image_location = metadata['properties']['image_location']
 
274
        bucket_name = image_location.split('/')[0]
 
275
        manifest_path = image_location[len(bucket_name) + 1:]
 
276
        bucket = self._conn(context).get_bucket(bucket_name)
 
277
        key = bucket.get_key(manifest_path)
 
278
        manifest = key.get_contents_as_string()
 
279
 
 
280
        manifest, image, image_uuid = self._s3_parse_manifest(context,
 
281
                                                              metadata,
 
282
                                                              manifest)
 
283
 
 
284
        def delayed_create():
 
285
            """This handles the fetching and decrypting of the part files."""
 
286
            context.update_store()
 
287
            log_vars = {'image_location': image_location,
 
288
                        'image_path': image_path}
 
289
            metadata['properties']['image_state'] = 'downloading'
 
290
            self.service.update(context, image_uuid, metadata)
 
291
 
 
292
            try:
 
293
                parts = []
 
294
                elements = manifest.find('image').getiterator('filename')
 
295
                for fn_element in elements:
 
296
                    part = self._download_file(bucket,
 
297
                                               fn_element.text,
 
298
                                               image_path)
 
299
                    parts.append(part)
 
300
 
 
301
                # NOTE(vish): this may be suboptimal, should we use cat?
 
302
                enc_filename = os.path.join(image_path, 'image.encrypted')
 
303
                with open(enc_filename, 'w') as combined:
 
304
                    for filename in parts:
 
305
                        with open(filename) as part:
 
306
                            shutil.copyfileobj(part, combined)
 
307
 
 
308
            except Exception:
 
309
                LOG.exception(_("Failed to download %(image_location)s "
 
310
                                "to %(image_path)s"), log_vars)
 
311
                metadata['properties']['image_state'] = 'failed_download'
 
312
                self.service.update(context, image_uuid, metadata)
 
313
                return
 
314
 
 
315
            metadata['properties']['image_state'] = 'decrypting'
 
316
            self.service.update(context, image_uuid, metadata)
 
317
 
 
318
            try:
 
319
                hex_key = manifest.find('image/ec2_encrypted_key').text
 
320
                encrypted_key = binascii.a2b_hex(hex_key)
 
321
                hex_iv = manifest.find('image/ec2_encrypted_iv').text
 
322
                encrypted_iv = binascii.a2b_hex(hex_iv)
 
323
 
 
324
                dec_filename = os.path.join(image_path, 'image.tar.gz')
 
325
                self._decrypt_image(context, enc_filename, encrypted_key,
 
326
                                    encrypted_iv, dec_filename)
 
327
            except Exception:
 
328
                LOG.exception(_("Failed to decrypt %(image_location)s "
 
329
                                "to %(image_path)s"), log_vars)
 
330
                metadata['properties']['image_state'] = 'failed_decrypt'
 
331
                self.service.update(context, image_uuid, metadata)
 
332
                return
 
333
 
 
334
            metadata['properties']['image_state'] = 'untarring'
 
335
            self.service.update(context, image_uuid, metadata)
 
336
 
 
337
            try:
 
338
                unz_filename = self._untarzip_image(image_path, dec_filename)
 
339
            except Exception:
 
340
                LOG.exception(_("Failed to untar %(image_location)s "
 
341
                                "to %(image_path)s"), log_vars)
 
342
                metadata['properties']['image_state'] = 'failed_untar'
 
343
                self.service.update(context, image_uuid, metadata)
 
344
                return
 
345
 
 
346
            metadata['properties']['image_state'] = 'uploading'
 
347
            self.service.update(context, image_uuid, metadata)
 
348
            try:
 
349
                with open(unz_filename) as image_file:
 
350
                    self.service.update(context, image_uuid,
 
351
                                        metadata, image_file)
 
352
            except Exception:
 
353
                LOG.exception(_("Failed to upload %(image_location)s "
 
354
                                "to %(image_path)s"), log_vars)
 
355
                metadata['properties']['image_state'] = 'failed_upload'
 
356
                self.service.update(context, image_uuid, metadata)
 
357
                return
 
358
 
 
359
            metadata['properties']['image_state'] = 'available'
 
360
            metadata['status'] = 'active'
 
361
            self.service.update(context, image_uuid, metadata)
 
362
 
 
363
            shutil.rmtree(image_path)
 
364
 
 
365
        eventlet.spawn_n(delayed_create)
 
366
 
 
367
        return image
 
368
 
 
369
    @staticmethod
 
370
    def _decrypt_image(context, encrypted_filename, encrypted_key,
 
371
                       encrypted_iv, decrypted_filename):
 
372
        elevated = context.elevated()
 
373
        try:
 
374
            key = rpc.call(elevated, FLAGS.cert_topic,
 
375
                           {"method": "decrypt_text",
 
376
                            "args": {"project_id": context.project_id,
 
377
                                     "text": base64.b64encode(encrypted_key)}})
 
378
        except Exception, exc:
 
379
            raise exception.Error(_('Failed to decrypt private key: %s')
 
380
                                  % exc)
 
381
        try:
 
382
            iv = rpc.call(elevated, FLAGS.cert_topic,
 
383
                          {"method": "decrypt_text",
 
384
                           "args": {"project_id": context.project_id,
 
385
                                    "text": base64.b64encode(encrypted_iv)}})
 
386
        except Exception, exc:
 
387
            raise exception.Error(_('Failed to decrypt initialization '
 
388
                                    'vector: %s') % exc)
 
389
 
 
390
        try:
 
391
            utils.execute('openssl', 'enc',
 
392
                          '-d', '-aes-128-cbc',
 
393
                          '-in', '%s' % (encrypted_filename,),
 
394
                          '-K', '%s' % (key,),
 
395
                          '-iv', '%s' % (iv,),
 
396
                          '-out', '%s' % (decrypted_filename,))
 
397
        except exception.ProcessExecutionError, exc:
 
398
            raise exception.Error(_('Failed to decrypt image file '
 
399
                                    '%(image_file)s: %(err)s') %
 
400
                                    {'image_file': encrypted_filename,
 
401
                                     'err': exc.stdout})
 
402
 
 
403
    @staticmethod
 
404
    def _test_for_malicious_tarball(path, filename):
 
405
        """Raises exception if extracting tarball would escape extract path"""
 
406
        tar_file = tarfile.open(filename, 'r|gz')
 
407
        for n in tar_file.getnames():
 
408
            if not os.path.abspath(os.path.join(path, n)).startswith(path):
 
409
                tar_file.close()
 
410
                raise exception.Error(_('Unsafe filenames in image'))
 
411
        tar_file.close()
 
412
 
 
413
    @staticmethod
 
414
    def _untarzip_image(path, filename):
 
415
        S3ImageService._test_for_malicious_tarball(path, filename)
 
416
        tar_file = tarfile.open(filename, 'r|gz')
 
417
        tar_file.extractall(path)
 
418
        image_file = tar_file.getnames()[0]
 
419
        tar_file.close()
 
420
        return os.path.join(path, image_file)