From 3609b4f1129760ad6141de5f5bbc91424f3270ab Mon Sep 17 00:00:00 2001 From: Paul Hummer Date: Thu, 24 Nov 2016 06:02:00 +0000 Subject: [PATCH] Update the glance<->lxd image sync. Kill the old deprecated api, and simplify the sync process. pylxd now supports the stuff needed to do multipart uploads. Removing the setup_image mock clicks things into place. I think, after this is all finished, we'll want to move some stuff around, so we don't have one big module with all our code in it (it's getting a little unwieldy), but I won't worry about that until it's all updated. Change-Id: Ic3ad53701fd360f1390e8198135c846294253ab4 --- nova/tests/unit/virt/lxd/test_driver.py | 13 +- nova/virt/lxd/driver.py | 251 ++++++------------------ nova/virt/lxd/session.py | 33 ---- 3 files changed, 66 insertions(+), 231 deletions(-) diff --git a/nova/tests/unit/virt/lxd/test_driver.py b/nova/tests/unit/virt/lxd/test_driver.py index 89b28293..3cad94d3 100644 --- a/nova/tests/unit/virt/lxd/test_driver.py +++ b/nova/tests/unit/virt/lxd/test_driver.py @@ -240,7 +240,6 @@ class LXDDriverTest(test.NoDBTestCase): # XXX: rockstar (6 Jul 2016) - There are a number of XXX comments # related to these calls in spawn. They require some work before we # can take out these mocks and follow the real codepaths. - lxd_driver.setup_image = mock.Mock() lxd_driver.firewall_driver = mock.Mock() lxd_driver._add_ephemeral = mock.Mock() @@ -248,8 +247,6 @@ class LXDDriverTest(test.NoDBTestCase): ctx, instance, image_meta, injected_files, admin_password, network_info, block_device_info) - lxd_driver.setup_image.assert_called_once_with( - ctx, instance, image_meta) self.vif_driver.plug.assert_called_once_with( instance, network_info[0]) fd = lxd_driver.firewall_driver @@ -303,7 +300,6 @@ class LXDDriverTest(test.NoDBTestCase): # XXX: rockstar (6 Jul 2016) - There are a number of XXX comments # related to these calls in spawn. They require some work before we # can take out these mocks and follow the real codepaths. - lxd_driver.setup_image = mock.Mock() lxd_driver.firewall_driver = mock.Mock() lxd_driver._add_ephemeral = mock.Mock() lxd_driver._add_configdrive = mock.Mock() @@ -312,8 +308,6 @@ class LXDDriverTest(test.NoDBTestCase): ctx, instance, image_meta, injected_files, admin_password, network_info, block_device_info) - lxd_driver.setup_image.assert_called_once_with( - ctx, instance, image_meta) self.vif_driver.plug.assert_called_once_with( instance, network_info[0]) fd = lxd_driver.firewall_driver @@ -350,7 +344,6 @@ class LXDDriverTest(test.NoDBTestCase): lxd_driver = driver.LXDDriver(virtapi) lxd_driver.init_host(None) - lxd_driver.setup_image = mock.Mock() lxd_driver.cleanup = mock.Mock() self.assertRaises( @@ -384,7 +377,6 @@ class LXDDriverTest(test.NoDBTestCase): lxd_driver = driver.LXDDriver(virtapi) lxd_driver.init_host(None) - lxd_driver.setup_image = mock.Mock() lxd_driver.cleanup = mock.Mock() self.assertRaises( @@ -416,7 +408,6 @@ class LXDDriverTest(test.NoDBTestCase): lxd_driver = driver.LXDDriver(virtapi) lxd_driver.init_host(None) - lxd_driver.setup_image = mock.Mock() lxd_driver.cleanup = mock.Mock() lxd_driver.client.containers.create = mock.Mock( side_effect=side_effect) @@ -461,10 +452,8 @@ class LXDDriverTest(test.NoDBTestCase): @mock.patch.object(drv, '_add_ephemeral') @mock.patch.object(drv, 'plug_vifs') - @mock.patch.object(drv, 'setup_image') @mock.patch('nova.virt.configdrive.required_by') - def test_spawn( - configdrive, setup_image, plug_vifs, add_ephemeral): + def test_spawn(configdrive, plug_vifs, add_ephemeral): def container_get(*args, **kwargs): raise lxdcore_exceptions.LXDAPIException(MockResponse(404)) self.client.containers.get.side_effect = container_get diff --git a/nova/virt/lxd/driver.py b/nova/virt/lxd/driver.py index cbf251cd..c2d68323 100644 --- a/nova/virt/lxd/driver.py +++ b/nova/virt/lxd/driver.py @@ -1,4 +1,3 @@ -# Copyright 2011 Justin Santa Barbara # Copyright 2015 Canonical Ltd # All Rights Reserved. # @@ -17,7 +16,6 @@ from __future__ import absolute_import import collections -import hashlib import io import json import os @@ -27,7 +25,6 @@ import shutil import socket import tarfile import tempfile -import uuid import eventlet import nova.conf @@ -90,10 +87,10 @@ IMAGE_API = image.API() MAX_CONSOLE_BYTES = 100 * units.Ki NOVA_CONF = nova.conf.CONF -BASE_DIR = os.path.join( - CONF.instances_path, CONF.image_cache_subdirectory_name) CONTAINER_DIR = os.path.join(CONF.lxd.root_dir, 'containers') +ACCEPTABLE_IMAGE_FORMATS = {'raw', 'root-tar', 'squashfs'} + _InstanceAttributes = collections.namedtuple('InstanceAttributes', [ 'instance_dir', 'console_path', 'storage_path']) @@ -301,10 +298,61 @@ def _make_network_config(instance, network_info): return network_devices +def _sync_glance_image_to_lxd(client, context, image_ref): + """Sync an image from glance to LXD image store. + + The image from glance can't go directly into the LXD image store, + as LXD needs some extra metadata connected to it. + + The image is stored in the LXD image store with an alias to + the image_ref. This way, it will only copy over once. + """ + lock_path = os.path.join(CONF.instances_path, 'locks') + with lockutils.lock( + lock_path, external=True, + lock_file_prefix='lxd-image-{}'.format(image_ref)): + + try: + image_file = tempfile.mkstemp()[1] + manifest_file = tempfile.mkstemp()[1] + + image = IMAGE_API.get(context, image_ref) + if image.get('disk_format') not in ACCEPTABLE_IMAGE_FORMATS: + raise exception.ImageUnacceptable( + image_id=image_ref, reason=_('Bad image format')) + IMAGE_API.download(context, image_ref, dest_path=image_file) + + metadata = { + 'architecture': image.get( + 'hw_architecture', obj_fields.Architecture.from_host()), + 'creation_date': int(os.stat(image_file).st_ctime)} + metadata_yaml = json.dumps( + metadata, sort_keys=True, indent=4, + separators=(',', ': '), + ensure_ascii=False).encode('utf-8') + b"\n" + + tarball = tarfile.open(manifest_file, "w:gz") + tarinfo = tarfile.TarInfo(name='metadata.yaml') + tarinfo.size = len(metadata_yaml) + tarball.addfile(tarinfo, io.BytesIO(metadata_yaml)) + tarball.close() + + with open(manifest_file, 'rb') as manifest: + with open(image_file, 'rb') as image: + image = client.images.create( + image.read(), metadata=manifest.read(), + wait=True) + image.add_alias(image_ref, '') + + finally: + os.unlink(image_file) + os.unlink(manifest_file) + + class LXDLiveMigrateData(migrate_data.LiveMigrateData): """LiveMigrateData for LXD.""" - VERSION = '1.0' + VERSION = '1.0' fields = {} @@ -378,7 +426,6 @@ class LXDDriver(driver.ComputeDriver): def get_info(self, instance): """Return an InstanceInfo object for the instance.""" - container = self.client.containers.get(instance.name) state = container.state() @@ -407,7 +454,6 @@ class LXDDriver(driver.ComputeDriver): See `nova.virt.driver.ComputeDriver.spawn` for more information. """ - try: self.client.containers.get(instance.name) raise exception.InstanceExists(name=instance.name) @@ -419,12 +465,15 @@ class LXDDriver(driver.ComputeDriver): if not os.path.exists(instance_dir): fileutils.ensure_tree(instance_dir) - # Fetch image from glance - # XXX: rockstar (6 Jul 2016) - The use of setup_image here is - # a little strange. setup_image is nat a driver required method, - # and is only called in this one place. It may be a candidate for - # refactoring. - self.setup_image(context, instance, image_meta) + # Check to see if LXD already has a copy of the image. If not, + # fetch it. + try: + self.client.images.get_by_alias(instance.image_ref) + except lxd_exceptions.LXDAPIException as e: + if e.response.status_code != 404: + raise + _sync_glance_image_to_lxd( + self.client, context, instance.image_ref) # Plug in the network if network_info: @@ -792,7 +841,7 @@ class LXDDriver(driver.ComputeDriver): def rescue(self, context, instance, network_info, image_meta, rescue_password): - """Rescue a LXD container + """Rescue a LXD container. Rescuing a instance requires a number of steps. First, the failed container is stopped. Next, '-rescue', is @@ -805,7 +854,6 @@ class LXDDriver(driver.ComputeDriver): See 'nova.virt.driver.ComputeDriver.rescue` for more information. """ - rescue = '%s-rescue' % instance.name container = self.client.containers.get(instance.name) @@ -838,7 +886,7 @@ class LXDDriver(driver.ComputeDriver): container.start(wait=True) def unrescue(self, instance, network_info): - """Unrescue an instance + """Unrescue an instance. Unrescue a container that has previously been rescued. First the rescue containerisremoved. Next the rootfs @@ -1292,175 +1340,6 @@ class LXDDriver(driver.ComputeDriver): return configdrive_dir - def setup_image(self, context, instance, image_meta): - """Download an image from glance and upload it to LXD - - :param context: context object - :param instance: The nova instance - :param image_meta: Image dict returned by nova.image.glance - """ - LOG.debug('setup_image called for instance', instance=instance) - lock_path = str(os.path.join(CONF.instances_path, 'locks')) - - container_image = os.path.join( - BASE_DIR, '%s-rootfs.tar.gz' % image_meta.id) - container_manifest = os.path.join( - BASE_DIR, '%s-manifest.tar.gz' % image_meta.id) - - with lockutils.lock(lock_path, - lock_file_prefix=('lxd-image-%s' % - instance.image_ref), - external=True): - - try: - self.client.images.get_by_alias(instance.image_ref) - return - except lxd_exceptions.LXDAPIException as e: - if e.response.status_code != 404: - raise - - base_dir = BASE_DIR - if not os.path.exists(base_dir): - fileutils.ensure_tree(base_dir) - - try: - # Inspect image for the correct format - try: - # grab the disk format of the image - img_meta = IMAGE_API.get(context, instance.image_ref) - disk_format = img_meta.get('disk_format') - if not disk_format: - reason = _('Bad image format') - raise exception.ImageUnacceptable( - image_id=instance.image_ref, reason=reason) - - if disk_format not in ['raw', 'root-tar']: - reason = _( - 'nova-lxd does not support images in %s format. ' - 'You should upload an image in raw or root-tar ' - 'format.') % disk_format - raise exception.ImageUnacceptable( - image_id=instance.image_ref, reason=reason) - except Exception as ex: - reason = _('Bad Image format: %(ex)s') \ - % {'ex': ex} - raise exception.ImageUnacceptable( - image_id=instance.image_ref, reason=reason) - - # Fetch the image from glance - with fileutils.remove_path_on_error(container_image): - IMAGE_API.download(context, instance.image_ref, - dest_path=container_image) - - # Generate the LXD manifest for the image - metadata_yaml = None - try: - # Create a basic LXD manifest from the image properties - image_arch = image_meta.properties.get('hw_architecture') - if image_arch is None: - image_arch = obj_fields.Architecture.from_host() - metadata = { - 'architecture': image_arch, - 'creation_date': int(os.stat(container_image).st_ctime) - } - - metadata_yaml = json.dumps( - metadata, sort_keys=True, indent=4, - separators=(',', ': '), - ensure_ascii=False).encode('utf-8') + b"\n" - except Exception as ex: - with excutils.save_and_reraise_exception(): - LOG.error( - _LE('Failed to generate manifest for %(image)s: ' - '%(reason)s'), - {'image': instance.name, 'ex': ex}, - instance=instance) - try: - # Compress the manifest using tar - target_tarball = tarfile.open(container_manifest, "w:gz") - metadata_file = tarfile.TarInfo() - metadata_file.size = len(metadata_yaml) - metadata_file.name = "metadata.yaml" - target_tarball.addfile(metadata_file, - io.BytesIO(metadata_yaml)) - target_tarball.close() - except Exception as ex: - with excutils.save_and_reraise_exception(): - LOG.error(_LE('Failed to generate manifest tarball for' - ' %(image)s: %(reason)s'), - {'image': instance.name, 'ex': ex}, - instance=instance) - - # Upload the image to the local LXD image store - headers = {} - - boundary = str(uuid.uuid1()) - - # Create the binary blob to upload the file to LXD - tmpdir = tempfile.mkdtemp() - upload_path = os.path.join(tmpdir, "upload") - body = open(upload_path, 'wb+') - - for name, path in [("metadata", (container_manifest)), - ("rootfs", container_image)]: - filename = os.path.basename(path) - body.write(bytearray("--%s\r\n" % boundary, "utf-8")) - body.write(bytearray("Content-Disposition: form-data; " - "name=%s; filename=%s\r\n" % - (name, filename), "utf-8")) - body.write("Content-Type: application/octet-stream\r\n") - body.write("\r\n") - with open(path, "rb") as fd: - shutil.copyfileobj(fd, body) - body.write("\r\n") - - body.write(bytearray("--%s--\r\n" % boundary, "utf-8")) - body.write('\r\n') - body.close() - - headers['Content-Type'] = "multipart/form-data; boundary=%s" \ - % boundary - - # Upload the file to LXD and then remove the tmpdir. - self.session.image_upload( - data=open(upload_path, 'rb'), headers=headers, - instance=instance) - shutil.rmtree(tmpdir) - - # Setup the LXD alias for the image - try: - with open((container_manifest), 'rb') as meta_fd: - with open(container_image, "rb") as rootfs_fd: - fingerprint = hashlib.sha256( - meta_fd.read() + rootfs_fd.read()).hexdigest() - image = self.client.images.get(fingerprint) - image.add_alias(instance.image_ref, image_meta.name) - except Exception as ex: - with excutils.save_and_reraise_exception: - LOG.error( - _LE('Failed to setup alias for %(image)s:' - ' %(ex)s'), {'image': instance.image_ref, - 'ex': ex}, instance=instance) - - # Remove image and manifest when done. - if os.path.exists(container_image): - os.unlink(container_image) - - if os.path.exists(container_manifest): - os.unlink(container_manifest) - - except Exception as ex: - with excutils.save_and_reraise_exception(): - LOG.error(_LE('Failed to upload %(image)s to LXD: ' - '%(reason)s'), - {'image': instance.image_ref, - 'reason': ex}, instance=instance) - if os.path.exists(container_image): - os.unlink(container_image) - - if os.path.exists(container_manifest): - os.unlink(container_manifest) - def get_container_migrate(self, container_migrate, host, instance): """Create the image source for a migrating container diff --git a/nova/virt/lxd/session.py b/nova/virt/lxd/session.py index c97ae4d3..881b5cf2 100644 --- a/nova/virt/lxd/session.py +++ b/nova/virt/lxd/session.py @@ -105,39 +105,6 @@ class LXDAPISession(object): {'instance': instance.name, 'reason': ex}, instance=instance) - # - # Image related API methods. - # - def image_upload(self, data, headers, instance): - """Upload an image to the local LXD image store - - :param data: image data - :param headers: image headers - :param instance: The nova instance - - """ - LOG.debug('upload_image called for instance', instance=instance) - try: - client = self.get_session() - (state, data) = client.image_upload(data=data, - headers=headers) - # XXX - zulcss (Dec 8, 2015) - Work around for older - # versions of LXD. - if 'operation' in data: - self.operation_wait(data.get('operation'), instance) - except lxd_exceptions.APIError as ex: - msg = _('Failed to communicate with LXD API %(instance)s:' - '%(reason)s') % {'instance': instance.image_ref, - 'reason': ex} - LOG.error(msg) - raise exception.NovaException(msg) - except Exception as e: - with excutils.save_and_reraise_exception(): - LOG.error(_LE('Error from LXD during image upload' - '%(instance)s: %(reason)s'), - {'instance': instance.image_ref, 'reason': e}, - instance=instance) - # # Operation methods #