Example #1
0
    def _systemctl(self):
        """
        Setup ``systemd`` unit files in the image.
        """
        if self.systemctl:
            for action in ("enable", "disable", "mask"):
                for unit in self.systemctl.get(action, []):
                    SystemdImage.machine_systemctl(self.image_path, action,
                                                   unit)

        # Clear {image}/etc/machine-id,
        # else subsequent containers get that machine ID.
        machine_id = os.path.join(self.image_path, "etc", "machine-id")
        if os.path.exists(machine_id):
            os.remove(machine_id)
Example #2
0
    def __init__(self, config, runtime_config=None):
        """
        Initialisation with the configuration data.

        Optional runtime configuration can be loaded,
        for local proxies, optimisations, name overrides, etc.

        :param config: configuration data
        :type config: dict
        :param runtime_config: local runtime configuration
        :type runtime_config: dict
        """
        self.all_config = config
        self.config = config["image"]
        if runtime_config:
            self.runtime_config = runtime_config.get("image", {})
        else:
            self.runtime_config = {}

        # These are required options and will not be missing
        self.name = self.config["name"]
        self.version = str(self.config["version"])
        if self.version == "now":
            self.version = datetime.now().strftime("%Y%m%dT%H%M%S")
            self.config["version"] = self.version
        self.build = self.config["build"]

        # These are not required
        self.architecture = self.config.get("architecture")
        if not self.architecture:
            machine = platform.machine()

            # We can't assume every distro reports i686.
            if machine in ("i386", "i486", "i586", "i686"):
                self.architecture = "x86"
            else:
                self.architecture = "x86-64"

            self.all_config["image"]["architecture"] = self.architecture

        self.srcimage = self.config.get("srcimage")
        self.systemctl = self.config.get("systemctl")

        self.image_path = None
        self.config_path = None
        self.is_latest = False
        self.log_path = os.path.join(BASE_LOG_DIR, self.name, self.version)

        # Reusable builders that are setup with user configurations
        self.systemd_builder = SystemdImage(self.runtime_config)
Example #3
0
    def __init__(self, name, config, runtime_config):
        """
        Delete an image.

        :param name: image name, not strictly needed.
        :type name: str
        :param config: configuration of the image
        :type config: dict
        :param runtime_config: runtime configuration
        :type runtime_config: dict
        """
        super().__init__(name, config, runtime_config)
        self.path = os.path.join(BASE_IMAGE_DIR, self.config["image"]["name"],
                                 self.config["image"]["version"])
        self.systemd = SystemdImage(runtime_config)
        self.unit_name = "{}.mount".format(self.systemd._escape(self.path))
