From: Daan De Meyer Date: Tue, 7 Nov 2023 21:10:47 +0000 (+0100) Subject: Add QemuDrives= option X-Git-Tag: v19~17 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3834fe19b3a1b4aa99320aff756840e8a8db6ff0;p=thirdparty%2Fmkosi.git Add QemuDrives= option 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 ``` --- diff --git a/NEWS.md b/NEWS.md index b30e43e9b..7b7c49fdc 100644 --- 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 diff --git a/mkosi/config.py b/mkosi/config.py index ec1682224..168b5aa59 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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: diff --git a/mkosi/qemu.py b/mkosi/qemu.py index 93d93fd2e..12d49a57a 100644 --- a/mkosi/qemu.py +++ b/mkosi/qemu.py @@ -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 diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 170e78236..3cb16f2e1 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -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` 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 diff --git a/tests/test_json.py b/tests/test_json.py index 373aedee8..11d1428ee 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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,