From dd744ddd453794b3bbb980c6978ecc899aaa5deb Mon Sep 17 00:00:00 2001 From: kylerankin Date: Mon, 14 Sep 2026 08:54:13 +0000 Subject: [PATCH] feat(installer): adopt Flatcar's EFI-SYSTEM/USR-A/USR-B/ROOT layout Replace the installer's target-disk repart.d drop-ins (10-esp.conf, 20-root-a.conf, 30-var.conf) with Flatcar's GPT layout, adopting the upstream type GUIDs verbatim (projectbluefin/server#134): EFI-SYSTEM ESP/vfat bootctl install + the OS UKI USR-A 5dfbf5f4-... CopyBlocks= the /usr DDI (read-only) USR-B 5dfbf5f4-... empty A/B rollback slot OEM 0fc63daf-... ext4, filesystem label OEM (stage-2 boot dep) ROOT 3884dd41-... writable state, grows to fill the disk BIOS-BOOT stays dropped (UEFI-only, hard rule 5); OEM-CONFIG stays dropped (nothing in the boot path references it). Verity stays off: stage 1 guards the veritysetup block behind [ "${usrhash}" != "" ], so this layout-only change is a bootable intermediate state and verity lands in the /usr DDI ticket. The installer media still stamps bluefin-installer-data, so USR-A's CopyBlocks= source is unchanged. Update the DDI and installer element docs, the fallback bootloader label (bluefin-server-root-a -> ROOT), and the docs/skills target-layout tables. tests/unit/test_repart_layout.py is rewritten for the new layout: it pins the four upstream type GUIDs verbatim, asserts USR-B is provisioned and empty, OEM is labelled ext4, and ROOT grows; the sysupdate label-match is moved to an xfail since wiring 50-root.transfer to USR-A/USR-B is a follow-on ticket. Signed-off-by: kylerankin --- docs/skills/ddi-installer-build.md | 2 +- docs/skills/ddi-installer.md | 17 ++- docs/skills/gap-analysis-distros.md | 4 +- elements/installer/installer-repart.bst | 8 +- elements/oci/bluefin-server-ddi.bst | 2 +- elements/oci/bluefin-server-installer.bst | 4 +- files/installer/repart.d/10-esp.conf | 4 +- files/installer/repart.d/20-root-a.conf | 11 -- files/installer/repart.d/20-usr-a.conf | 10 ++ files/installer/repart.d/30-usr-b.conf | 7 + files/installer/repart.d/30-var.conf | 13 -- files/installer/repart.d/40-oem.conf | 10 ++ files/installer/repart.d/50-root.conf | 11 ++ tests/unit/test_repart_layout.py | 166 ++++++++++++++-------- 14 files changed, 167 insertions(+), 102 deletions(-) delete mode 100644 files/installer/repart.d/20-root-a.conf create mode 100644 files/installer/repart.d/20-usr-a.conf create mode 100644 files/installer/repart.d/30-usr-b.conf delete mode 100644 files/installer/repart.d/30-var.conf create mode 100644 files/installer/repart.d/40-oem.conf create mode 100644 files/installer/repart.d/50-root.conf diff --git a/docs/skills/ddi-installer-build.md b/docs/skills/ddi-installer-build.md index 5d958a2..9f85f90 100644 --- a/docs/skills/ddi-installer-build.md +++ b/docs/skills/ddi-installer-build.md @@ -130,7 +130,7 @@ The release process is driven by `.github/workflows/build.yml`: - [ ] The live installer does not bake hardcoded SSH keys or pre-hashed root passwords. - [ ] The DDI is decompressed after the cpio step. -- [ ] `files/installer/repart.d/20-root-a.conf` has `GrowFileSystem=yes`. +- [ ] `files/installer/repart.d/50-root.conf` has `GrowFileSystem=yes`. ## See also diff --git a/docs/skills/ddi-installer.md b/docs/skills/ddi-installer.md index f6fdfcf..744801e 100644 --- a/docs/skills/ddi-installer.md +++ b/docs/skills/ddi-installer.md @@ -17,7 +17,7 @@ metadata: - Writing or refining `systemd-repart`, `bootctl`, or `ukify` configurations. - Packaging or publishing DDI assets to GitHub Releases. - Managing partition recipes for the target disk layout (`10-esp.conf`, - `20-root-a.conf`, `30-var.conf`). + `20-usr-a.conf`, `30-usr-b.conf`, `40-oem.conf`, `50-root.conf`). ## When NOT to Use @@ -72,8 +72,9 @@ container runtime pod sandboxes. 5. `systemd-sysinstall` reads partition recipes from `/usr/lib/repart.sysinstall.d/` if it is populated; otherwise it falls back to `/usr/lib/repart.d/`. The target recipes are staged at - `/usr/lib/repart.d/` (`10-esp.conf`, `20-root-a.conf`, `30-var.conf`). -6. `20-root-a.conf` copies the DDI block-for-block from + `/usr/lib/repart.d/` (`10-esp.conf`, `20-usr-a.conf`, `30-usr-b.conf`, + `40-oem.conf`, `50-root.conf`). +6. `20-usr-a.conf` copies the DDI block-for-block from `/dev/disk/by-partlabel/bluefin-installer-data` (the embedded DDI data partition on the installer media). 7. Target OS volume expansion is handled by `systemd-growfs`; the target OS @@ -93,9 +94,11 @@ container runtime pod sandboxes. | Partition | Type | Size | Contents | |---|---|---|---| -| ESP | vfat | 500 MiB – 1 GiB | `systemd-boot` + target OS UKI (`bluefin-server.efi`) | -| `bluefin-server-root-a` | XFS | 4 GiB – 16 GiB | OS root filesystem (copied from installer data partition) | -| `var` | XFS | ≥ 4 GiB | Writable persistent `/var`; grows to fill remaining disk | +| EFI-SYSTEM | vfat | 500 MiB – 1 GiB | `systemd-boot` + target OS UKI | +| USR-A | Flatcar `usr` (5dfbf5f4-…) | ≥ 4 GiB | read-only `/usr` (DDI copied block-for-block from installer data partition) | +| USR-B | Flatcar `usr` (5dfbf5f4-…) | ≥ 4 GiB | empty A/B rollback slot | +| OEM | ext4 | 1 GiB | provider/first-boot state; filesystem label `OEM` | +| ROOT | Flatcar `root` (3884dd41-…) | writable | writable state; grows to fill remaining disk | ## Installer Boot Flow @@ -187,7 +190,7 @@ offline installation. - [ ] The interactive installer service sets `TTYPath=/dev/tty0` so the TUI appears on the attached display even when serial is the primary console. - [ ] `bluefin-server-installer.bst` decompresses the DDI after the cpio step. -- [ ] `files/installer/repart.d/20-root-a.conf` has `GrowFileSystem=yes`. +- [ ] `files/installer/repart.d/50-root.conf` has `GrowFileSystem=yes`. ## See also diff --git a/docs/skills/gap-analysis-distros.md b/docs/skills/gap-analysis-distros.md index 12b4413..734f252 100644 --- a/docs/skills/gap-analysis-distros.md +++ b/docs/skills/gap-analysis-distros.md @@ -95,7 +95,7 @@ Bluefin Server is a BuildStream 2-based, image-based Linux server OS built from | Axis | Bluefin Server (as-implemented) | |------|---------------------------------| | **Philosophy** | Systemd-native, minimal, image-based server OS appliance; base DDI includes bash for login and bring-up while heavy developer/debug tools live in sysexts or system containers; intended to run container workloads and Kubernetes via optional sysexts. Sources: [AGENTS.md](../../AGENTS.md), [factory-integration.md](factory-integration.md). | -| **State model** | Target OS DDI is an XFS filesystem image. A separate persistent `/var` partition is created by the installer. There is no second root slot provisioned today, and the UKI cmdline currently uses `rw`, so the root is not mounted read-only at runtime. Sources: [bluefin-server-ddi.bst](../../elements/oci/bluefin-server-ddi.bst), [20-root-a.conf](../../files/installer/repart.d/20-root-a.conf), [bluefin-server-installer.bst](../../elements/oci/bluefin-server-installer.bst). | +| **State model** | Target OS DDI is an XFS filesystem image. A separate persistent `/var` partition is created by the installer. There is no second root slot provisioned today, and the UKI cmdline currently uses `rw`, so the root is not mounted read-only at runtime. Sources: [bluefin-server-ddi.bst](../../elements/oci/bluefin-server-ddi.bst), [20-usr-a.conf](../../files/installer/repart.d/20-usr-a.conf), [bluefin-server-installer.bst](../../elements/oci/bluefin-server-installer.bst). | | **Updates** | `systemd-sysupdate` reads root/UKI transfers from `files/os/sysupdate.d/` and the optional k0s transfer from the `k0s` component directory. Assets are published to GitHub Releases, and the combined `SHA256SUMS` manifest is signed in CI with a GPG key. `Verify=yes` is the default. Sources: [systemd-sysupdate-verification.md](systemd-sysupdate-verification.md), [50-root.transfer](../../files/os/sysupdate.d/50-root.transfer), [60-uki.transfer](../../files/os/sysupdate.d/60-uki.transfer), [70-k0s.transfer](../../files/os/sysupdate.k0s.d/70-k0s.transfer), also `systemd-sysupdate(8)`. | | **Provisioning** | The installer is an offline `systemd-sysinstall` image that embeds the DDI as a data partition. First-boot configuration is intended to be delivered via `systemd-creds` through the ESP or hypervisor metadata. Today only `passwd.hashed-password.root` is consumed via `systemd-sysusers.d`; the documented `tmpfiles.extra` path for SSH keys and similar files is not implemented. Sources: [bluefin-server-installer.bst](../../elements/oci/bluefin-server-installer.bst), [10-root-creds.conf](../../files/os/sysusers.d/10-root-creds.conf), [os-creds-prov.bst](../../elements/bluefin-server/os-creds-prov.bst), [systemd-creds(1)](https://www.freedesktop.org/software/systemd/man/latest/systemd-creds.html). | | **Customization** | Adds software through `systemd-sysext` (overlay `/usr`) and `systemd-confext` (overlay `/etc`) images. The base OS `os-release` advertises `ID=flatcar` and a matching `VERSION_ID` so pre-built Flatcar Bakery extensions load. k0s is shipped as a separately built, optionally enabled sysext. Sources: [systemd-sysext-extensions.md](systemd-sysext-extensions.md), [k0s-sysext.md](k0s-sysext.md), [os-release-flatcar.bst](../../elements/bluefin-server/os-release-flatcar.bst), [systemd-sysext(8)](https://www.freedesktop.org/software/systemd/man/latest/systemd-sysext.html). | @@ -189,7 +189,7 @@ These gaps drive the priorities in [architecture-roadmap.md](architecture-roadma - [elements/bluefin-server/os-release-flatcar.bst](../../elements/bluefin-server/os-release-flatcar.bst) - [elements/bluefin-server/os-creds-prov.bst](../../elements/bluefin-server/os-creds-prov.bst) - [elements/bluefin-server/os-kured-hook.bst](../../elements/bluefin-server/os-kured-hook.bst) -- [files/installer/repart.d/20-root-a.conf](../../files/installer/repart.d/20-root-a.conf) +- [files/installer/repart.d/20-usr-a.conf](../../files/installer/repart.d/20-usr-a.conf) - [files/os/sysupdate.d/50-root.transfer](../../files/os/sysupdate.d/50-root.transfer) - [files/os/sysupdate.d/60-uki.transfer](../../files/os/sysupdate.d/60-uki.transfer) - [files/os/sysupdate.k0s.d/70-k0s.transfer](../../files/os/sysupdate.k0s.d/70-k0s.transfer) diff --git a/elements/installer/installer-repart.bst b/elements/installer/installer-repart.bst index 6d94b3b..83780e3 100644 --- a/elements/installer/installer-repart.bst +++ b/elements/installer/installer-repart.bst @@ -6,9 +6,11 @@ description: | to partition and populate the TARGET disk. Partition layout: - 10-esp.conf EFI System Partition (vfat, 500M–1G) - 20-root-a.conf Root slot A — DDI filesystem image copied here - 30-var.conf /var — xfs, writable, grows to fill disk + 10-esp.conf EFI-SYSTEM (vfat, 500M–1G) + 20-usr-a.conf USR-A — DDI /usr payload copied here block-for-block + 30-usr-b.conf USR-B — empty A/B rollback slot + 40-oem.conf OEM — ext4, filesystem label OEM (stage 2 boot dep) + 50-root.conf ROOT — writable state, grows to fill the disk sources: - kind: local diff --git a/elements/oci/bluefin-server-ddi.bst b/elements/oci/bluefin-server-ddi.bst index 9f80157..a68930a 100644 --- a/elements/oci/bluefin-server-ddi.bst +++ b/elements/oci/bluefin-server-ddi.bst @@ -5,7 +5,7 @@ description: | Output: bluefin-server-ddi-.raw.zst + SHA256SUMS The output .raw is a filesystem image consumed by installer repart config: - files/installer/repart.d/20-root-a.conf via CopyBlocks=. + files/installer/repart.d/20-usr-a.conf via CopyBlocks=. build-depends: - base/base-stack.bst diff --git a/elements/oci/bluefin-server-installer.bst b/elements/oci/bluefin-server-installer.bst index 44d9f30..9fd1126 100644 --- a/elements/oci/bluefin-server-installer.bst +++ b/elements/oci/bluefin-server-installer.bst @@ -216,7 +216,7 @@ config: udevadm settle --timeout=10 || true local target="${TARGET_DISK:-}" if [ -z "${target}" ]; then - local root_part="$(readlink -f /dev/disk/by-partlabel/bluefin-server-root-a 2>/dev/null || true)" + local root_part="$(readlink -f /dev/disk/by-partlabel/ROOT 2>/dev/null || true)" if [ -n "${root_part}" ]; then target="$(lsblk -p -n -d -o PKNAME "${root_part}" 2>/dev/null || true)" fi @@ -400,7 +400,7 @@ config: # 20-ddi-data.conf: Data (XFS) — bluefin-server-ddi.raw copied here block-for-block # # At install time, systemd-sysinstall mounts the data partition to - # locate the DDI at the path expected by repart.d/20-root-a.conf (CopyBlocks=). + # locate the DDI at the path expected by repart.d/20-usr-a.conf (CopyBlocks=). - | set -eux OUT="%{install-root}" diff --git a/files/installer/repart.d/10-esp.conf b/files/installer/repart.d/10-esp.conf index de2e201..cfdff78 100644 --- a/files/installer/repart.d/10-esp.conf +++ b/files/installer/repart.d/10-esp.conf @@ -1,9 +1,11 @@ -# EFI System Partition +# EFI System Partition — PARTLABEL EFI-SYSTEM # Created empty by systemd-repart; populated post-install by the installer # script via: bootctl install --esp-path= --root= # which copies systemd-boot and registers the OS UKI from the installed rootfs. +# Type=esp sets the ESP type GUID (c12a7328-f81f-11d2-ba4b-00a0c93ec93b) verbatim. [Partition] Type=esp Format=vfat +Label=EFI-SYSTEM SizeMinBytes=500M SizeMaxBytes=1G diff --git a/files/installer/repart.d/20-root-a.conf b/files/installer/repart.d/20-root-a.conf deleted file mode 100644 index b10319b..0000000 --- a/files/installer/repart.d/20-root-a.conf +++ /dev/null @@ -1,11 +0,0 @@ -# Root partition — slot A -# systemd-repart copies the DDI payload block-for-block from the embedded -# installer data partition on the boot medium. -# The source is a raw filesystem image; repart copies it verbatim. -[Partition] -Type=root -Label=bluefin-server-root-a -CopyBlocks=/dev/disk/by-partlabel/bluefin-installer-data -SizeMinBytes=4G -SizeMaxBytes=16G -GrowFileSystem=yes diff --git a/files/installer/repart.d/20-usr-a.conf b/files/installer/repart.d/20-usr-a.conf new file mode 100644 index 0000000..ea73187 --- /dev/null +++ b/files/installer/repart.d/20-usr-a.conf @@ -0,0 +1,10 @@ +# /usr slot A — read-only, verity-capable (veritysetup lands in the /usr DDI ticket) +# Type GUID: Flatcar's usr partition, 5dfbf5f4-2848-4bac-aa5e-0d9a20b745a6, verbatim. +# systemd-repart copies the DDI /usr payload block-for-block from the embedded +# installer data partition on the boot medium (same source the old 20-root-a.conf +# consumed): the raw XFS DDI image stamped bluefin-installer-data. +[Partition] +Type=5dfbf5f4-2848-4bac-aa5e-0d9a20b745a6 +Label=USR-A +CopyBlocks=/dev/disk/by-partlabel/bluefin-installer-data +SizeMinBytes=4G diff --git a/files/installer/repart.d/30-usr-b.conf b/files/installer/repart.d/30-usr-b.conf new file mode 100644 index 0000000..96847e5 --- /dev/null +++ b/files/installer/repart.d/30-usr-b.conf @@ -0,0 +1,7 @@ +# /usr slot B — the A/B rollback slot. Empty and unformatted in this ticket; +# verity lands in the /usr DDI ticket. Same type GUID as USR-A so the kernel's +# usr=PARTLABEL=USR-A logic recognises the pair; sized to match USR-A. +[Partition] +Type=5dfbf5f4-2848-4bac-aa5e-0d9a20b745a6 +Label=USR-B +SizeMinBytes=4G diff --git a/files/installer/repart.d/30-var.conf b/files/installer/repart.d/30-var.conf deleted file mode 100644 index 7f2ec34..0000000 --- a/files/installer/repart.d/30-var.conf +++ /dev/null @@ -1,13 +0,0 @@ -# /var partition — writable, persistent across updates -# NOTE: Do NOT set FactoryReset=yes here: systemd-sysinstall hardcodes -# deferPartitionsFactoryReset=true, which causes the installer to skip -# creating /var entirely. -# GrowFileSystem=yes: XFS grows to fill all remaining disk space after -# the ESP and root-a partitions are placed. -[Partition] -Type=var -Label=var -Format=xfs -GrowFileSystem=yes -CopyFiles=/k0s.raw:/lib/k0s/k0s.raw -SizeMinBytes=4G diff --git a/files/installer/repart.d/40-oem.conf b/files/installer/repart.d/40-oem.conf new file mode 100644 index 0000000..15dddaf --- /dev/null +++ b/files/installer/repart.d/40-oem.conf @@ -0,0 +1,10 @@ +# OEM partition — provider/first-boot state. +# Required, not optional: stage 2 depends on dev-disk-by-label-OEM.device, so +# without it the boot waits 90s and drops to an emergency shell. The dependency +# matches on the filesystem LABEL, so Label=OEM formats the fs with that label. +# Type GUID: Flatcar's OEM partition, 0fc63daf-8483-4772-8e79-3d69d8477de4, verbatim. +[Partition] +Type=0fc63daf-8483-4772-8e79-3d69d8477de4 +Format=ext4 +Label=OEM +SizeMinBytes=1024M diff --git a/files/installer/repart.d/50-root.conf b/files/installer/repart.d/50-root.conf new file mode 100644 index 0000000..4443f6a --- /dev/null +++ b/files/installer/repart.d/50-root.conf @@ -0,0 +1,11 @@ +# Root partition — writable state, the tail of the disk. +# Type GUID: Flatcar's root partition, 3884dd41-8582-4404-b9a8-e9b84f2df50e, verbatim. +# The kernel boots this as root (root=PARTLABEL=ROOT); GrowFileSystem=yes makes +# it fill all space left after EFI-SYSTEM, USR-A, USR-B and OEM are placed. +# Format= leaves the filesystem type to systemd-repart's auto-detection. +[Partition] +Type=3884dd41-8582-4404-b9a8-e9b84f2df50e +Format= +Label=ROOT +GrowFileSystem=yes +SizeMinBytes=4G diff --git a/tests/unit/test_repart_layout.py b/tests/unit/test_repart_layout.py index bf5a0bd..9a24185 100644 --- a/tests/unit/test_repart_layout.py +++ b/tests/unit/test_repart_layout.py @@ -3,9 +3,13 @@ ``files/installer/repart.d/*.conf`` is the recipe ``systemd-repart`` follows when the live installer partitions the *target* disk. Nothing in CI parses these files today, so a typo in a ``Type=``, a ``CopyBlocks=`` source that no longer -matches the label the installer media stamps on its data partition, or an -``esp``/``root``/``var`` slot going missing would only surface as a failed or -silently mis-partitioned install. +matches the label the installer media stamps on its data partition, or a +partition going missing would only surface as a failed or silently +mis-partitioned install. + +The layout is Flatcar's EFI-SYSTEM / USR-A / USR-B / OEM / ROOT, adopted in +projectbluefin/server#134. The ``Type=`` values for USR-A, USR-B, OEM and ROOT +are upstream GPT type GUIDs and must appear verbatim. These tests are pure static checks: they read the shipped configs (plus the installer element and the sysupdate transfer for cross-file consistency) and @@ -26,6 +30,15 @@ ELEMENTS_DIR = REPO_ROOT / "elements" ROOT_TRANSFER = REPO_ROOT / "files" / "os" / "sysupdate.d" / "50-root.transfer" +# Flatcar GPT type GUIDs, adopted verbatim by projectbluefin/server#134. +ESP_GUID = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" +USR_GUID = "5dfbf5f4-2848-4bac-aa5e-0d9a20b745a6" +OEM_GUID = "0fc63daf-8483-4772-8e79-3d69d8477de4" +ROOT_GUID = "3884dd41-8582-4404-b9a8-e9b84f2df50e" + +# PARTLABELs the installer provisions on the target disk, in repart order. +EXPECTED_LABELS = ["EFI-SYSTEM", "USR-A", "USR-B", "OEM", "ROOT"] + SIZE_SUFFIXES = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4} @@ -105,15 +118,23 @@ def test_config_ordering_prefixes_are_unique(): ) -def test_expected_partition_types_are_present_exactly_once(): - types = [section["Type"] for section in partitions().values()] - assert sorted(types) == ["esp", "root", "var"], ( - "the target layout must be exactly one esp, one root and one var " - f"partition, got {sorted(types)}" +def test_expected_partitions_are_present_exactly_once(): + labels = [ + section["Label"] + for section in partitions().values() + if "Label" in section + ] + assert sorted(labels) == sorted(EXPECTED_LABELS), ( + "the target layout must be exactly one each of EFI-SYSTEM, USR-A, " + f"USR-B, OEM and ROOT partition, got {sorted(labels)}" ) -@pytest.mark.parametrize("name,section", sorted(partitions().items())) +@pytest.mark.parametrize( + "name,section", + sorted(partitions().items()), + ids=[p.name for p in CONFIG_PATHS], +) def test_size_bounds_are_consistent(name: str, section: dict[str, str]): minimum = section.get("SizeMinBytes") maximum = section.get("SizeMaxBytes") @@ -136,8 +157,13 @@ def test_partition_labels_are_unique(): ) -def test_esp_is_vfat_and_bounded(): - esp = next(s for s in partitions().values() if s["Type"] == "esp") +def test_efi_system_is_vfat_and_bounded(): + esp = next( + (s for s in partitions().values() if s.get("Label") == "EFI-SYSTEM"), + None, + ) + assert esp is not None, "EFI-SYSTEM partition missing" + assert esp.get("Type") == "esp", "the EFI-SYSTEM partition must be an ESP" assert esp["Format"] == "vfat", "an ESP that is not vfat is unbootable by UEFI" assert parse_size(esp["SizeMinBytes"]) >= 100 * 1024**2, ( "the ESP must be large enough for systemd-boot plus at least one UKI" @@ -145,12 +171,19 @@ def test_esp_is_vfat_and_bounded(): assert "SizeMaxBytes" in esp, "the ESP must be capped so it cannot eat the disk" -def test_root_slot_copies_blocks_from_a_label_the_installer_media_stamps(): - root = next(s for s in partitions().values() if s["Type"] == "root") - copy_blocks = root.get("CopyBlocks") +def test_usr_a_copies_blocks_from_a_label_the_installer_media_stamps(): + usr_a = next( + (s for s in partitions().values() if s.get("Label") == "USR-A"), + None, + ) + assert usr_a is not None, "USR-A partition missing" + assert usr_a["Type"] == USR_GUID, ( + "USR-A must carry Flatcar's usr type GUID verbatim" + ) + copy_blocks = usr_a.get("CopyBlocks") assert copy_blocks, ( - "the root partition must CopyBlocks= the DDI payload; without it the " - "installed system has an empty root filesystem" + "USR-A must CopyBlocks= the /usr DDI payload; without it the installed " + "system has an empty /usr" ) prefix = "/dev/disk/by-partlabel/" assert copy_blocks.startswith(prefix), ( @@ -171,65 +204,76 @@ def test_root_slot_copies_blocks_from_a_label_the_installer_media_stamps(): ) -def test_root_slot_grows_and_is_bounded_below_the_var_partition(): - root = next(s for s in partitions().values() if s["Type"] == "root") - assert root.get("GrowFileSystem") == "yes", ( - "the root filesystem must grow to its partition, the DDI payload is " - "smaller than SizeMinBytes" +def test_usr_b_exists_and_is_empty(): + usr_b = next( + (s for s in partitions().values() if s.get("Label") == "USR-B"), + None, + ) + assert usr_b is not None, "USR-B partition missing" + assert usr_b["Type"] == USR_GUID, ( + "USR-B must carry Flatcar's usr type GUID verbatim so the kernel " + "recognises the USR-A/USR-B pair" ) - assert "SizeMaxBytes" in root, ( - "the root slot must be capped, otherwise /var gets no space on small disks" + assert "CopyBlocks" not in usr_b, ( + "USR-B is the rollback slot and must stay empty, not a copy of the DDI" ) + assert "CopyFiles" not in usr_b, "USR-B must be empty, not seeded from files" + assert "Format" not in usr_b, "USR-B must be left unformatted (empty)" -def test_var_is_a_growing_xfs_tail(): - var = next(s for s in partitions().values() if s["Type"] == "var") - assert var["Format"] == "xfs" - # FactoryReset=yes must NOT be set on the installer var partition: - # systemd-sysinstall hardcodes deferPartitionsFactoryReset=true via - # Varlink io.systemd.Repart.Run, causing it to defer creating /var. - assert "FactoryReset" not in var, ( - "FactoryReset must not be set on installer var partition or " - "systemd-sysinstall will defer creating it" +def test_oem_is_labelled_ext4(): + oem = next( + (s for s in partitions().values() if s.get("Label") == "OEM"), + None, ) - assert var["GrowFileSystem"] == "yes" - assert "SizeMaxBytes" not in var, ( - "/var is the tail partition and must grow into all remaining space" + assert oem is not None, "OEM partition missing" + assert oem["Type"] == OEM_GUID, ( + "OEM must carry Flatcar's OEM type GUID verbatim" + ) + assert oem.get("Format") == "ext4", "OEM must be ext4" + # Stage 2 waits on dev-disk-by-label-OEM.device: the match is on the + # filesystem LABEL, so Label=OEM must also be the filesystem label. + assert oem.get("Label") == "OEM", ( + "OEM must be formatted with filesystem label OEM or stage 2 never finds it" ) -def test_var_seeds_the_offline_k0s_sysext(): - var = next(s for s in partitions().values() if s["Type"] == "var") - assert var["CopyFiles"] == "/k0s.raw:/lib/k0s/k0s.raw" - - -def test_root_partition_label_is_matched_by_the_sysupdate_root_transfer(): - root = next(s for s in partitions().values() if s["Type"] == "root") - targets = sysupdate_root_targets() - prefix, _, suffix = targets[0].partition("@v") - assert root["Label"].startswith(prefix) and root["Label"].endswith(suffix.lstrip("_")), ( - f"the installed root label {root['Label']!r} is not matched by " - f"sysupdate target pattern {targets[0]}; OTA updates would find no slot" +def test_root_grows_and_is_bounded_below_oem(): + root = next( + (s for s in partitions().values() if s.get("Label") == "ROOT"), + None, + ) + assert root is not None, "ROOT partition missing" + assert root["Type"] == ROOT_GUID, ( + "ROOT must carry Flatcar's root type GUID verbatim" + ) + assert root.get("GrowFileSystem") == "yes", ( + "the ROOT partition must grow to fill the disk after the other slots" + ) + assert "SizeMaxBytes" not in root, ( + "ROOT is the tail partition and must grow into all remaining space" ) @pytest.mark.xfail( strict=True, reason=( - "Known gap, tracked in docs/MVP_1_0_READINESS.md: 50-root.transfer names " - "root-a and root-b, but the installer provisions only root-a, so " - "systemd-sysupdate has no inactive slot to stage into and no atomic " - "rollback path. When root-b is added this test XPASSes and must be " - "un-xfailed." + "Follow-on: 50-root.transfer still targets the legacy " + "bluefin-server-root-_a/_b labels, but the Flatcar layout (project" + "bluefin/server#134) provisions USR-A/USR-B instead. Wiring the transfer " + "to the real /usr slots and retiring this xfail is a later ticket, so " + "the label-match assertion is expected to fail until then." ), ) -def test_every_sysupdate_root_target_is_provisioned_by_the_installer(): - targets = set(sysupdate_root_targets()) - provisioned = { - section["Label"] - for section in partitions().values() - if section["Type"] == "root" - } - assert len(targets) <= len(provisioned), ( - f"sysupdate targets {sorted(targets)} require dual slots, only {provisioned} provisioned" +def test_root_partition_label_is_matched_by_the_sysupdate_root_transfer(): + root = next( + (s for s in partitions().values() if s.get("Label") == "ROOT"), + None, + ) + assert root is not None + targets = sysupdate_root_targets() + prefix, _, suffix = targets[0].partition("@v") + assert root["Label"].startswith(prefix) and root["Label"].endswith(suffix.lstrip("_")), ( + f"the installed root label {root['Label']!r} is not matched by " + f"sysupdate target pattern {targets[0]}; OTA updates would find no slot" )