Example #4
0
class Builder(object):
    """
    Build a ``systemd-nspawn``able image.

    :raises: sponson.image.build.BuilderError
    """
    def __init__(self, config, runtime_config=None):
        """
        Initialisation with the configuration data.

        Optional runtime configuration can be loaded,
        for local proxies, optimisations, name overrides, etc.

        :param config: configuration data
        :type config: dict
        :param runtime_config: local runtime configuration
        :type runtime_config: dict
        """
        self.all_config = config
        self.config = config["image"]
        if runtime_config:
            self.runtime_config = runtime_config.get("image", {})
        else:
            self.runtime_config = {}

        # These are required options and will not be missing
        self.name = self.config["name"]
        self.version = str(self.config["version"])
        if self.version == "now":
            self.version = datetime.now().strftime("%Y%m%dT%H%M%S")
            self.config["version"] = self.version
        self.build = self.config["build"]

        # These are not required
        self.architecture = self.config.get("architecture")
        if not self.architecture:
            machine = platform.machine()

            # We can't assume every distro reports i686.
            if machine in ("i386", "i486", "i586", "i686"):
                self.architecture = "x86"
            else:
                self.architecture = "x86-64"

            self.all_config["image"]["architecture"] = self.architecture

        self.srcimage = self.config.get("srcimage")
        self.systemctl = self.config.get("systemctl")

        self.image_path = None
        self.config_path = None
        self.is_latest = False
        self.log_path = os.path.join(BASE_LOG_DIR, self.name, self.version)

        # Reusable builders that are setup with user configurations
        self.systemd_builder = SystemdImage(self.runtime_config)

    def start(self):
        """
        Start building the image.
        """
        logger.info("Starting")
        logger.info("Sanity check")
        self._sanity_check()
        logger.info("Mounting source image")
        self._srcimage()
        logger.info("Build")
        self._build_steps()
        logger.info("Clean up")
        self._build_clean_up()
        logger.info("Setup systemd unit files")
        self._systemctl()
        logger.info("Linking latest image")
        self._link_latest_image()
        logger.info("Persisting configuration")
        self.persist_config()
        logger.info("Finished")

    def _sanity_check(self):
        """
        Check if the image path already exists,
        and if so raise an error.
        :raises: sponson.image.build.BuilderError
        """
        self.image_path = os.path.join(BASE_IMAGE_DIR, self.name, self.version)
        self.config_path = os.path.join(BASE_CONF_DIR, self.name,
                                        "{}.yaml".format(self.version))

        if os.path.exists(self.image_path):
            raise BuilderError("Image path already exists")

        try:
            os.makedirs(self.image_path)
        except PermissionError:
            raise BuilderError("Insufficient permissions to make image")

        if not os.path.exists(os.path.dirname(self.config_path)):
            try:
                os.makedirs(os.path.dirname(self.config_path))
            except PermissionError:
                raise BuilderError("Insufficient permissions to save config")

        if self.srcimage:
            srcimage = check_if_versioned(self.srcimage)
            if not srcimage or not os.path.exists(srcimage):
                raise BuilderError("Cannot find source image")

    def _image_dest_path(self, dest_path):
        """
        Create and check a path that should be relative to the image directory.

        :param dest_path: path in the image directory
        :type dest_path: str
        :return: full path rooted in the image directory
        :rtype: str
        """
        # os.path.join throws away path arguments
        # if one argument is an absolute path
        dest = os.path.join(self.image_path, dest_path.lstrip("/"))

        if not dest.startswith(BASE_IMAGE_DIR):
            raise BuilderError("Operating outside image directory: "
                               "{}".format(dest))

        return dest

    def _check_if_version(self, image):
        """
        Checks if supplied image is versioned.

        :param image: image with possible version to check
        :type image: str
        :return: sanitised/checked image string
        :rtype: str
        :raises: BuilderError
        """
        version = check_if_versioned(image)
        if version:
            return version
        else:
            raise BuilderError("Cannot determine version of the image")

    def _setup_dnf_builder(self):
        """
        Set up the DNF builder.
        """
        dnf_conf = {}
        if self.config.get("dnf"):
            dnf_conf = self.config["dnf"]

        dnf_runtime = None
        if (self.runtime_config.get("build") and
                self.runtime_config["build"].get("dnf")):
            dnf_runtime = self.runtime_config["build"].get("dnf")

        self.dnf_builder = DNFBuilder(self.image_path, self.log_path,
                                      self.architecture, dnf_conf, dnf_runtime)

    def _build_steps(self):
        """
        Iterate through the build steps on the image.
        """
        # Setup the builders first
        self._setup_dnf_builder()

        for count, build in enumerate(self.build):
            try:
                # This should only ever be one key in the dictionary
                for action, args in build.items():
                    func_name = "_build_step_{}".format(action)
                    if hasattr(self, func_name):
                        f = getattr(self, func_name)
                        logger.info("Start {}{}".format(count, func_name))
                        f(args)
                        logger.info("Finish {}{}".format(count, func_name))
            except KeyError:
                continue

    def _build_step_dnf(self, args):
        """
        DNF build step
        :param args: dictionary of options for build step
        :type args: dict
        """
        if "pkg" not in args and "pkglist" not in args:
            raise BuilderError("Missing required DNF build step arguments")

        if "pkg" in args:
            pkglist = [args["pkg"]]
        else:
            pkglist = args["pkglist"]

        try:
            self.dnf_builder.install(pkglist)
        except DNFError:
            logger.exception("DNF build step error")
            raise BuilderError("DNF build step error")

    def _build_step_copy(self, args):
        """
        Copy build step.

        This is recursive.

        The ``args["dest"]`` setting is always
        relative to the image directory,
        such that ``/etc/foo/bar.conf`` is really
        ``/var/lib/sponson/images/{name}/{version}/etc/foo/bar.conf``.

        :param args: dictionary of options for build step
        :type args: dict
        """
        if "src" not in args or "dest" not in args:
            raise BuilderError("Missing required copy build step arguments")

        if "srcimage" in args:
            # Check if the srcimage we've been given
            # is the name or the actual version
            image_vers = self._check_if_version(args["srcimage"])
            mount_src = args["src"].lstrip("/")
            source = os.path.join(image_vers, mount_src)
        else:
            source = os.path.abspath(args["src"])

        dest = self._image_dest_path(args["dest"])

        safe_copy(source, dest, args.get("owner"), args.get("group"),
                  args.get("chmod"))

    def _build_step_delete(self, args):
        """
        Delete build step
        :param args: dictionary of options for build step
        :type args: dict
        """
        if "targets" not in args:
            raise BuilderError("Missing required delete build step arguments")

        for target in args["target"]:
            img_target = self._image_dest_path(target)

            if not os.path.exists(img_target):
                continue

            if os.path.isdir(img_target):
                shutil.rmtree(img_target, args.get("force", False))
            else:
                os.unlink(img_target)

    def _build_step_run(self, args):
        """
        Run build step
        :param args: dictionary of options for build step
        :type args: dict
        """
        if "command" not in args:
            raise BuilderError("Missing required run build step arguments")

        command = args["command"].format(image=self.image_path).split()
        container = args.get("container", True)
        fail = args.get("fail", True)
        user = args.get("user")

        process = BuildRunProcess(self.image_path, command, user, fail,
                                  container)

        if process:
            try:
                process.start()
                process.join()
            except (subprocess.CalledProcessError,
                    multiprocessing.ProcessError):
                logger.exception("Process error")
                if fail:
                    raise BuilderError("Run build step failed")
            else:
                if fail and process.exitcode != 0:
                    raise BuilderError("Run build step failed")
        else:
            logger.error("Cannot create run build step")

    def _build_clean_up(self):
        """
        Clean up after builders.
        """
        if self.dnf_builder:
            self.dnf_builder.finish()

    def _srcimage(self):
        """
        Mount source image(s) into the image.
        """
        if self.srcimage:
            self.systemd_builder.mount_srcimage(self.name, self.version,
                                                self.srcimage)

    def _systemctl(self):
        """
        Setup ``systemd`` unit files in the image.
        """
        if self.systemctl:
            for action in ("enable", "disable", "mask"):
                for unit in self.systemctl.get(action, []):
                    SystemdImage.machine_systemctl(self.image_path, action,
                                                   unit)

        # Clear {image}/etc/machine-id,
        # else subsequent containers get that machine ID.
        machine_id = os.path.join(self.image_path, "etc", "machine-id")
        if os.path.exists(machine_id):
            os.remove(machine_id)

    def _link_latest_image(self):
        """
        Link the latest version of the image to ``/latest``
        """
        image_name_dir = os.path.join(BASE_IMAGE_DIR, self.name)
        latest_image = os.path.join(image_name_dir, "latest")

        if not os.path.exists(latest_image):
            # Assume this image is the latest if the latest symlink is missing
            os.symlink(self.image_path, latest_image)
            self.is_latest = True
            return

        if not os.path.islink(latest_image):
            # This isn't technically fatal, but should raise a warning
            logger.warning(
                "Latest image is not a symlink, not trying to link")
            return

        self.is_latest = link_latest_image(image_name_dir)

    def persist_config(self):
        """
        Write out the configuration data to a YAML file.
        """
        configfile.write_config_file(self.all_config, self.config_path)

        img_sponson_dir = os.path.join(self.image_path, "etc", "sponson")
        os.makedirs(img_sponson_dir, exist_ok=True)

        img_sponson_conf = os.path.join(
            img_sponson_dir, "{}.{}.yaml".format(self.name, self.version))
        configfile.write_config_file(self.all_config, img_sponson_conf)

        if self.is_latest:
            latest_config = os.path.join(BASE_CONF_DIR, self.name,
                                         "latest.yaml")

            if os.path.exists(latest_config):
                os.remove(latest_config)
            os.symlink(self.config_path, latest_config)
