diff options
Diffstat (limited to 'tests/cloud_tests/images/lxd.py')
-rw-r--r-- | tests/cloud_tests/images/lxd.py | 176 |
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 |