]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add sysext, confext and portable support 2128/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 6 Dec 2023 14:04:54 +0000 (15:04 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 7 Dec 2023 17:14:28 +0000 (18:14 +0100)
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.

19 files changed:
docs/sysext.md
mkosi/__init__.py
mkosi/config.py
mkosi/resources/mkosi.md
mkosi/resources/repart/__init__.py [new file with mode: 0644]
mkosi/resources/repart/definitions/__init__.py [new file with mode: 0644]
mkosi/resources/repart/definitions/confext.repart.d/10-root.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/confext.repart.d/20-root-verity.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/confext.repart.d/30-root-verity-sig.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/portable.repart.d/10-root.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/portable.repart.d/20-root-verity.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/portable.repart.d/30-root-verity-sig.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/sysext.repart.d/10-root.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/sysext.repart.d/20-root-verity.conf [new file with mode: 0644]
mkosi/resources/repart/definitions/sysext.repart.d/30-root-verity-sig.conf [new file with mode: 0644]
mkosi/run.py
mkosi/util.py
tests/__init__.py
tests/test_boot.py

index 045e10c484b37ec8bfc8c36a59c8e7dfb7e6ef12..52c6dff1abb3ffdd0ebe99abe60fb18a952f3fca 100644 (file)
@@ -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=<distribution>
-VERSION_ID=<distribution-version>
-ARCHITECTURE=<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.
 
index 1ae70f872daa5488e39e44b292730fdb3b08ef59..cb5a11a6a54311fd11619c7558d389c1f7599a48 100644 (file)
@@ -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)
 
index 7b6ffbf874af8802acec31e35a271a129e3cee98..92670f803e5f2ad2aa33ed0489007de02189cdb8 100644 (file)
@@ -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")}:
index 873e9017ece74ac877dcef76db42fc9a38871b27..770191bd203495c8ebf1b34cb0e93b03456e267c 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/mkosi/resources/repart/definitions/__init__.py b/mkosi/resources/repart/definitions/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
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 (file)
index 0000000..6b7b8f5
--- /dev/null
@@ -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 (file)
index 0000000..f8f5b80
--- /dev/null
@@ -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 (file)
index 0000000..1962679
--- /dev/null
@@ -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 (file)
index 0000000..2153470
--- /dev/null
@@ -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 (file)
index 0000000..f8f5b80
--- /dev/null
@@ -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 (file)
index 0000000..1962679
--- /dev/null
@@ -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 (file)
index 0000000..ff35e38
--- /dev/null
@@ -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 (file)
index 0000000..f8f5b80
--- /dev/null
@@ -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 (file)
index 0000000..1962679
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Partition]
+Type=root-verity-sig
+Verity=signature
+VerityMatchKey=root
index a862c9e91be5ee353ef145bbaf8bbca10b3783bd..ea3790a41d3e9e70795222f0779f2e904e0fcdf1 100644 (file)
@@ -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
index 64284974ebe821588eea5cb4d2b8c0f3d4ea1307..d3c2f178d2186718025c756c72b0037d1ce9d4dc 100644 (file)
@@ -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:
index eb743f70f9b8436ae3fd8e07f197e9ef01bc3e6e..8e4c815b1c931cfc3a0ae4dbcccf05f7f4d3a107 100644 (file)
@@ -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]:
index 7a55c2f35f3f8f031e6da626839fcb24b5fcd978..15bc2a6018d52e2885d316da5d7f4b2bcdac6c28 100644 (file)
@@ -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():