Example #5
0
class DeleteImage(Delete):
    """
    Delete images.
    """
    def __init__(self, name, config, runtime_config):
        """
        Delete an image.

        :param name: image name, not strictly needed.
        :type name: str
        :param config: configuration of the image
        :type config: dict
        :param runtime_config: runtime configuration
        :type runtime_config: dict
        """
        super().__init__(name, config, runtime_config)
        self.path = os.path.join(BASE_IMAGE_DIR, self.config["image"]["name"],
                                 self.config["image"]["version"])
        self.systemd = SystemdImage(runtime_config)
        self.unit_name = "{}.mount".format(self.systemd._escape(self.path))

    def _sanity_check(self):
        """
        Image deletion sanity check.
        """
        containers = []
        for container_fname in os.listdir(ETC_CONTAINER_CONF_DIR):
            container_config = read_config_file(
                os.path.join(ETC_CONTAINER_CONF_DIR, container_fname))

            if (container_config["image"]["name"] ==
                    self.config["image"]["name"] and
                    container_config["image"]["version"] ==
                    self.config["image"]["version"]):
                containers.append(container_config["container"]["name"])

        if containers:
            raise DeleteError("Containers require this image: \n\t"
                              "{}".format("\n\t".join(containers)))

    def _clean_sponson_conf(self):
        """
        Remove sponson image configuration files.
        """
        config_base_path = os.path.join(BASE_CONF_DIR,
                                        self.config["image"]["name"])
        config_path = os.path.join(
            config_base_path,
            "{}.yaml".format(self.config["image"]["version"]))
        latest_path = os.path.join(config_base_path, "latest.yaml")

        os.remove(config_path)

        if os.readlink(latest_path) == config_path:
            os.unlink(latest_path)

            remaining = sorted(os.listdir(config_base_path))

            if remaining:
                new_latest = os.path.join(config_base_path, remaining.pop())
                os.link(new_latest, latest_path)

    def _clean_dir(self):
        """
        Image removal specific cleaning function,
        to relink the "latest" image version
        if the removed directory was the previous "latest" version.
        """
        super()._clean_dir()

        link_latest_image(os.path.join(BASE_IMAGE_DIR,
                                       self.config["image"]["name"]))