From: Daan De Meyer Date: Sun, 12 May 2024 14:01:06 +0000 (+0200) Subject: Add optional file ID for qemu drives X-Git-Tag: v23.1~57 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=55f17a8ae67ece9712324c3107804daad94ad0d9;p=thirdparty%2Fmkosi.git Add optional file ID for qemu drives For testing multipath in systemd's integration tests, we need multiple qemu drives backed by the same file. Let's allow specifying an additional file ID to make this possible with QemuDrive=. --- diff --git a/mkosi/config.py b/mkosi/config.py index 759a54dc4..100d006ca 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -138,6 +138,7 @@ class QemuDrive: size: int directory: Optional[Path] options: Optional[str] + file_id: str # We use negative numbers for specifying special constants @@ -1051,7 +1052,7 @@ def parse_drive(value: str) -> QemuDrive: if len(parts) < 2: die(f"Missing size in drive '{value}") - if len(parts) > 4: + if len(parts) > 5: die(f"Too many components in drive '{value}") id = parts[0] @@ -1062,8 +1063,9 @@ def parse_drive(value: str) -> QemuDrive: 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 + file_id = parts[4] if len(parts) > 4 and parts[4] else id - return QemuDrive(id=id, size=size, directory=directory, options=options) + return QemuDrive(id=id, size=size, directory=directory, options=options, file_id=file_id) def config_parse_sector_size(value: Optional[str], old: Optional[int]) -> Optional[int]: @@ -4154,19 +4156,20 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[ 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"], + size=d["Size"] if isinstance(d["Size"], int) else parse_bytes(d["Size"]), + directory=Path(d["Directory"]) if d.get("Directory") else None, + options=d.get("Options"), + file_id=d.get("FileId", d["Id"]), ) ) + return ret def generic_version_transformer( diff --git a/mkosi/qemu.py b/mkosi/qemu.py index 13b725d4b..85c429485 100644 --- a/mkosi/qemu.py +++ b/mkosi/qemu.py @@ -46,7 +46,7 @@ from mkosi.sandbox import Mount from mkosi.tree import copy_tree, rmtree from mkosi.types import PathString from mkosi.user import INVOKING_USER, become_root, become_root_cmd -from mkosi.util import StrEnum, flock, flock_or_die, try_or +from mkosi.util import StrEnum, flock, flock_or_die, groupby, try_or from mkosi.versioncomp import GenericVersion QEMU_KVM_DEVICE_VERSION = GenericVersion("9.0") @@ -1123,14 +1123,15 @@ def run_qemu(args: Args, config: Config) -> None: f"type=11,value=io.systemd.boot.kernel-cmdline-extra={' '.join(kcl).replace(',', ',,')}", ] - for drive in config.qemu_drives: - file = stack.enter_context(finalize_drive(drive)) + for _, drives in groupby(config.qemu_drives, key=lambda d: d.file_id): + file = stack.enter_context(finalize_drive(drives[0])) - arg = f"if=none,id={drive.id},file={file},format=raw" - if drive.options: - arg += f",{drive.options}" + for drive in drives: + arg = f"if=none,id={drive.id},file={file},format=raw,file.locking=off" + if drive.options: + arg += f",{drive.options}" - cmdline += ["-drive", arg] + cmdline += ["-drive", arg] cmdline += config.qemu_args cmdline += args.cmdline diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 965bd6044..aa56c198e 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -1477,15 +1477,18 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, `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 verbatim to qemu's - `-drive` option. + `:[:[:[:]]]`. `id` specifies + the qemu ID assigned 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 verbatim + to qemu's `-drive` option. `file-id` specifies the ID of the file + backing the drive. Drives with the same file ID will share the + backing file. The directory and size of the file will be determined + from the first drive with a given file ID. : Example usage: diff --git a/mkosi/util.py b/mkosi/util.py index 6bd3ac3ef..34c2c434b 100644 --- a/mkosi/util.py +++ b/mkosi/util.py @@ -17,7 +17,7 @@ import re import resource import stat import tempfile -from collections.abc import Iterable, Iterator, Mapping, Sequence +from collections.abc import Hashable, Iterable, Iterator, Mapping, Sequence from pathlib import Path from types import ModuleType from typing import Any, Callable, Optional, TypeVar, no_type_check @@ -27,6 +27,7 @@ from mkosi.types import PathString T = TypeVar("T") V = TypeVar("V") +S = TypeVar("S", bound=Hashable) def dictify(f: Callable[..., Iterator[tuple[T, V]]]) -> Callable[..., dict[T, V]]: @@ -319,3 +320,17 @@ def try_or(fn: Callable[..., T], exception: type[Exception], default: T) -> T: return fn() except exception: return default + + +def groupby(seq: Sequence[T], key: Callable[[T], S]) -> list[tuple[S, list[T]]]: + grouped: dict[S, list[T]] = {} + + for i in seq: + k = key(i) + + if k not in grouped: + grouped[k] = [] + + grouped[k].append(i) + + return [(key, group) for key, group in grouped.items()] diff --git a/tests/test_json.py b/tests/test_json.py index 3008bc771..88ce42b33 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -232,12 +232,14 @@ def test_config() -> None: "QemuDrives": [ { "Directory": "/foo/bar", + "FileId": "red", "Id": "abc", "Options": "abc,qed", "Size": 200 }, { "Directory": null, + "FileId": "wcd", "Id": "abc", "Options": "", "Size": 200 @@ -437,7 +439,10 @@ def test_config() -> None: proxy_url="https://my/proxy", qemu_args=[], qemu_cdrom=False, - qemu_drives=[QemuDrive("abc", 200, Path("/foo/bar"), "abc,qed"), QemuDrive("abc", 200, None, "")], + qemu_drives=[ + QemuDrive("abc", 200, Path("/foo/bar"), "abc,qed", "red"), + QemuDrive("abc", 200, None, "", "wcd"), + ], qemu_firmware=QemuFirmware.linux, qemu_firmware_variables=Path("/foo/bar"), qemu_gui=True,