1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 United States Government as represented by the
4
# Administrator of the National Aeronautics and Space Administration.
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
11
# http://www.apache.org/licenses/LICENSE-2.0
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
19
"""Proxy AMI-related calls from cloud controller to objectstore service."""
27
from xml.etree import ElementTree
29
import boto.s3.connection
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
42
LOG = logging.getLogger(__name__)
45
cfg.StrOpt('image_decryption_dir',
47
help='parent dir for tempdir used for image decryption'),
48
cfg.StrOpt('s3_access_key',
50
help='access key to use for s3 server for images'),
51
cfg.StrOpt('s3_secret_key',
53
help='secret key to use for s3 server for images'),
54
cfg.BoolOpt('s3_use_ssl',
56
help='whether to use ssl when talking to s3'),
57
cfg.BoolOpt('s3_affix_tenant',
59
help='whether to affix the tenant id to the access key '
60
'when downloading from s3'),
64
FLAGS.register_opts(s3_opts)
67
class S3ImageService(object):
68
"""Wraps an existing image service to support s3 based register."""
70
def __init__(self, service=None, *args, **kwargs):
71
self.service = service or image.get_default_image_service()
72
self.service.__init__(*args, **kwargs)
74
def _translate_uuids_to_ids(self, context, images):
75
return [self._translate_uuid_to_id(context, img) for img in images]
77
def _translate_uuid_to_id(self, context, image):
78
image_copy = image.copy()
81
image_uuid = image_copy['id']
85
image_copy['id'] = ec2utils.glance_id_to_id(context, image_uuid)
87
for prop in ['kernel_id', 'ramdisk_id']:
89
image_uuid = image_copy['properties'][prop]
90
except (KeyError, ValueError):
93
image_id = ec2utils.glance_id_to_id(context, image_uuid)
94
image_copy['properties'][prop] = image_id
98
def _translate_id_to_uuid(self, context, image):
99
image_copy = image.copy()
102
image_id = image_copy['id']
106
image_copy['id'] = ec2utils.id_to_glance_id(context, image_id)
108
for prop in ['kernel_id', 'ramdisk_id']:
110
image_id = image_copy['properties'][prop]
111
except (KeyError, ValueError):
114
image_uuid = ec2utils.id_to_glance_id(context, image_id)
115
image_copy['properties'][prop] = image_uuid
119
def create(self, context, metadata, data=None):
122
metadata['properties'] should contain image_location.
125
image = self._s3_create(context, metadata)
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)
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)
138
def index(self, context):
139
#NOTE(bcwaldon): sort asc to make sure we assign lower ids
141
images = self.service.index(context, sort_dir='asc')
142
return self._translate_uuids_to_ids(context, images)
144
def detail(self, context):
145
#NOTE(bcwaldon): sort asc to make sure we assign lower ids
147
images = self.service.detail(context, sort_dir='asc')
148
return self._translate_uuids_to_ids(context, images)
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)
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)
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,
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
182
def _s3_parse_manifest(self, context, metadata, manifest):
183
manifest = ElementTree.fromstring(manifest)
185
image_type = 'machine'
188
kernel_id = manifest.find('machine_configuration/kernel_id').text
189
if kernel_id == 'true':
191
image_type = 'kernel'
197
ramdisk_id = manifest.find('machine_configuration/ramdisk_id').text
198
if ramdisk_id == 'true':
200
image_type = 'ramdisk'
206
arch = manifest.find('machine_configuration/architecture').text
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}
219
# Do NOT confuse this with ec2-register's block device mapping
223
block_device_mapping = manifest.findall('machine_configuration/'
224
'block_device_mapping/'
226
for bdm in block_device_mapping:
227
mappings.append({'virtual': bdm.find('virtual').text,
228
'device': bdm.find('device').text})
232
properties = metadata['properties']
233
properties['architecture'] = arch
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
240
_translate_dependent_image_id('kernel_id', kernel_id)
243
_translate_dependent_image_id('ramdisk_id', ramdisk_id)
246
properties['mappings'] = mappings
248
metadata.update({'disk_format': image_format,
249
'container_format': image_format,
252
'properties': properties})
253
metadata['properties']['image_state'] = 'pending'
255
#TODO(bcwaldon): right now, this removes user-defined ids.
256
# We need to re-enable this.
257
image_id = metadata.pop('id', None)
259
image = self.service.create(context, metadata)
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)
265
# return image_uuid so the caller can still make use of image_service
266
return manifest, image, image_uuid
268
def _s3_create(self, context, metadata):
269
"""Gets a manifest from s3 and makes an image."""
271
image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir)
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()
280
manifest, image, image_uuid = self._s3_parse_manifest(context,
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)
294
elements = manifest.find('image').getiterator('filename')
295
for fn_element in elements:
296
part = self._download_file(bucket,
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)
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)
315
metadata['properties']['image_state'] = 'decrypting'
316
self.service.update(context, image_uuid, metadata)
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)
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)
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)
334
metadata['properties']['image_state'] = 'untarring'
335
self.service.update(context, image_uuid, metadata)
338
unz_filename = self._untarzip_image(image_path, dec_filename)
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)
346
metadata['properties']['image_state'] = 'uploading'
347
self.service.update(context, image_uuid, metadata)
349
with open(unz_filename) as image_file:
350
self.service.update(context, image_uuid,
351
metadata, image_file)
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)
359
metadata['properties']['image_state'] = 'available'
360
metadata['status'] = 'active'
361
self.service.update(context, image_uuid, metadata)
363
shutil.rmtree(image_path)
365
eventlet.spawn_n(delayed_create)
370
def _decrypt_image(context, encrypted_filename, encrypted_key,
371
encrypted_iv, decrypted_filename):
372
elevated = context.elevated()
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')
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 '
391
utils.execute('openssl', 'enc',
392
'-d', '-aes-128-cbc',
393
'-in', '%s' % (encrypted_filename,),
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,
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):
410
raise exception.Error(_('Unsafe filenames in image'))
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]
420
return os.path.join(path, image_file)