From 255767ee9f6ea0c466fc8a35153b5c0dcd581a6d Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Wed, 20 Sep 2023 11:39:09 +0200 Subject: [PATCH] Add support for runtime trees Let's allow mounting various directories into containers/VMs that we run using a new RuntimeTrees= option. For containers, we use nspawn's --bind option. For VMs, we use virtiofs along with the virtiofsd project. --- mkosi/__init__.py | 8 ++++ mkosi/config.py | 10 +++++ mkosi/qemu.py | 95 +++++++++++++++++++++++++++++++++++----- mkosi/resources/mkosi.md | 15 +++++++ 4 files changed, 116 insertions(+), 12 deletions(-) diff --git a/mkosi/__init__.py b/mkosi/__init__.py index af88f10b7..405283e48 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -2057,6 +2057,14 @@ def run_shell(args: MkosiArgs, config: MkosiConfig) -> None: else: cmdline += ["--image", fname] + for src, tgt in config.runtime_trees: + # We add norbind because very often RuntimeTrees= will be used to mount the source directory into the + # container and the output directory from which we're running will very likely be a subdirectory of the + # source directory which would mean we'd be mounting the container root directory as a subdirectory in + # itself which tends to lead to all kinds of weird issues, which we avoid by not doing a recursive mount + # which means the container root directory mounts will be skipped. + cmdline += ["--bind", f"{src}:{tgt or f'/root/src/{src.name}'}:norbind,rootidmap"] + if args.verb == Verb.boot: # Add nspawn options first since systemd-nspawn ignores all options after the first argument. cmdline += args.cmdline diff --git a/mkosi/config.py b/mkosi/config.py index 64591d19a..f2341464c 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -729,6 +729,7 @@ class MkosiConfig: tools_tree_distribution: Optional[Distribution] tools_tree_release: Optional[str] tools_tree_packages: list[str] + runtime_trees: list[tuple[Path, Optional[Path]]] # QEMU-specific options qemu_gui: bool @@ -1680,6 +1681,14 @@ SETTINGS = ( parse=config_make_list_parser(delimiter=","), help="Add additional packages to the tools tree", ), + MkosiConfigSetting( + dest="runtime_trees", + long="--runtime-tree", + metavar="SOURCE:[TARGET]", + section="Host", + parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser()), + help="Additional mounts to add when booting the image", + ), ) MATCHES = ( @@ -2460,6 +2469,7 @@ Clean Package Manager Metadata: {yes_no_auto(config.clean_package_metadata)} Tools Tree: {config.tools_tree} Tools Tree Distribution: {none_to_none(config.tools_tree_distribution)} Tools Tree Release: {none_to_none(config.tools_tree_release)} + Runtime Trees: {line_join_source_target_list(config.runtime_trees)} QEMU GUI: {yes_no(config.qemu_gui)} QEMU CPU Cores: {config.qemu_smp} diff --git a/mkosi/qemu.py b/mkosi/qemu.py index 36e11036c..163cdb20e 100644 --- a/mkosi/qemu.py +++ b/mkosi/qemu.py @@ -28,7 +28,12 @@ from mkosi.partition import finalize_root, find_partitions from mkosi.run import MkosiAsyncioThread, run, spawn from mkosi.tree import copy_tree, rmtree from mkosi.types import PathString -from mkosi.util import format_bytes, qemu_check_kvm_support, qemu_check_vsock_support +from mkosi.util import ( + InvokingUser, + format_bytes, + qemu_check_kvm_support, + qemu_check_vsock_support, +) def machine_cid(config: MkosiConfig) -> int: @@ -155,6 +160,45 @@ def start_swtpm() -> Iterator[Path]: proc.wait() +@contextlib.contextmanager +def start_virtiofsd(directory: Path) -> Iterator[Path]: + uid, gid = InvokingUser.uid_gid() + + with tempfile.TemporaryDirectory() as state: + # Make sure virtiofsd is allowed to create its socket in this temporary directory. + os.chown(state, uid, gid) + + sock = Path(state) / Path("sock") + + virtiofsd = shutil.which("virtiofsd") + if virtiofsd is None: + if Path("/usr/libexec/virtiofsd").exists(): + virtiofsd = "/usr/libexec/virtiofsd" + elif Path("/usr/lib/virtiofsd").exists(): + virtiofsd = "/usr/lib/virtiofsd" + else: + die("virtiofsd must be installed to use RuntimeMounts= with mkosi qemu") + + # virtiofsd has to run unprivileged to use the --uid-map and --gid-map options, so we always run it as the user + # running mkosi. + proc = spawn([ + virtiofsd, + "--socket-path", sock, + "--shared-dir", directory, + "--xattr", + "--posix-acl", + # Map the user running mkosi to root in the virtual machine for the virtiofs instance to make sure all + # files created by root in the VM are owned by the user running mkosi on the host. + "--uid-map", f":0:{uid}:1:", + "--gid-map", f":0:{gid}:1:", + ], user=uid, group=gid) + + try: + yield sock + finally: + proc.wait() + + @contextlib.contextmanager def vsock_notify_handler() -> Iterator[tuple[str, dict[str, str]]]: """ @@ -226,6 +270,9 @@ def run_qemu(args: MkosiArgs, config: MkosiConfig) -> None: ): die(f"{config.output_format} images cannot be booted with the '{config.qemu_firmware}' firmware") + if (config.runtime_trees and config.qemu_firmware == QemuFirmware.bios): + die("RuntimeTrees= cannot be used when booting in BIOS firmware") + accel = "tcg" auto = ( config.qemu_kvm == ConfigFeature.auto and @@ -245,11 +292,18 @@ def run_qemu(args: MkosiArgs, config: MkosiConfig) -> None: ovmf, ovmf_supports_sb = find_ovmf_firmware(config) if firmware == QemuFirmware.uefi else (None, False) + shm = [] + if config.runtime_trees: + shm = ["-object", "memory-backend-memfd,id=mem,size=2G,share=on"] + if config.architecture == Architecture.arm64: machine = f"type=virt,accel={accel}" else: machine = f"type=q35,accel={accel},smm={'on' if ovmf_supports_sb else 'off'}" + if shm: + machine += ",memory-backend=mem" + cmdline: list[PathString] = [ find_qemu_binary(config), "-machine", machine, @@ -258,6 +312,7 @@ def run_qemu(args: MkosiArgs, config: MkosiConfig) -> None: "-object", "rng-random,filename=/dev/urandom,id=rng0", "-device", "virtio-rng-pci,rng=rng0,id=rng-device0", "-nic", "user,model=virtio-net-pci", + *shm, ] use_vsock = (config.qemu_vsock == ConfigFeature.enabled or @@ -280,23 +335,39 @@ def run_qemu(args: MkosiArgs, config: MkosiConfig) -> None: "-mon", "console", ] - if config.architecture.supports_smbios(): - for k, v in config.credentials.items(): - cmdline += [ - "-smbios", f"type=11,value=io.systemd.credential.binary:{k}={base64.b64encode(v.encode()).decode()}" - ] - cmdline += [ - "-smbios", - f"type=11,value=io.systemd.stub.kernel-cmdline-extra={' '.join(config.kernel_command_line_extra)}" - ] - # QEMU has built-in logic to look for the BIOS firmware so we don't need to do anything special for that. if firmware == QemuFirmware.uefi: cmdline += ["-drive", f"if=pflash,format=raw,readonly=on,file={ovmf}"] + if firmware == QemuFirmware.linux or config.output_format in (OutputFormat.cpio, OutputFormat.uki): + kcl = config.kernel_command_line + config.kernel_command_line_extra + elif config.architecture.supports_smbios(): + kcl = config.kernel_command_line_extra + else: + kcl = [] + notifications: dict[str, str] = {} with contextlib.ExitStack() as stack: + for src, target in config.runtime_trees: + sock = stack.enter_context(start_virtiofsd(src)) + cmdline += [ + "-chardev", f"socket,id={sock.name},path={sock}", + "-device", f"vhost-user-fs-pci,queue-size=1024,chardev={sock.name},tag={sock.name}", + ] + kcl += [f"systemd.mount-extra={sock.name}:{target or f'/root/src/{src.name}'}:virtiofs"] + + if config.architecture.supports_smbios(): + for k, v in config.credentials.items(): + cmdline += [ + "-smbios", f"type=11,value=io.systemd.credential.binary:{k}={base64.b64encode(v.encode()).decode()}" + ] + + cmdline += [ + "-smbios", + f"type=11,value=io.systemd.stub.kernel-cmdline-extra={' '.join(kcl)}" + ] + if firmware == QemuFirmware.uefi and ovmf_supports_sb: ovmf_vars = stack.enter_context(tempfile.NamedTemporaryFile(prefix=".mkosi-")) shutil.copy2(find_ovmf_vars(config), Path(ovmf_vars.name)) @@ -362,7 +433,7 @@ def run_qemu(args: MkosiArgs, config: MkosiConfig) -> None: else: root = "" - cmdline += ["-append", " ".join([root] + config.kernel_command_line + config.kernel_command_line_extra)] + cmdline += ["-append", " ".join([root] + kcl)] if config.output_format == OutputFormat.cpio: cmdline += ["-initrd", fname] diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index b3a771b6c..091c5f6cb 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -1157,6 +1157,21 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, of package specifications. This option may be used multiple times in which case the specified package lists are combined. +`RuntimeTrees=`, `--runtime-tree=` + +: Takes a colon separated pair of paths. The first path refers to a + directory to mount into any machine (container or VM) started by + mkosi. The second path refers to the target directory inside the + machine. If the second path is not provided, the directory is mounted + below `/root/src` in the machine. + +: For each mounted directory, the uid and gid of the user running mkosi + are mapped to the root user in the machine. This means that all the + files and directories will appear as if they're owned by root in the + machine, and all new files and directories created by root in the + machine in these directories will be owned by the user running mkosi + on the host. + ## Supported distributions Images may be created containing installations of the following -- 2.47.2