# This file is part of cloud-init. See LICENSE file for license information. """LXD Image Base Class.""" import os import shutil import tempfile from ..images import Image from .snapshot import LXDSnapshot from cloudinit import subp from cloudinit import util as c_util from tests.cloud_tests import util class LXDImage(Image): """LXD backed image.""" platform_name = "lxd" def __init__(self, platform, config, pylxd_image): """Set up image. @param platform: platform object @param config: image configuration """ self.modified = False self._img_instance = None self._pylxd_image = None self.pylxd_image = pylxd_image super(LXDImage, self).__init__(platform, config) @property def pylxd_image(self): """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._img_instance: self._instance.destroy() self._img_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): """Internal use only, returns a instance This starts an lxc instance from the image, so it is "dirty". Better would be some way to modify this "at rest". lxc-pstart would be an option.""" if not self._img_instance: self._img_instance = self.platform.launch_container( self.properties, self.config, self.features, use_desc='image-modification', image_desc=str(self), image=self.pylxd_image.fingerprint) self._img_instance.start() return self._img_instance @property def properties(self): """{} containing: 'arch', 'os', 'version', 'release'.""" properties = self.pylxd_image.properties return { 'arch': properties.get('architecture'), 'os': properties.get('os'), 'version': properties.get('version'), 'release': properties.get('release'), } def export_image(self, output_dir): """Export image from lxd image store to disk. @param output_dir: dir to store the exported image in @return_value: tuple of path to metadata tarball and rootfs Only the "split" image format with separate rootfs and metadata files is supported, e.g: 71f171df[...]cd31.squashfs (could also be: .tar.xz or .tar.gz) meta-71f171df[...]cd31.tar.xz Combined images made by a single tarball are not supported. """ # pylxd's image export feature doesn't do split exports, so use cmdline fp = self.pylxd_image.fingerprint subp.subp(['lxc', 'image', 'export', fp, output_dir], capture=True) image_files = [p for p in os.listdir(output_dir) if fp in p] if len(image_files) != 2: raise NotImplementedError( "Image %s has unsupported format. " "Expected 2 files, found %d: %s." % (fp, len(image_files), ', '.join(image_files))) metadata = os.path.join( output_dir, next(p for p in image_files if p.startswith('meta-'))) rootfs = os.path.join( output_dir, next(p for p in image_files 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') subp.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'.""" return self._instance.push_file(local_path, remote_path) def run_script(self, *args, **kwargs): """Run script in image, modifying image. @return_value: script output """ return self._instance.run_script(*args, **kwargs) def snapshot(self): """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', 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 LXDSnapshot(self.platform, self.properties, self.config, self.features, instance) def destroy(self): """Clean up data associated with image.""" self.pylxd_image = None super(LXDImage, self).destroy() # vi: ts=4 expandtab