]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add support for runtime trees 1910/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 20 Sep 2023 09:39:09 +0000 (11:39 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 20 Sep 2023 15:05:44 +0000 (17:05 +0200)
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
mkosi/config.py
mkosi/qemu.py
mkosi/resources/mkosi.md

index af88f10b740fd774c8d41535fc507ac786e76f9f..405283e4817d17df2bd7a5c536559f9b2fe3b444 100644 (file)
@@ -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
index 64591d19ab4bf6a39dca2ca39b03c89538a06a06..f2341464c02a005d882b7d0aad6773af8298a67f 100644 (file)
@@ -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}
index 36e11036c721d1f45d3a1e1c81cd55779c30863b..163cdb20e07ddcc20a1bd36943728675e3fd4837 100644 (file)
@@ -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]
index b3a771b6cd2c5b1d1eabf637a6fc5679dad16b08..091c5f6cbfa5f798e8ae0fc8ada007f2089cb993 100644 (file)
@@ -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