From 3a9aae8a4c439f6ad00cf1497dc95f08718bd2e6 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Wed, 6 Dec 2023 15:04:54 +0100 Subject: [PATCH] Add sysext, confext and portable support Wwe also write the extension-release file in case of sysexts and confexts and make sure we skip a bunch of our automatic features when building extension images or enabling the Overlay= option as in these cases many of our automatic features are undesireable. --- docs/sysext.md | 53 +---- mkosi/__init__.py | 181 ++++++++++++++---- mkosi/config.py | 36 ++-- mkosi/resources/mkosi.md | 5 +- mkosi/resources/repart/__init__.py | 0 .../resources/repart/definitions/__init__.py | 0 .../definitions/confext.repart.d/10-root.conf | 8 + .../confext.repart.d/20-root-verity.conf | 6 + .../confext.repart.d/30-root-verity-sig.conf | 5 + .../portable.repart.d/10-root.conf | 8 + .../portable.repart.d/20-root-verity.conf | 6 + .../portable.repart.d/30-root-verity-sig.conf | 5 + .../definitions/sysext.repart.d/10-root.conf | 8 + .../sysext.repart.d/20-root-verity.conf | 6 + .../sysext.repart.d/30-root-verity-sig.conf | 5 + mkosi/run.py | 12 +- mkosi/util.py | 21 +- tests/__init__.py | 3 + tests/test_boot.py | 4 +- 19 files changed, 259 insertions(+), 113 deletions(-) create mode 100644 mkosi/resources/repart/__init__.py create mode 100644 mkosi/resources/repart/definitions/__init__.py create mode 100644 mkosi/resources/repart/definitions/confext.repart.d/10-root.conf create mode 100644 mkosi/resources/repart/definitions/confext.repart.d/20-root-verity.conf create mode 100644 mkosi/resources/repart/definitions/confext.repart.d/30-root-verity-sig.conf create mode 100644 mkosi/resources/repart/definitions/portable.repart.d/10-root.conf create mode 100644 mkosi/resources/repart/definitions/portable.repart.d/20-root-verity.conf create mode 100644 mkosi/resources/repart/definitions/portable.repart.d/30-root-verity-sig.conf create mode 100644 mkosi/resources/repart/definitions/sysext.repart.d/10-root.conf create mode 100644 mkosi/resources/repart/definitions/sysext.repart.d/20-root-verity.conf create mode 100644 mkosi/resources/repart/definitions/sysext.repart.d/30-root-verity-sig.conf diff --git a/docs/sysext.md b/docs/sysext.md index 045e10c48..52c6dff1a 100644 --- a/docs/sysext.md +++ b/docs/sysext.md @@ -47,7 +47,7 @@ top of it by writing the following to `mkosi.images/btrfs/mkosi.conf`: Dependencies=base [Output] -Format=disk +Format=sysext Overlay=yes [Content] @@ -58,61 +58,14 @@ Packages=btrfs-progs `BaseTrees=` point to our base image and `Overlay=yes` instructs mkosi to only package the files added on top of the base tree. -We'll also want to mark our extension as a system extension. We'll -assume that our extension is intended for an initramfs, so we'll need to -configure it as such with `SYSEXT_SCOPE=`. To do that, write the -following to -`mkosi.images/btrfs/mkosi.extra/usr/lib/extension-release.d/extension-release.btrfs`: - -```conf -ID= -VERSION_ID= -ARCHITECTURE= -SYSEXT_SCOPE=initrd -``` - -We'll want to package this up as a signed extension, so let's define the -necessary systemd-repart files to make that possible: - -`mkosi.images/btrfs/mkosi.repart/10-root.conf`: - -```conf -[Partition] -Type=root -Format=squashfs -CopyFiles=/usr/ -Verity=data -VerityMatchKey=root -Minimize=best -``` - -`mkosi.images/btrfs/mkosi.repart/20-root-verity.conf`: - -```conf -[Partition] -Type=root-verity -Verity=hash -VerityMatchKey=root -Minimize=best -``` - -`mkosi.images/btrfs/mkosi.repart/30-root-verity-sig.conf`: - -```conf -[Partition] -Type=root-verity-sig -Verity=signature -VerityMatchKey=root -``` - -Of course we can't sign anything without a key, so let's generate one +We can't sign the extension image without a key, so let's generate one with `mkosi genkey` (or write your own private key and certificate yourself to `mkosi.key` and `mkosi.crt` respectively). Note that this key will need to be loaded into your kernel keyring either at build time or via MOK for systemd to accept the system extension at runtime as trusted. -Finally, you build the base image and the extensions by running +Finally, you can build the base image and the extensions by running `mkosi -f`. You'll find `btrfs.raw` in `mkosi.output` which is the extension image. diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 1ae70f872..cb5a11a6a 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -69,14 +69,14 @@ from mkosi.util import ( format_rlimit, make_executable, one_zero, + read_env_file, + read_os_release, scopedenv, try_import, umask, ) from mkosi.versioncomp import GenericVersion -MINIMUM_SYSTEMD_VERSION = GenericVersion("254") - MKOSI_AS_CALLER = ( "setpriv", f"--reuid={INVOKING_USER.uid}", @@ -137,23 +137,24 @@ def install_distribution(state: MkosiState) -> None: with complete_step(f"Installing {str(state.config.distribution).capitalize()}"): state.config.distribution.install(state) - if not (state.root / "etc/machine-id").exists(): - # Uninitialized means we want it to get initialized on first boot. - with umask(~0o444): - (state.root / "etc/machine-id").write_text("uninitialized\n") - - # Ensure /efi exists so that the ESP is mounted there, as recommended by - # https://0pointer.net/blog/linux-boot-partitions.html. Use the most restrictive access mode we - # can without tripping up mkfs tools since this directory is only meant to be overmounted and - # should not be read from or written to. - with umask(~0o500): - (state.root / "efi").mkdir(exist_ok=True) - - # Some distributions install EFI binaries directly to /boot/efi. Let's redirect them to /efi - # instead. - rmtree(state.root / "boot/efi") - (state.root / "boot").mkdir(exist_ok=True) - (state.root / "boot/efi").symlink_to("../efi") + if not state.config.overlay: + if not (state.root / "etc/machine-id").exists(): + # Uninitialized means we want it to get initialized on first boot. + with umask(~0o444): + (state.root / "etc/machine-id").write_text("uninitialized\n") + + # Ensure /efi exists so that the ESP is mounted there, as recommended by + # https://0pointer.net/blog/linux-boot-partitions.html. Use the most restrictive access mode we + # can without tripping up mkfs tools since this directory is only meant to be overmounted and + # should not be read from or written to. + with umask(~0o500): + (state.root / "efi").mkdir(exist_ok=True) + + # Some distributions install EFI binaries directly to /boot/efi. Let's redirect them to /efi + # instead. + rmtree(state.root / "boot/efi") + (state.root / "boot").mkdir(exist_ok=True) + (state.root / "boot/efi").symlink_to("../efi") if state.config.packages: state.config.distribution.install_packages(state, state.config.packages) @@ -209,6 +210,9 @@ def configure_os_release(state: MkosiState) -> None: if not state.config.image_id and not state.config.image_version: return + if state.config.overlay or state.config.output_format in (OutputFormat.sysext, OutputFormat.confext): + return + for candidate in ["usr/lib/os-release", "etc/os-release", "usr/lib/initrd-release", "etc/initrd-release"]: osrelease = state.root / candidate # at this point we know we will either change or add to the file @@ -239,6 +243,44 @@ def configure_os_release(state: MkosiState) -> None: newosrelease.rename(osrelease) +def configure_extension_release(state: MkosiState) -> None: + if state.config.output_format not in (OutputFormat.sysext, OutputFormat.confext): + return + + prefix = "SYSEXT" if state.config.output_format == OutputFormat.sysext else "CONFEXT" + d = "usr/lib" if state.config.output_format == OutputFormat.sysext else "etc" + p = state.root / d / f"extension-release.d/extension-release.{state.config.output}" + p.parent.mkdir(parents=True, exist_ok=True) + + osrelease = read_os_release(state.root) + extrelease = read_env_file(p) if p.exists() else {} + new = p.with_suffix(".new") + + with new.open() as f: + for k, v in extrelease.items(): + f.write(f"{k}={v}\n") + + if "ID" not in extrelease: + f.write(f"ID={osrelease.get('ID', '_any')}\n") + + if "VERSION_ID" not in extrelease and (version := osrelease.get("VERSION_ID")): + f.write(f"VERSION_ID={version}\n") + + if f"{prefix}_ID" not in extrelease and state.config.image_id: + f.write(f"{prefix}_ID={state.config.image_id}\n") + + if f"{prefix}_VERSION_ID" not in extrelease and state.config.image_version: + f.write(f"{prefix}_VERSION_ID={state.config.image_version}\n") + + if f"{prefix}_SCOPE" not in extrelease: + f.write(f"{prefix}_SCOPE=initrd system portable\n") + + if "ARCHITECTURE" not in extrelease: + f.write(f"ARCHITECTURE={state.config.architecture}\n") + + new.rename(p) + + def configure_autologin_service(state: MkosiState, service: str, extra: str) -> None: dropin = state.root / f"usr/lib/systemd/system/{service}.d/autologin.conf" with umask(~0o755): @@ -374,7 +416,7 @@ def finalize_host_scripts( def finalize_chroot_scripts(state: MkosiState) -> contextlib.AbstractContextManager[Path]: - git = {"git": ("git", "-c", "safe.directory=*")} if find_binary("git", state.root) else {} + git = {"git": ("git", "-c", "safe.directory=*")} if find_binary("git", root=state.root) else {} return finalize_scripts(git) @@ -685,7 +727,14 @@ def install_systemd_boot(state: MkosiState) -> None: if state.config.bootloader != Bootloader.systemd_boot: return - if state.config.output_format == OutputFormat.cpio and state.config.bootable == ConfigFeature.auto: + if ( + ( + state.config.output_format == OutputFormat.cpio or + state.config.output_format.is_extension_image() or + state.config.overlay + ) + and state.config.bootable == ConfigFeature.auto + ): return if state.config.architecture.to_efi() is None and state.config.bootable == ConfigFeature.auto: @@ -782,7 +831,7 @@ def find_grub_bios_directory(state: MkosiState) -> Optional[Path]: def find_grub_binary(state: MkosiState, binary: str) -> Optional[Path]: assert "grub" in binary and "grub2" not in binary - return find_binary(binary, state.root) or find_binary(binary.replace("grub", "grub2"), state.root) + return find_binary(binary, root=state.root) or find_binary(binary.replace("grub", "grub2"), root=state.root) def find_grub_prefix(state: MkosiState) -> Optional[str]: @@ -800,6 +849,9 @@ def want_grub_efi(state: MkosiState) -> bool: if state.config.bootloader != Bootloader.grub: return False + if state.config.overlay or state.config.output_format.is_extension_image(): + return False + if not any((state.root / "efi").rglob("grub*.efi")): if state.config.bootable == ConfigFeature.enabled: die("A bootable EFI image with grub was requested but grub for EFI is not installed in /efi") @@ -819,6 +871,9 @@ def want_grub_bios(state: MkosiState, partitions: Sequence[Partition] = ()) -> b if state.config.bios_bootloader != BiosBootloader.grub: return False + if state.config.overlay: + return False + have = find_grub_bios_directory(state) is not None if not have and state.config.bootable == ConfigFeature.enabled: die("A BIOS bootable image with grub was requested but grub for BIOS is not installed") @@ -1359,7 +1414,10 @@ def want_uki(config: MkosiConfig) -> bool: if config.bootloader == Bootloader.none: return False - if config.output_format == OutputFormat.cpio and config.bootable == ConfigFeature.auto: + if ( + (config.output_format == OutputFormat.cpio or config.output_format.is_extension_image() or config.overlay) + and config.bootable == ConfigFeature.auto + ): return False if config.architecture.to_efi() is None and config.bootable == ConfigFeature.auto: @@ -1691,16 +1749,18 @@ def check_outputs(config: MkosiConfig) -> None: die(f"Output path {f} exists already. (Consider invocation with --force.)") -def check_systemd_tool(*tools: PathString, reason: str, hint: Optional[str] = None) -> None: - for tool in tools: - if shutil.which(tool): - break - else: +def systemd_tool_version(tool: PathString) -> GenericVersion: + return GenericVersion(run([tool, "--version"], stdout=subprocess.PIPE).stdout.split()[1]) + + +def check_systemd_tool(*tools: PathString, version: str, reason: str, hint: Optional[str] = None) -> None: + tool = find_binary(*tools) + if not tool: die(f"Could not find '{tools[0]}' which is required to {reason}.", hint=hint) - v = GenericVersion(run([tool, "--version"], stdout=subprocess.PIPE).stdout.split()[1]) - if v < MINIMUM_SYSTEMD_VERSION: - die(f"Found '{tool}' version {v} but version {MINIMUM_SYSTEMD_VERSION} or newer is required to {reason}.", + v = systemd_tool_version(tool) + if v < version: + die(f"Found '{tool}' version {v} but version {version} or newer is required to {reason}.", hint=f"Use ToolsTree=default to get a newer version of '{tool}'.") @@ -1708,15 +1768,16 @@ def check_tools(args: MkosiArgs, config: MkosiConfig) -> None: if want_uki(config): check_systemd_tool( "ukify", "/usr/lib/systemd/ukify", + version="254", reason="build bootable images", hint="Bootable=no can be used to create a non-bootable image", ) if config.output_format in (OutputFormat.disk, OutputFormat.esp): - check_systemd_tool("systemd-repart", reason="build disk images") + check_systemd_tool("systemd-repart", version="254", reason="build disk images") if args.verb == Verb.boot: - check_systemd_tool("systemd-nspawn", reason="boot images") + check_systemd_tool("systemd-nspawn", version="254", reason="boot images") def configure_ssh(state: MkosiState) -> None: @@ -1775,6 +1836,9 @@ def configure_ssh(state: MkosiState) -> None: def configure_initrd(state: MkosiState) -> None: + if state.config.overlay or state.config.output_format.is_extension_image(): + return + if ( not (state.root / "init").exists() and not (state.root / "init").is_symlink() and @@ -1790,11 +1854,17 @@ def configure_initrd(state: MkosiState) -> None: def configure_clock(state: MkosiState) -> None: + if state.config.overlay or state.config.output_format.is_extension_image(): + return + with umask(~0o644): (state.root / "usr/lib/clock-epoch").touch() def run_depmod(state: MkosiState) -> None: + if state.config.overlay or state.config.output_format.is_extension_image(): + return + outputs = ( "modules.dep", "modules.dep.bin", @@ -1839,6 +1909,9 @@ def run_preset(state: MkosiState) -> None: def run_hwdb(state: MkosiState) -> None: + if state.config.overlay or state.config.output_format.is_extension_image(): + return + if not shutil.which("systemd-hwdb"): logging.info("systemd-hwdb is not installed, not generating hwdb") return @@ -1851,6 +1924,9 @@ def run_hwdb(state: MkosiState) -> None: def run_firstboot(state: MkosiState) -> None: + if state.config.overlay or state.config.output_format.is_extension_image(): + return + password, hashed = state.config.root_password or (None, False) pwopt = "--root-password-hashed" if hashed else "--root-password" pwcred = "passwd.hashed-password.root" if hashed else "passwd.plaintext-password.root" @@ -2167,6 +2243,43 @@ def make_esp(state: MkosiState, uki: Path) -> list[Partition]: return make_image(state, msg="Generating ESP image", definitions=[definitions]) +def make_extension_image(state: MkosiState, output: Path) -> None: + cmdline: list[PathString] = [ + "systemd-repart", + "--root", state.root, + "--dry-run=no", + "--no-pager", + "--offline=yes", + "--seed", str(state.config.seed) if state.config.seed else "random", + "--empty=create", + "--size=auto", + output, + ] + + if not state.config.architecture.is_native(): + cmdline += ["--architecture", str(state.config.architecture)] + if state.config.passphrase: + cmdline += ["--key-file", state.config.passphrase] + if state.config.verity_key: + cmdline += ["--private-key", state.config.verity_key] + if state.config.verity_certificate: + cmdline += ["--certificate", state.config.verity_certificate] + if state.config.sector_size: + cmdline += ["--sector-size", str(state.config.sector_size)] + + env = { + option: value + for option, value in state.config.environment.items() + if option.startswith("SYSTEMD_REPART_MKFS_OPTIONS_") or option == "SOURCE_DATE_EPOCH" + } + + with ( + importlib.resources.path("mkosi.resources.repart.definitions", f"{state.config.output_format}.repart.d") as d, + complete_step(f"Building {state.config.output_format} extension image") + ): + run(cmdline + ["--definitions", d], env=env) + + def finalize_staging(state: MkosiState) -> None: # Our output unlinking logic removes everything prefixed with the name of the image, so let's make # sure that everything we put into the output directory is prefixed with the name of the output. @@ -2302,6 +2415,8 @@ def build_image(args: MkosiArgs, config: MkosiConfig) -> None: elif state.config.output_format == OutputFormat.esp: make_uki(state, state.staging / state.config.output_split_uki) make_esp(state, state.staging / state.config.output_split_uki) + elif state.config.output_format.is_extension_image(): + make_extension_image(state, state.staging / state.config.output_with_format) elif state.config.output_format == OutputFormat.directory: state.root.rename(state.staging / state.config.output_with_format) diff --git a/mkosi/config.py b/mkosi/config.py index 7b6ffbf87..92670f803 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -135,27 +135,34 @@ class SecureBootSignTool(StrEnum): class OutputFormat(StrEnum): - directory = enum.auto() - tar = enum.auto() + confext = enum.auto() cpio = enum.auto() + directory = enum.auto() disk = enum.auto() - uki = enum.auto() esp = enum.auto() none = enum.auto() + portable = enum.auto() + sysext = enum.auto() + tar = enum.auto() + uki = enum.auto() def extension(self) -> str: return { - OutputFormat.disk: ".raw", - OutputFormat.esp: ".raw", - OutputFormat.cpio: ".cpio", - OutputFormat.tar: ".tar", - OutputFormat.uki: ".efi", + OutputFormat.confext: ".raw", + OutputFormat.cpio: ".cpio", + OutputFormat.disk: ".raw", + OutputFormat.esp: ".raw", + OutputFormat.portable: ".raw", + OutputFormat.sysext: ".raw", + OutputFormat.tar: ".tar", + OutputFormat.uki: ".efi", }.get(self, "") def use_outer_compression(self) -> bool: - return self in (OutputFormat.tar, - OutputFormat.cpio, - OutputFormat.disk) + return self in (OutputFormat.tar, OutputFormat.cpio, OutputFormat.disk) or self.is_extension_image() + + def is_extension_image(self) -> bool: + return self in (OutputFormat.sysext, OutputFormat.confext, OutputFormat.portable) class ManifestFormat(StrEnum): @@ -1087,6 +1094,7 @@ class MkosiConfig: "packages": self.packages, "build_packages": self.build_packages, "repositories": self.repositories, + "overlay": self.overlay, "prepare_scripts": [ base64.b64encode(script.read_bytes()).decode() for script in self.prepare_scripts @@ -3010,7 +3018,11 @@ def summary(config: MkosiConfig) -> str: SSH: {yes_no(config.ssh)} """ - if config.output_format in (OutputFormat.disk, OutputFormat.uki, OutputFormat.esp): + if config.output_format.is_extension_image() or config.output_format in ( + OutputFormat.disk, + OutputFormat.uki, + OutputFormat.esp, + ): summary += f"""\ {bold("VALIDATION")}: diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 873e9017e..770191bd2 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -585,8 +585,9 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, archive is generated), `disk` (a block device OS image with a GPT partition table), `uki` (a unified kernel image with the OS image in the `.initrd` PE section), `esp` (`uki` but wrapped in a disk image - with only an ESP partition) or `none` (the OS image is solely intended - as a build image to produce another artifact). + with only an ESP partition), `sysext`, `confext`, `portable` or `none` + (the OS image is solely intended as a build image to produce another + artifact). : If the `disk` output format is used, the disk image is generated using `systemd-repart`. The repart partition definition files to use can be diff --git a/mkosi/resources/repart/__init__.py b/mkosi/resources/repart/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mkosi/resources/repart/definitions/__init__.py b/mkosi/resources/repart/definitions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mkosi/resources/repart/definitions/confext.repart.d/10-root.conf b/mkosi/resources/repart/definitions/confext.repart.d/10-root.conf new file mode 100644 index 000000000..6b7b8f511 --- /dev/null +++ b/mkosi/resources/repart/definitions/confext.repart.d/10-root.conf @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root +Format=erofs +CopyFiles=/etc/ +Verity=data +VerityMatchKey=root +Minimize=best diff --git a/mkosi/resources/repart/definitions/confext.repart.d/20-root-verity.conf b/mkosi/resources/repart/definitions/confext.repart.d/20-root-verity.conf new file mode 100644 index 000000000..f8f5b8001 --- /dev/null +++ b/mkosi/resources/repart/definitions/confext.repart.d/20-root-verity.conf @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root-verity +Verity=hash +VerityMatchKey=root +Minimize=best diff --git a/mkosi/resources/repart/definitions/confext.repart.d/30-root-verity-sig.conf b/mkosi/resources/repart/definitions/confext.repart.d/30-root-verity-sig.conf new file mode 100644 index 000000000..19626791b --- /dev/null +++ b/mkosi/resources/repart/definitions/confext.repart.d/30-root-verity-sig.conf @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root-verity-sig +Verity=signature +VerityMatchKey=root diff --git a/mkosi/resources/repart/definitions/portable.repart.d/10-root.conf b/mkosi/resources/repart/definitions/portable.repart.d/10-root.conf new file mode 100644 index 000000000..21534702a --- /dev/null +++ b/mkosi/resources/repart/definitions/portable.repart.d/10-root.conf @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root +Format=erofs +CopyFiles=/ +Verity=data +VerityMatchKey=root +Minimize=best diff --git a/mkosi/resources/repart/definitions/portable.repart.d/20-root-verity.conf b/mkosi/resources/repart/definitions/portable.repart.d/20-root-verity.conf new file mode 100644 index 000000000..f8f5b8001 --- /dev/null +++ b/mkosi/resources/repart/definitions/portable.repart.d/20-root-verity.conf @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root-verity +Verity=hash +VerityMatchKey=root +Minimize=best diff --git a/mkosi/resources/repart/definitions/portable.repart.d/30-root-verity-sig.conf b/mkosi/resources/repart/definitions/portable.repart.d/30-root-verity-sig.conf new file mode 100644 index 000000000..19626791b --- /dev/null +++ b/mkosi/resources/repart/definitions/portable.repart.d/30-root-verity-sig.conf @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root-verity-sig +Verity=signature +VerityMatchKey=root diff --git a/mkosi/resources/repart/definitions/sysext.repart.d/10-root.conf b/mkosi/resources/repart/definitions/sysext.repart.d/10-root.conf new file mode 100644 index 000000000..ff35e3871 --- /dev/null +++ b/mkosi/resources/repart/definitions/sysext.repart.d/10-root.conf @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root +Format=erofs +CopyFiles=/usr/ +Verity=data +VerityMatchKey=root +Minimize=best diff --git a/mkosi/resources/repart/definitions/sysext.repart.d/20-root-verity.conf b/mkosi/resources/repart/definitions/sysext.repart.d/20-root-verity.conf new file mode 100644 index 000000000..f8f5b8001 --- /dev/null +++ b/mkosi/resources/repart/definitions/sysext.repart.d/20-root-verity.conf @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root-verity +Verity=hash +VerityMatchKey=root +Minimize=best diff --git a/mkosi/resources/repart/definitions/sysext.repart.d/30-root-verity-sig.conf b/mkosi/resources/repart/definitions/sysext.repart.d/30-root-verity-sig.conf new file mode 100644 index 000000000..19626791b --- /dev/null +++ b/mkosi/resources/repart/definitions/sysext.repart.d/30-root-verity-sig.conf @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Partition] +Type=root-verity-sig +Verity=signature +VerityMatchKey=root diff --git a/mkosi/run.py b/mkosi/run.py index a862c9e91..ea3790a41 100644 --- a/mkosi/run.py +++ b/mkosi/run.py @@ -301,9 +301,13 @@ def have_effective_cap(capability: Capability) -> bool: return (int(hexcap, 16) & (1 << capability.value)) != 0 -def find_binary(name: str, root: Optional[Path] = None) -> Optional[Path]: - path = ":".join(os.fspath(p) for p in [root / "usr/bin", root / "usr/sbin"]) if root else os.environ["PATH"] - return Path("/") / Path(binary).relative_to(root or "/") if (binary := shutil.which(name, path=path)) else None +def find_binary(*names: PathString, root: Optional[Path] = None) -> Optional[Path]: + for name in names: + path = ":".join(os.fspath(p) for p in [root / "usr/bin", root / "usr/sbin"]) if root else os.environ["PATH"] + if (binary := shutil.which(name, path=path)): + return Path("/") / Path(binary).relative_to(root or "/") + + return None def bwrap( @@ -451,7 +455,7 @@ def chroot_cmd(root: Path, *, resolve: bool = False, options: Sequence[PathStrin cmdline += [*options] - if setpgid := find_binary("setpgid", root): + if setpgid := find_binary("setpgid", root=root): cmdline += [setpgid, "--foreground", "--"] return apivfs_cmd(root) + cmdline diff --git a/mkosi/util.py b/mkosi/util.py index 64284974e..d3c2f178d 100644 --- a/mkosi/util.py +++ b/mkosi/util.py @@ -33,15 +33,8 @@ def dictify(f: Callable[..., Iterator[tuple[T, V]]]) -> Callable[..., dict[T, V] @dictify -def read_os_release() -> Iterator[tuple[str, str]]: - try: - filename = "/etc/os-release" - f = open(filename) - except FileNotFoundError: - filename = "/usr/lib/os-release" - f = open(filename) - - with f: +def read_env_file(path: Path) -> Iterator[tuple[str, str]]: + with path.open() as f: for line_number, line in enumerate(f, start=1): line = line.rstrip() if not line or line.startswith("#"): @@ -52,7 +45,15 @@ def read_os_release() -> Iterator[tuple[str, str]]: val = ast.literal_eval(val) yield name, val else: - logging.info(f"{filename}:{line_number}: bad line {line!r}") + logging.info(f"{path}:{line_number}: bad line {line!r}") + + +def read_os_release(root: Path = Path("/")) -> dict[str, str]: + filename = root / "etc/os-release" + if not filename.exists(): + filename = root / "usr/lib/os-release" + + return read_env_file(filename) def format_rlimit(rlimit: int) -> str: diff --git a/tests/__init__.py b/tests/__init__.py index eb743f70f..8e4c815b1 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -98,6 +98,9 @@ class Image: def summary(self, options: Sequence[str] = ()) -> CompletedProcess: return self.mkosi("summary", options, user=INVOKING_USER.uid, group=INVOKING_USER.gid) + def genkey(self) -> CompletedProcess: + return self.mkosi("genkey", ["--force"], user=INVOKING_USER.uid, group=INVOKING_USER.gid) + @pytest.fixture(scope="session", autouse=True) def suspend_capture_stdin(pytestconfig: Any) -> Iterator[None]: diff --git a/tests/test_boot.py b/tests/test_boot.py index 7a55c2f35..15bc2a601 100644 --- a/tests/test_boot.py +++ b/tests/test_boot.py @@ -27,7 +27,7 @@ def test_boot(format: OutputFormat) -> None: options = ["--format", str(format)] image.summary(options) - + image.genkey() image.build(options=options) if format in (OutputFormat.disk, OutputFormat.directory) and os.getuid() == 0: @@ -45,7 +45,7 @@ def test_boot(format: OutputFormat) -> None: if image.distribution == Distribution.rhel_ubi: return - if format == OutputFormat.tar: + if format == OutputFormat.tar or format.is_extension_image(): return if format == OutputFormat.directory and not find_virtiofsd(): -- 2.47.2