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
This commit is contained in:
parent
757163cfc7
commit
3609b4f112
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
Loading…
Reference in New Issue