summaryrefslogtreecommitdiff
path: root/tests/cloud_tests/images/lxd.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/cloud_tests/images/lxd.py')
-rw-r--r--tests/cloud_tests/images/lxd.py176
1 files changed, 137 insertions, 39 deletions
diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py
index 7a416141..fd4e93c2 100644
--- a/tests/cloud_tests/images/lxd.py
+++ b/tests/cloud_tests/images/lxd.py
@@ -1,43 +1,67 @@
# This file is part of cloud-init. See LICENSE file for license information.
+"""LXD Image Base Class."""
+
+import os
+import shutil
+import tempfile
+
+from cloudinit import util as c_util
from tests.cloud_tests.images import base
from tests.cloud_tests.snapshots import lxd as lxd_snapshot
+from tests.cloud_tests import util
class LXDImage(base.Image):
- """
- LXD backed image
- """
+ """LXD backed image."""
+
platform_name = "lxd"
- def __init__(self, name, config, platform, pylxd_image):
- """
- setup
+ def __init__(self, platform, config, pylxd_image):
+ """Set up image.
+
+ @param platform: platform object
+ @param config: image configuration
"""
- self.platform = platform
- self._pylxd_image = pylxd_image
+ self.modified = False
self._instance = None
- super(LXDImage, self).__init__(name, config, platform)
+ self._pylxd_image = None
+ self.pylxd_image = pylxd_image
+ super(LXDImage, self).__init__(platform, config)
@property
def pylxd_image(self):
- self._pylxd_image.sync()
+ """Property function."""
+ if self._pylxd_image:
+ self._pylxd_image.sync()
return self._pylxd_image
+ @pylxd_image.setter
+ def pylxd_image(self, pylxd_image):
+ if self._instance:
+ self._instance.destroy()
+ self._instance = None
+ if (self._pylxd_image and
+ (self._pylxd_image is not pylxd_image) and
+ (not self.config.get('cache_base_image') or self.modified)):
+ self._pylxd_image.delete(wait=True)
+ self.modified = False
+ self._pylxd_image = pylxd_image
+
@property
def instance(self):
+ """Property function."""
if not self._instance:
self._instance = self.platform.launch_container(
- image=self.pylxd_image.fingerprint,
- image_desc=str(self), use_desc='image-modification')
- self._instance.start(wait=True, wait_time=self.config.get('timeout'))
+ self.properties, self.config, self.features,
+ use_desc='image-modification', image_desc=str(self),
+ image=self.pylxd_image.fingerprint)
+ self._instance.start()
return self._instance
@property
def properties(self):
- """
- {} containing: 'arch', 'os', 'version', 'release'
- """
+ """{} containing: 'arch', 'os', 'version', 'release'."""
properties = self.pylxd_image.properties
return {
'arch': properties.get('architecture'),
@@ -46,47 +70,121 @@ class LXDImage(base.Image):
'release': properties.get('release'),
}
- def execute(self, *args, **kwargs):
+ def export_image(self, output_dir):
+ """Export image from lxd image store to (split) tarball on disk.
+
+ @param output_dir: dir to store tarballs in
+ @return_value: tuple of path to metadata tarball and rootfs tarball
"""
- execute command in image, modifying image
+ # pylxd's image export feature doesn't do split exports, so use cmdline
+ c_util.subp(['lxc', 'image', 'export', self.pylxd_image.fingerprint,
+ output_dir], capture=True)
+ tarballs = [p for p in os.listdir(output_dir) if p.endswith('tar.xz')]
+ metadata = os.path.join(
+ output_dir, next(p for p in tarballs if p.startswith('meta-')))
+ rootfs = os.path.join(
+ output_dir, next(p for p in tarballs if not p.startswith('meta-')))
+ return (metadata, rootfs)
+
+ def import_image(self, metadata, rootfs):
+ """Import image to lxd image store from (split) tarball on disk.
+
+ Note, this will replace and delete the current pylxd_image
+
+ @param metadata: metadata tarball
+ @param rootfs: rootfs tarball
+ @return_value: imported image fingerprint
+ """
+ alias = util.gen_instance_name(
+ image_desc=str(self), use_desc='update-metadata')
+ c_util.subp(['lxc', 'image', 'import', metadata, rootfs,
+ '--alias', alias], capture=True)
+ self.pylxd_image = self.platform.query_image_by_alias(alias)
+ return self.pylxd_image.fingerprint
+
+ def update_templates(self, template_config, template_data):
+ """Update the image's template configuration.
+
+ Note, this will replace and delete the current pylxd_image
+
+ @param template_config: config overrides for template metadata
+ @param template_data: template data to place into templates/
"""
+ # set up tmp files
+ export_dir = tempfile.mkdtemp(prefix='cloud_test_util_')
+ extract_dir = tempfile.mkdtemp(prefix='cloud_test_util_')
+ new_metadata = os.path.join(export_dir, 'new-meta.tar.xz')
+ metadata_yaml = os.path.join(extract_dir, 'metadata.yaml')
+ template_dir = os.path.join(extract_dir, 'templates')
+
+ try:
+ # extract old data
+ (metadata, rootfs) = self.export_image(export_dir)
+ shutil.unpack_archive(metadata, extract_dir)
+
+ # update metadata
+ metadata = c_util.read_conf(metadata_yaml)
+ templates = metadata.get('templates', {})
+ templates.update(template_config)
+ metadata['templates'] = templates
+ util.yaml_dump(metadata, metadata_yaml)
+
+ # write out template files
+ for name, content in template_data.items():
+ path = os.path.join(template_dir, name)
+ c_util.write_file(path, content)
+
+ # store new data, mark new image as modified
+ util.flat_tar(new_metadata, extract_dir)
+ self.import_image(new_metadata, rootfs)
+ self.modified = True
+
+ finally:
+ # remove tmpfiles
+ shutil.rmtree(export_dir)
+ shutil.rmtree(extract_dir)
+
+ def execute(self, *args, **kwargs):
+ """Execute command in image, modifying image."""
return self.instance.execute(*args, **kwargs)
def push_file(self, local_path, remote_path):
- """
- copy file at 'local_path' to instance at 'remote_path', modifying image
- """
+ """Copy file at 'local_path' to instance at 'remote_path'."""
return self.instance.push_file(local_path, remote_path)
- def run_script(self, script):
- """
- run script in image, modifying image
- return_value: script output
+ def run_script(self, *args, **kwargs):
+ """Run script in image, modifying image.
+
+ @return_value: script output
"""
- return self.instance.run_script(script)
+ return self.instance.run_script(*args, **kwargs)
def snapshot(self):
- """
- create snapshot of image, block until done
- """
- # clone current instance, start and freeze clone
+ """Create snapshot of image, block until done."""
+ # get empty user data to pass in to instance
+ # if overrides for user data provided, use them
+ empty_userdata = util.update_user_data(
+ {}, self.config.get('user_data_overrides', {}))
+ conf = {'user.user-data': empty_userdata}
+ # clone current instance
instance = self.platform.launch_container(
+ self.properties, self.config, self.features,
container=self.instance.name, image_desc=str(self),
- use_desc='snapshot')
- instance.start(wait=True, wait_time=self.config.get('timeout'))
+ use_desc='snapshot', container_config=conf)
+ # wait for cloud-init before boot_clean_script is run to ensure
+ # /var/lib/cloud is removed cleanly
+ instance.start(wait=True, wait_for_cloud_init=True)
if self.config.get('boot_clean_script'):
instance.run_script(self.config.get('boot_clean_script'))
+ # freeze current instance and return snapshot
instance.freeze()
return lxd_snapshot.LXDSnapshot(
- self.properties, self.config, self.platform, instance)
+ self.platform, self.properties, self.config,
+ self.features, instance)
def destroy(self):
- """
- clean up data associated with image
- """
- if self._instance:
- self._instance.destroy()
- self.pylxd_image.delete(wait=True)
+ """Clean up data associated with image."""
+ self.pylxd_image = None
super(LXDImage, self).destroy()
# vi: ts=4 expandtab