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:
parent
78b6c14f2c
commit
b947a9afb3
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue