Added support for the LXD unified tarball format

Attempt to detect if image imported from glance is in unified LXD format
(metadata + rootfs/) and import this image to LXD 'as is' if any -
without implicit metdata injection.

Existing behavior leads to unusable for instance creation LXD images
if they are in unified format and imported via nova LXD driver
as LXD can not instantiate rootfs properly for such images

Simple use case does not work without this fix:
1. create instance -> create snapshot -> launch instance from snapshot
image

Image format identification is straightforward - attempt to search
metadata.yaml in tarball /. If found 'unified' format assumed.

Additional issues fixed:
1. fixed issue when instance from snapshot image can not be launched
   on compute node where snapshot was created. The reason is image
   already present in LXD without glance alias after snapshot creation.
   As result nova tries to import it again from glance and got error
   from LXD - "Image with same fingerprint already exists".
   Attempt to lookup LXD image also by fingerprint during import
   and if any do not import but simply add required by nova alias.

Closes-Bug: 1651506
Change-Id: I77d3b7c8d7cf43d505fd86b294779dada204919a
This commit is contained in:
Alexander Kharkov 2017-07-28 02:09:32 +00:00
parent 78b6c14f2c
commit b947a9afb3
2 changed files with 114 additions and 17 deletions

View File

@ -14,6 +14,8 @@
# under the License.
import collections
import json
import base64
from contextlib import closing
import eventlet
from oslo_config import cfg
@ -218,6 +220,32 @@ class LXDDriverTest(test.NoDBTestCase):
self.assertEqual(['mock-instance-1', 'mock-instance-2'], instances)
@mock.patch('nova.virt.lxd.driver.IMAGE_API')
@mock.patch('nova.virt.lxd.driver.lockutils.lock')
def test_spawn_unified_image(self, lock, IMAGE_API=None):
def image_get(*args, **kwargs):
raise lxdcore_exceptions.LXDAPIException(MockResponse(404))
self.client.images.get_by_alias.side_effect = image_get
self.client.images.exists.return_value = False
image = {'name': mock.Mock(), 'disk_format': 'raw'}
IMAGE_API.get.return_value = image
def download_unified(*args, **kwargs):
# unified image with metadata
# structure is gzipped tarball, content:
# /
# metadata.yaml
# rootfs/
unified_tgz = 'H4sIALpegVkAA+3SQQ7CIBCFYY7CCXRAppwHo66sTVpYeHsh0a'\
'Ru1A2Lxv/bDGQmYZLHeM7plHLa3dN4NX1INQyhVRdV1vXFuIML'\
'4lVVopF28cZKp33elCWn2VpTjuWWy4e5L/2NmqcpX5Z91zdawD'\
'HqT/kHrf/E+Xo0Vrtu9fTn+QMAAAAAAAAAAAAAAADYrgfk/3zn'\
'ACgAAA=='
with closing(open(kwargs['dest_path'], 'wb+')) as img:
img.write(base64.b64decode(unified_tgz))
IMAGE_API.download = download_unified
self.test_spawn()
@mock.patch('nova.virt.configdrive.required_by')
def test_spawn(self, configdrive, neutron_failure=None):
def container_get(*args, **kwargs):

View File

@ -24,10 +24,13 @@ import shutil
import socket
import tarfile
import tempfile
import hashlib
import eventlet
import nova.conf
import nova.context
from contextlib import closing
from nova import exception
from nova import i18n
from nova import image
@ -236,26 +239,92 @@ def _sync_glance_image_to_lxd(client, context, image_ref):
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"
# It is possible that LXD already have the same image
# but NOT aliased as result of previous publish/export operation
# (snapshot from openstack).
# In that case attempt to add it again
# (implicitly via instance launch from affected image) will produce
# LXD error - "Image with same fingerprint already exists".
# Error does not have unique identifier to handle it we calculate
# fingerprint of image as LXD do it and check if LXD already have
# image with such fingerprint.
# If any we will add alias to this image and will not re-import it
def add_alias():
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()
def lxdimage_fingerprint():
def sha256_file():
sha256 = hashlib.sha256()
with closing(open(image_file, 'rb')) as f:
for block in iter(lambda: f.read(65536), b''):
sha256.update(block)
return sha256.hexdigest()
with open(manifest_file, 'rb') as manifest:
return sha256_file()
fingerprint = lxdimage_fingerprint()
if client.images.exists(fingerprint):
LOG.info(
'Image with fingerprint %(fingerprint)s already exists'
'but not accessible by alias %(alias)s, add alias',
{'fingerprint': fingerprint, 'alias': image_ref})
lxdimage = client.images.get(fingerprint)
lxdimage.add_alias(image_ref, '')
return True
return False
if add_alias():
return
# up2date LXD publish/export operations produce images which
# already contains /rootfs and metdata.yaml in exported file.
# We should not pass metdata explicitly in that case as imported
# image will be unusable bacause LXD will think that it containts
# rootfs and will not extract embedded /rootfs properly.
# Try to detect if image content already has metadata and not pass
# explicit metadata in that case
def imagefile_has_metadata(image_file):
try:
with closing(tarfile.TarFile.open(
name=image_file, mode='r:*')) as tf:
try:
tf.getmember('metadata.yaml')
return True
except KeyError:
pass
except tarfile.ReadError:
pass
return False
if imagefile_has_metadata(image_file):
LOG.info('Image %(alias)s already has metadata, '
'skipping metadata injection...',
{'alias': image_ref})
with open(image_file, 'rb') as image:
image = client.images.create(
image.read(), metadata=manifest.read(),
wait=True)
image = client.images.create(image.read(), wait=True)
else:
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: