]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add QemuDrives= option
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 7 Nov 2023 21:10:47 +0000 (22:10 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 8 Nov 2023 11:04:38 +0000 (12:04 +0100)
This option allows specifying extra qemu drives to pass to qemu.
mkosi will create a file (optionally in the given directory) of
the given size and pass it to qemu via -drive, optionally with some
extra options.

Doing this in mkosi allows mkosi to automatically manage the lifetime
of the file backing the drive. We can create it as needed and remove it
when we exit.

Example usage:

```conf
[Host]
QemuDrives=btrfs:10G
           ext4:20G
QemuArgs=-device nvme,serial=btrfs,drive=btrfs
         -device nvme,serial=ext4,drive=ext4
```

NEWS.md
mkosi/config.py
mkosi/qemu.py
mkosi/resources/mkosi.md
tests/test_json.py

diff --git a/NEWS.md b/NEWS.md
index b30e43e9b617be118e7ef5bb59c16f69902d681c..7b7c49fdc17cbe41cc019ed158fd179e5e36344b 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
@@ -63,6 +63,8 @@
 - mkosi now fails if configuration specified via the CLI does not apply
   to any image (because it is overridden).
 - `PackageManagerTrees=` was moved to the `Distribution` section.
+- Added `QemuDrives=` to have mkosi create extra qemu drives and pass
+  them to qemu when using the `qemu` verb.
 
 ## v18
 
index ec16822243652b32f59e3d3213087b2af46e56f9..168b5aa59f62e9ba7e7bff3f1b928307f0045cf9 100644 (file)
@@ -84,6 +84,14 @@ class ConfigTree:
         return (self.source, prefix / os.fspath(self.target).lstrip("/") if self.target else prefix)
 
 
+@dataclasses.dataclass(frozen=True)
+class QemuDrive:
+    id: str
+    size: int
+    directory: Optional[Path]
+    options: Optional[str]
+
+
 class SecureBootSignTool(StrEnum):
     auto   = enum.auto()
     sbsign = enum.auto()
@@ -489,10 +497,7 @@ def match_systemd_version(value: str) -> bool:
     return config_match_version(value, version)
 
 
-def config_parse_bytes(value: Optional[str], old: Optional[int] = None) -> Optional[int]:
-    if not value:
-        return None
-
+def parse_bytes(value: str) -> int:
     if value.endswith("G"):
         factor = 1024**3
     elif value.endswith("M"):
@@ -516,6 +521,13 @@ def config_parse_bytes(value: Optional[str], old: Optional[int] = None) -> Optio
     return result
 
 
+def config_parse_bytes(value: Optional[str], old: Optional[int] = None) -> Optional[int]:
+    if not value:
+        return None
+
+    return parse_bytes(value)
+
+
 def config_parse_profile(value: Optional[str], old: Optional[int] = None) -> Optional[str]:
     if not value:
         return None
@@ -527,6 +539,29 @@ def config_parse_profile(value: Optional[str], old: Optional[int] = None) -> Opt
     return value
 
 
+def parse_drive(value: str) -> QemuDrive:
+    parts = value.split(":", maxsplit=3)
+    if not parts or not parts[0]:
+        die(f"No ID specified for drive '{value}'")
+
+    if len(parts) < 2:
+        die(f"Missing size in drive '{value}")
+
+    if len(parts) > 4:
+        die(f"Too many components in drive '{value}")
+
+    id = parts[0]
+    if not is_valid_filename(id):
+        die(f"Unsupported path character in drive id '{id}'")
+
+    size = parse_bytes(parts[1])
+
+    directory = parse_path(parts[2]) if len(parts) > 2 and parts[2] else None
+    options = parts[3] if len(parts) > 3 and parts[3] else None
+
+    return QemuDrive(id=id, size=size, directory=directory, options=options)
+
+
 @dataclasses.dataclass(frozen=True)
 class MkosiConfigSetting:
     dest: str
@@ -843,6 +878,7 @@ class MkosiConfig:
     qemu_cdrom: bool
     qemu_firmware: QemuFirmware
     qemu_kernel: Optional[Path]
+    qemu_drives: list[QemuDrive]
     qemu_args: list[str]
 
     image: Optional[str]
@@ -1850,6 +1886,14 @@ SETTINGS = (
         parse=config_make_path_parser(),
         help="Specify the kernel to use for qemu direct kernel boot",
     ),
+    MkosiConfigSetting(
+        dest="qemu_drives",
+        long="--qemu-drive",
+        metavar="DRIVE",
+        section="Host",
+        parse=config_make_list_parser(delimiter=" ", parse=parse_drive),
+        help="Specify a qemu drive that mkosi should create and pass to qemu",
+    ),
     MkosiConfigSetting(
         dest="qemu_args",
         metavar="ARGS",
@@ -2964,6 +3008,24 @@ def json_type_transformer(refcls: Union[type[MkosiArgs], type[MkosiConfig]]) ->
     def str_tuple_transformer(strtup: list[str], fieldtype: list[tuple[str, ...]]) -> tuple[str, ...]:
         return tuple(strtup)
 
+    def config_drive_transformer(drives: list[dict[str, Any]], fieldtype: type[QemuDrive]) -> list[QemuDrive]:
+        # TODO: exchange for TypeGuard and list comprehension once on 3.10
+        ret = []
+        for d in drives:
+            assert "id" in d
+            assert "size" in d
+            assert "directory" in d
+            assert "options" in d
+            ret.append(
+                QemuDrive(
+                    id=d["id"],
+                    size=int(d["size"]),
+                    directory=Path(d["directory"]) if d["directory"] else None,
+                    options=d["options"],
+                )
+            )
+        return ret
+
     transformers = {
         Path: path_transformer,
         Optional[Path]: optional_path_transformer,
@@ -2985,6 +3047,7 @@ def json_type_transformer(refcls: Union[type[MkosiArgs], type[MkosiConfig]]) ->
         list[ManifestFormat]: enum_list_transformer,
         Verb: enum_transformer,
         DocFormat: enum_transformer,
+        list[QemuDrive]: config_drive_transformer,
     }
 
     def json_transformer(key: str, val: Any) -> Any:
index 93d93fd2e7d04d93271e59199632a6f9c18c5fec..12d49a57a2c3bd7c047e64bbc84d331ef72596f9 100644 (file)
@@ -623,6 +623,19 @@ def run_qemu(args: MkosiArgs, config: MkosiConfig, qemu_device_fds: Mapping[Qemu
             addr, notifications = stack.enter_context(vsock_notify_handler())
             cmdline += ["-smbios", f"type=11,value=io.systemd.credential:vmm.notify_socket={addr}"]
 
+        for drive in config.qemu_drives:
+            file = stack.enter_context(
+                tempfile.NamedTemporaryFile(dir=drive.directory or "/var/tmp", prefix=f"mkosi-drive-{drive.id}")
+            )
+            file.truncate(drive.size)
+            os.chown(file.name, INVOKING_USER.uid, INVOKING_USER.gid)
+
+            arg = f"if=none,id={drive.id},file={file.name},format=raw"
+            if drive.options:
+                arg += f",{drive.options}"
+
+            cmdline += ["-drive", arg]
+
         cmdline += config.qemu_args
         cmdline += args.cmdline
 
index 170e782365246b04d21bba2ade6837064ec7002f..3cb16f2e1b8cb2fc8e26d669eecb4ccb808829f1 100644 (file)
@@ -1247,6 +1247,19 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
   configured firmware, qemu might boot the kernel itself or using the
   configured firmware.
 
+`QemuDrives=`, `--qemu-drive=`
+
+: Add a qemu drive. Takes a colon-delimited string of format
+  `<id>:<size>[:<directory>[:<options>]]`. `id` specifies the qemu id we
+  assign to the drive. This can be used as the `drive=` property in
+  various qemu devices. `size` specifies the size of the drive. This
+  takes a size in bytes. Additionally, the suffixes `K`, `M` and `G` can
+  be used to specify a size in kilobytes, megabytes and gigabytes
+  respectively. `directory` optionally specifies the directory in which
+  to create the file backing the drive. `options` optionally specifies
+  extra comma-delimited properties which are passed verbatime to qemu's
+  `-drive` option.
+
 `QemuArgs=`
 
 : Space-delimited list of additional arguments to pass when invoking
index 373aedee8e584d69b7f4b6218daf4dcd2797d609..11d1428ee5d4c7b643a1757dd9dfbb44685175d3 100644 (file)
@@ -13,6 +13,7 @@ from mkosi.config import (
     BiosBootloader,
     Bootloader,
     Compression,
+    QemuDrive,
     ConfigFeature,
     ConfigTree,
     DocFormat,
@@ -186,6 +187,20 @@ def test_config() -> None:
             "Profile": "profile",
             "QemuArgs": [],
             "QemuCdrom": false,
+            "QemuDrives": [
+                {
+                    "directory": "/foo/bar",
+                    "id": "abc",
+                    "options": "abc,qed",
+                    "size": 200
+                },
+                {
+                    "directory": null,
+                    "id": "abc",
+                    "options": "",
+                    "size": 200
+                }
+            ],
             "QemuFirmware": "linux",
             "QemuGui": true,
             "QemuKernel": null,
@@ -320,6 +335,7 @@ def test_config() -> None:
         profile = "profile",
         qemu_args = [],
         qemu_cdrom = False,
+        qemu_drives = [QemuDrive("abc", 200, Path("/foo/bar"), "abc,qed"), QemuDrive("abc", 200, None, "")],
         qemu_firmware = QemuFirmware.linux,
         qemu_gui = True,
         qemu_kernel = None,