#!/usr/bin/python3 """ Deploy an OStree commit Create an OSTree deployment[1] for a given ref. Since OStree internally uses a hardlink farm to create the file system tree for the deployment from the commit data, the mountpoints for the final image need to be supplied via the `mounts` option, as hardlinks must not span across file systems and therefore the boundaries need to be known when doing the deployment. Creating a deployment also entails generating the Boot Loader Specification entries to boot the system, which contain this the kernel command line. The `rootfs` option can be used to indicate the root file system, containing the sysroot and the deployments. Additional kernel options can be passed via `kernel_opts`. [1] https://ostree.readthedocs.io/en/latest/manual/deployment/ """ import os import subprocess import sys import tempfile import osbuild.api from osbuild.util.mnt import MountGuard from osbuild.util import containers CAPABILITIES = ["CAP_MAC_ADMIN"] SCHEMA_2 = """ "options": { "additionalProperties": false, "required": ["osname", "ref"], "properties": { "mounts": { "description": "Mount points of the final file system", "type": "array", "items": { "description": "Description of one mount point", "type": "string" } }, "osname": { "description": "Name of the stateroot to be used in the deployment", "type": "string" }, "kernel_opts": { "description": "Additional kernel command line options", "type": "array", "items": { "description": "A single kernel command line option", "type": "string" } }, "ref": { "description": "OStree ref to use for the deployment", "type": "string" }, "remote": { "description": "optional OStree remote to use for the deployment", "type": "string" }, "container": { "description": "Bool to indicate if the deployment is a container", "type": "boolean" }, "rootfs": { "description": "Identifier to locate the root file system", "type": "object", "oneOf": [{ "required": ["uuid"] }, { "required": ["label"] }], "properties": { "label": { "description": "Identify the root file system by label", "type": "string" }, "uuid": { "description": "Identify the root file system by UUID", "type": "string" } } } } }, "inputs": { "type": "object", "additionalProperties": false, "required": ["images"], "properties": { "images": { "type": "object", "additionalProperties": true }, "manifest-lists": { "type": "object", "description": "Optional manifest lists to merge into images. The metadata must specify an image ID to merge to.", "additionalProperties": true } } } """ def ostree(*args, _input=None, **kwargs): args = list(args) + [f'--{k}={v}' for k, v in kwargs.items()] print("ostree " + " ".join(args), file=sys.stderr) subprocess.run(["ostree"] + args, encoding="utf8", stdout=sys.stderr, input=_input, check=True) def make_fs_identifier(desc): for key in ["uuid", "label"]: val = desc.get(key) if val: return f"{key.upper()}={val}" raise ValueError("unknown rootfs type") def main(inputs, tree, options): osname = options["osname"] rootfs = options.get("rootfs") mounts = options.get("mounts", []) kopts = options.get("kernel_opts", []) ref = options["ref"] remote = options.get("remote") is_container = options.get("container") kargs = [] if len(inputs) == 0: # this is the old path if len(inputs) > 1: error if inputs[0].type == container: if (not is_container): if remote: ref = f"{remote}:{ref}" if rootfs: rootfs_id = make_fs_identifier(rootfs) kargs += [f"--karg=root={rootfs_id}"] for opt in kopts: kargs += [f"--karg-append={opt}"] else: for opt in kopts: kargs += [f"--karg={opt}"] with MountGuard() as mounter: for mount in mounts: path = mount.lstrip("/") path = os.path.join(tree, path) mounter.mount(path, path) if (not is_container): ostree("admin", "deploy", ref, *kargs, sysroot=tree, os=osname) else: images = containers.parse_containers_input(inputs) for image in images.values(): source = image["filepath"] source_data = image["data"] container_format = source_data["format"] image_name = source_data["name"] with tempfile.TemporaryDirectory() as tmpdir: tmp_source = os.path.join(tmpdir, "image") if container_format == "dir" and image["manifest-list"]: # copy the source container to the tmp source so we can merge the manifest into it subprocess.run(["cp", "-a", "--reflink=auto", source, tmp_source], check=True) containers.merge_manifest(image["manifest-list"], tmp_source) else: # We can't have special characters like ":" in the source names because containers/image # treats them special, like e.g. /some/path:tag, so we make a symlink to the real name # and pass the symlink name to skopeo to make it work with anything os.symlink(source, tmp_source) if container_format not in ("dir", "oci-archive"): raise RuntimeError(f"Unknown container format {container_format}") source = f"{container_format}:{tmp_source}" # create a temporary directory to store the ostree commit id after deploying the container image os.mkdir(f"{tree}/tmp") imgref = f"{remote}:{source}" extra_args = [] if remote: extra_args.append(f'--imgref={imgref}') extra_args.append(f'--stateroot={osname}') extra_args.append(f'--write-commitid-to={tree}/tmp/commit.txt') # hard coded for now, but we can modify it later to take a parameter extra_args.append('--target-imgref=ostree-unverified-registry:quay.io/fedora/fedora-coreos:stable') ostree("container", "image", "deploy", *extra_args, sysroot=tree, *kargs) if __name__ == '__main__': stage_args = osbuild.api.arguments() r = main(stage_args["inputs"], stage_args["tree"], stage_args["options"]) sys.exit(r)