#!/usr/bin/python3 """ Assemble tree into a raw filesystem image Assemble the tree into a raw filesystem image named `filename`, with the UUID `root_fs_uuid`. The image is a sparse file of the given `size`, which is created using the `truncate(1)` command. The `size` is an integer with an optional suffix: K,M,G,T,... (for powers of 1024) or KB,MB,GB,TB,... (powers of 1000). NOTE: If the tree contents are larger than `size`, this assembler will fail. On the other hand, since the image is a sparse file, the unused parts of the image take up almost no disk space - so a 1GB tree in a 20GB image should not use much more than 1GB disk space. The filesystem UUID should be a standard (RFC4122) UUID, which you can generate with uuid.uuid4() in Python, `uuidgen(1)` in a shell script, or read from `/proc/sys/kernel/random/uuid` if your kernel provides it. """ import contextlib import os import subprocess import sys import osbuild.api import osbuild.remoteloop as remoteloop SCHEMA = """ "additionalProperties": false, "required": ["filename", "root_fs_uuid", "size"], "properties": { "filename": { "description": "Raw filesystem image filename", "type": "string" }, "root_fs_uuid": { "description": "UUID for the filesystem", "type": "string", "pattern": "^[0-9A-Za-z]{8}(-[0-9A-Za-z]{4}){3}-[0-9A-Za-z]{12}$", "examples": ["9c6ae55b-cf88-45b8-84e8-64990759f39d"] }, "size": { "description": "Maximum size of the filesystem", "type": "integer" }, "fs_type": { "description": "Filesystem type", "type": "string", "enum": ["ext4", "xfs", "btrfs"], "default": "ext4" } } """ @contextlib.contextmanager def mount(source, dest, *options): os.makedirs(dest, 0o755, True) subprocess.run(["mount", *options, source, dest], check=True) try: yield finally: subprocess.run(["umount", "-R", dest], check=True) def mkfs_ext4(device, uuid): subprocess.run(["mkfs.ext4", "-U", uuid, device], input="y", encoding='utf8', check=True) def mkfs_xfs(device, uuid): subprocess.run(["mkfs.xfs", "-m", f"uuid={uuid}", device], encoding='utf8', check=True) def mkfs_btrfs(device, uuid): subprocess.run(["mkfs.btrfs", "-U", uuid, device], encoding='utf8', check=True) def main(tree, output_dir, options, loop_client): filename = options["filename"] root_fs_uuid = options["root_fs_uuid"] size = options["size"] fs_type = options.get("fs_type", "ext4") image = "/var/tmp/osbuild-image.raw" mountpoint = "/tmp/osbuild-mnt" subprocess.run(["truncate", "--size", str(size), image], check=True) if fs_type == "ext4": mkfs_ext4(image, root_fs_uuid) elif fs_type == "xfs": mkfs_xfs(image, root_fs_uuid) elif fs_type == "btrfs": mkfs_btrfs(image, root_fs_uuid) else: raise ValueError("`fs_type` must be ext4, xfs or btrfs") # Copy the tree into the target image with loop_client.device(image) as loop, mount(loop, mountpoint): subprocess.run(["cp", "-a", f"{tree}/.", mountpoint], check=True) subprocess.run(["mv", image, f"{output_dir}/{filename}"], check=True) if __name__ == '__main__': args = osbuild.api.arguments() args_input = args["inputs"]["tree"]["path"] args_output = args["tree"] r = main(args_input, args_output, args["options"], remoteloop.LoopClient("/run/osbuild/api/remoteloop")) sys.exit(r)