From 8967a27c2bd23e98cd6fd86a0ece79edc58d2090 Mon Sep 17 00:00:00 2001 From: Tomaz Zaman Date: Tue, 18 Aug 2026 23:26:54 +0200 Subject: [PATCH 75/77] layerscape: mono_gateway-dk A/B + persistent /data eMMC layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-carve the eMMC from the old 2-partition table (boot + a rootfs that filled the disk) into a 5-partition A/B layout with a persistent /data partition that sysupgrade never rewrites: p1 bootA 64M · p2 rootA 1G · p3 bootB 64M · p4 rootB 1G · p5 data ~28G Slot A keeps the pre-A/B start sectors, so an existing unit's rootfs stays put and only the GPT changes on migration. - mono_gpt.py: 2 -> 5 GPT entries (still in the 4 KiB, 8-entry array); MONO_ROOTFS_PART is now the per-slot size (1 GiB). Ships the primary + 33-sector backup GPT blobs in the boot partition. - First boot: 11-mono-gateway-data formats p5 and mounts /data (by mount-test, no block-tool dependency); adds e2fsprogs and re-enables block-mount (it was disabled in the seed) to remount /data each boot. - 05-mono-gateway-migrate: an old-layout unit boots the new image on the old GPT, then this rewrites the GPT from the shipped blobs and reboots into the A/B layout - single step, GPT-only, rootfs untouched; a marker guards against a reboot loop. Validated on the bench DUT: an old 2-partition unit migrated to A/B on first boot, /data auto-mounts, and a file in /data survived a sysupgrade. Co-Authored-By: Claude Opus 4.8 --- configs/mono_gateway-dk.seed | 2 +- .../etc/uci-defaults/05-mono-gateway-migrate | 62 +++++++++++++++++++ .../10-mono-gateway-expand-rootfs | 9 +-- .../etc/uci-defaults/11-mono-gateway-data | 52 ++++++++++++++++ .../base-files/lib/upgrade/platform.sh | 5 +- target/linux/layerscape/image/armv8_64b.mk | 15 +++-- .../layerscape/image/gen_mono_emmc_img.sh | 14 +++-- target/linux/layerscape/image/mono_gpt.py | 48 +++++++++----- 8 files changed, 175 insertions(+), 32 deletions(-) create mode 100644 target/linux/layerscape/base-files/etc/uci-defaults/05-mono-gateway-migrate create mode 100644 target/linux/layerscape/base-files/etc/uci-defaults/11-mono-gateway-data diff --git a/configs/mono_gateway-dk.seed b/configs/mono_gateway-dk.seed index cd2bdd1094..260fdc8e9e 100644 --- a/configs/mono_gateway-dk.seed +++ b/configs/mono_gateway-dk.seed @@ -21,7 +21,7 @@ CONFIG_KERNEL_GIT_CLONE_URI="https://github.com/nxp-qoriq/linux" CONFIG_KERNEL_GIT_LOCAL_REPOSITORY="" CONFIG_KERNEL_GIT_MIRROR_HASH="" CONFIG_KERNEL_GIT_REF="df24f9428e38740256a410b983003a478e72a7c0" -# CONFIG_PACKAGE_block-mount is not set +CONFIG_PACKAGE_block-mount=y # CONFIG_PACKAGE_ethtool-full is not set # CONFIG_PACKAGE_ip-full is not set CONFIG_PACKAGE_ip-tiny=y diff --git a/target/linux/layerscape/base-files/etc/uci-defaults/05-mono-gateway-migrate b/target/linux/layerscape/base-files/etc/uci-defaults/05-mono-gateway-migrate new file mode 100644 index 0000000000..312fd5c97a --- /dev/null +++ b/target/linux/layerscape/base-files/etc/uci-defaults/05-mono-gateway-migrate @@ -0,0 +1,62 @@ +#!/bin/sh +# One-time migration to the A/B + persistent-/data eMMC layout, run on the +# FIRST boot of the new image. A unit flashed before the A/B layout boots here +# on the OLD 2-partition GPT (p2 = the whole eMMC, no /data); sysupgrade never +# rewrites the GPT, and it runs the OLD system's platform.sh anyway - so the +# migration happens here instead, from the running new image. +# +# p1 (bootA) and p2's start (rootA @ 196608) are identical in both layouts, so +# the running rootfs on p2 is undisturbed: only the GPT sectors change (the +# primary at 0-3 and the 33-sector backup tail). The new table comes from the +# blobs shipped in the boot partition. After the swap we reboot so the kernel +# re-reads the table (it will not re-read while p2 is mounted); p5 then appears +# and 11-mono-gateway-data formats and mounts /data. +# +# Runs before 10-expand / 11-data (numeric order) and reboots before them. +# No-op once on the A/B layout (p5 present); a marker guards against a reboot +# loop should p5 ever fail to appear. uci-defaults self-delete on exit 0. +. /lib/functions.sh + +case "$(board_name)" in +mono,gateway-dk) ;; +*) exit 0 ;; +esac + +# Already migrated (p5 present), or migration already attempted - do nothing. +[ -e /sys/class/block/mmcblk0p5 ] && exit 0 +[ -f /etc/mono-ab-migrated ] && exit 0 + +disk=/dev/mmcblk0 +sectors="$(cat /sys/block/mmcblk0/size 2>/dev/null)" +[ -n "$sectors" ] || { logger -t mono-migrate "no disk size, deferring"; exit 1; } + +mkdir -p /tmp/mono-bootp +mount -t ext4 -o ro /dev/mmcblk0p1 /tmp/mono-bootp 2>/dev/null || { + logger -t mono-migrate "cannot mount boot partition, deferring" + exit 1 +} +P=/tmp/mono-bootp/boot/primary-gpt.bin +B=/tmp/mono-bootp/boot/backup-gpt.bin +if [ ! -f "$P" ] || [ ! -f "$B" ]; then + umount /tmp/mono-bootp 2>/dev/null + logger -t mono-migrate "GPT blobs missing in boot partition, cannot migrate" + exit 1 +fi + +logger -t mono-migrate "migrating eMMC to the A/B + /data GPT" +# Commit the new table: primary (sectors 0-3) first, then the 33-sector backup +# tail. Only the GPT changes; the mounted rootfs on p2 is untouched. +if dd if="$P" of="$disk" bs=512 conv=fsync 2>/dev/null && \ + dd if="$B" of="$disk" bs=512 seek=$(( sectors - 33 )) conv=fsync 2>/dev/null; then + sync + : > /etc/mono-ab-migrated + sync + umount /tmp/mono-bootp 2>/dev/null + logger -t mono-migrate "GPT migrated; rebooting to activate the new table" + reboot -f + exit 0 +fi + +umount /tmp/mono-bootp 2>/dev/null +logger -t mono-migrate "GPT write failed, will retry next boot" +exit 1 diff --git a/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs b/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs index b9bf04bf41..ca26bdf111 100644 --- a/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs +++ b/target/linux/layerscape/base-files/etc/uci-defaults/10-mono-gateway-expand-rootfs @@ -1,9 +1,10 @@ #!/bin/sh # First-boot disk fixups for the Mono Gateway. Two jobs: -# 1. Grow the rootfs to fill its partition. The image ships a small ext4 -# inside a partition already sized to the full eMMC (the on-device GPT -# is never rewritten by an upgrade, and its entry array sits in the -# first 4 KiB clear of the boot firmware; see mono_gpt.py). +# 1. Grow rootA (p2) to fill its 1 GiB partition. The image ships a small +# ext4 inside the (now fixed-size) rootfs slot; the on-device GPT is never +# rewritten by a normal upgrade, and its entry array sits in the first +# 4 KiB clear of the boot firmware (see mono_gpt.py). The persistent /data +# partition (p5) is set up separately by 11-mono-gateway-data. # 2. Write the backup GPT to the end of the device from the blob shipped in # the boot partition, so the on-disk table is complete and partition # tools do not "repair" it. diff --git a/target/linux/layerscape/base-files/etc/uci-defaults/11-mono-gateway-data b/target/linux/layerscape/base-files/etc/uci-defaults/11-mono-gateway-data new file mode 100644 index 0000000000..f00fbe3be8 --- /dev/null +++ b/target/linux/layerscape/base-files/etc/uci-defaults/11-mono-gateway-data @@ -0,0 +1,52 @@ +#!/bin/sh +# First-boot setup of the persistent /data partition (p5) for the Mono Gateway. +# The A/B eMMC layout (see mono_gpt.py) reserves p5 for user data that MUST +# survive sysupgrade - which rewrites only bootA (p1) and rootA (p2), never p5. +# The flashed image seeds only bootA+rootA, so p5 is created here: format it +# once, register it in fstab (block-mount remounts it every boot), and mount it. +# uci-defaults self-delete on exit 0; this exits non-zero to retry next boot +# until /data is mounted, and is idempotent. +. /lib/functions.sh + +case "$(board_name)" in +mono,gateway-dk) ;; +*) exit 0 ;; +esac + +DATA_DEV=/dev/mmcblk0p5 +[ -b "$DATA_DEV" ] || { + logger -t mono-firstboot "no $DATA_DEV yet, deferring /data setup" + exit 1 +} + +mkdir -p /data + +# Format once. Detect "already formatted" by trying to mount it - not with +# block/blkid, so this never depends on an optional tool. lazy_*_init keeps the +# one-time mkfs on the ~28 GiB partition from stretching first boot. +if ! mount -t ext4 -o rw,noatime "$DATA_DEV" /data 2>/dev/null; then + logger -t mono-firstboot "formatting $DATA_DEV as ext4 (label data)" + if ! mkfs.ext4 -q -F -L data -E lazy_itable_init=1,lazy_journal_init=1 "$DATA_DEV" >/dev/null 2>&1; then + logger -t mono-firstboot "mkfs on $DATA_DEV failed, will retry" + exit 1 + fi + mount -t ext4 -o rw,noatime "$DATA_DEV" /data 2>/dev/null || { + logger -t mono-firstboot "mount of $DATA_DEV failed, will retry" + exit 1 + } +fi + +# Register the mount by fs label (named section => idempotent); block-mount +# remounts it on every subsequent boot. +uci -q batch < $@.bootdir/boot/extlinux/extlinux.conf - # Backup GPT (33-sector tail) travels in the boot partition; first boot - # dd's it to the device end so the on-disk table is complete. + # GPT blobs travel in the boot partition: the 33-sector backup tail (first + # boot dd's it to the device end so the on-disk table is complete) and the + # 4 KiB primary table (the sysupgrade migration path dd's it to repartition + # an old-layout unit to the A/B + /data GPT - platform_do_upgrade_mono). python3 mono_gpt.py backup $@.bootdir/boot/backup-gpt.bin \ $(MONO_EMMC_SECTORS) $(MONO_BOOTFS_SIZE) $(MONO_ROOTFS_PART) + python3 mono_gpt.py primary $@.bootdir/boot/primary-gpt.bin \ + $(MONO_EMMC_SECTORS) $(MONO_BOOTFS_SIZE) $(MONO_ROOTFS_PART) truncate -s $(MONO_BOOTFS_SIZE)M $@.bootfs $(STAGING_DIR_HOST)/bin/mkfs.ext4 -F -L boot -d $@.bootdir $@.bootfs endef @@ -466,14 +470,17 @@ define Device/mono_gateway-dk block-mount kmod-usb-storage-uas kmod-fs-exfat kmod-fs-ntfs3 \ kmod-fs-vfat smartmontools usbutils pciutils i2c-tools \ tmux vim-full curl rsync jq less bind-dig openssh-sftp-server \ - usign ca-bundle file ip-full resize2fs mono-update luci-app-mono-update \ + usign ca-bundle file ip-full resize2fs e2fsprogs mono-update luci-app-mono-update \ cmmqos \ policycoreutils-setfiles policycoreutils-sestatus KERNEL_NAME := Image KERNEL := kernel-bin | gzip FILESYSTEMS := ext4 + # A/B + persistent-data eMMC layout (see mono_gpt.py): two 1 GiB rootfs slots + # (rootA=p2 keeps the pre-A/B start sector), two 64 MiB boot slots, and /data + # (p5) filling the rest (~27.5 GiB). MONO_ROOTFS_PART is the PER-SLOT size. MONO_BOOTFS_SIZE := 64 - MONO_ROOTFS_PART := 30208 + MONO_ROOTFS_PART := 1024 MONO_EMMC_SECTORS := 62160896 # Keep the -sdboot alias: units flashed before 02_sysinfo_fixup stopped # appending it still report mono,gateway-dk-sdboot at runtime, and sysupgrade diff --git a/target/linux/layerscape/image/gen_mono_emmc_img.sh b/target/linux/layerscape/image/gen_mono_emmc_img.sh index 233d9692aa..d70dfb4e54 100755 --- a/target/linux/layerscape/image/gen_mono_emmc_img.sh +++ b/target/linux/layerscape/image/gen_mono_emmc_img.sh @@ -4,9 +4,10 @@ # Mono Gateway eMMC image. Layout (see mono_gpt.py for the GPT rationale): # 0-4 KiB complete GPT (8-entry array), clear of the firmware region # 4 KiB-32 MiB boot firmware, owned by a separate update tool - untouched -# 32 MiB partition 1: boot (ext4: /boot/extlinux + Image.gz + dtb) -# +bootfs partition 2: rootfs, partition sized to the eMMC; the ext4 -# inside is smaller and grows on first boot +# 32 MiB p1 bootA (ext4: /boot/extlinux + Image.gz + dtb) +# +bootfs p2 rootA: slot-A rootfs; the ext4 inside is smaller and grows on +# first boot. p3 bootB / p4 rootB / p5 data follow in the GPT but are +# created on the device - this image only seeds bootA + rootA. # set -ex [ $# -eq 6 ] || { @@ -36,9 +37,10 @@ ROOTFSOFFSET=$(( (32 + BOOTFSSIZE) * 2048 )) dd bs=512 if="$BOOTFS" of="$OUTPUT" seek=${BOOTOFFSET} conv=notrunc dd bs=512 if="$ROOTFS" of="$OUTPUT" seek=${ROOTFSOFFSET} conv=notrunc -# Trim after the rootfs payload; the partition extends to end-of-device but -# the flashing procedure never writes that far (and the backup GPT is placed -# by first-boot, not the image). +# Trim after the rootA payload: this image carries only bootA + rootA. p3-p5 +# (bootB/rootB/data) live in the GPT but are created on the device at first +# boot, and the flashing procedure never writes past rootA. The backup GPT is +# placed by first-boot, not the image. ROOTFSBYTES=$(stat -c%s "$ROOTFS") truncate -s $(( ROOTFSOFFSET * 512 + ROOTFSBYTES )) "$OUTPUT" rm -f "$OUTPUT.rootfs" diff --git a/target/linux/layerscape/image/mono_gpt.py b/target/linux/layerscape/image/mono_gpt.py index 84177a58af..f51e8bddf6 100644 --- a/target/linux/layerscape/image/mono_gpt.py +++ b/target/linux/layerscape/image/mono_gpt.py @@ -3,9 +3,18 @@ # array) so the boot-firmware region (4 KiB-32 MiB, owned by a separate # update tool) stays clear; ptgen cannot emit a small array, hence this. # +# A/B layout (5 partitions, all within the 8-entry array): +# p1 bootA @ 32 MiB , boot_mb ext4: /boot/extlinux + Image.gz + dtb +# p2 rootA @ 32+boot_mb MiB , root_mb slot-A rootfs (== the pre-A/B rootfs start) +# p3 bootB @ after rootA , boot_mb slot-B boot (empty until an A/B update) +# p4 rootB @ after bootB , root_mb slot-B rootfs (empty until an A/B update) +# p5 data @ after rootB , fill persistent /data; survives sysupgrade +# Slot A keeps the pre-A/B offsets so an existing unit's rootfs stays put and the +# migration only shrinks p2 and adds p3-p5. root_mb is now the PER-SLOT rootfs size. +# # Two modes: -# primary -> protective MBR + header (LBA1) + array (LBA2-3), for the -# head of the flashable image. +# primary -> protective MBR + header (LBA1) + array (LBA2-3), for the head +# of the flashable image. # backup -> array + header tail (33 sectors) written at LBA N-33 of the # real device on first boot; makes the on-disk table complete # so partition tools do not see a half-corrupt GPT and try to @@ -16,29 +25,36 @@ def guid(s): return uuid.UUID(s).bytes_le LINUX_FS = guid("0FC63DAF-8483-4772-8E79-3D69D8477DE4") DISK_GUID = guid("6D6F6E6F-1046-4000-8000-4D6F6E6F0000") -PART_GUID = [guid("6D6F6E6F-1046-4000-8000-4D6F6E6F0001"), - guid("6D6F6E6F-1046-4000-8000-4D6F6E6F0002")] +# One stable partition GUID per partition p1..p5 (…0001 … …0005). +PART_GUID = [guid("6D6F6E6F-1046-4000-8000-4D6F6E6F000%d" % n) for n in range(1, 6)] SEC, ENTRIES, ENTRY_SZ = 512, 8, 128 +MiB = 2048 # 512-byte sectors per MiB + +def build(disk_sectors, boot_mb, root_mb): + first_usable = 32 * MiB # keep the 4 KiB-32 MiB firmware region out of range + last_usable = disk_sectors - 34 # 33-sector backup GPT tail lives above this -def build(disk_sectors, boot_mb, root_part_mb): - boot_first = 32 * 2048 - boot_last = boot_first + boot_mb * 2048 - 1 - root_first = boot_last + 1 - root_last = root_first + root_part_mb * 2048 - 1 - if root_last > disk_sectors - 34: - sys.exit("mono_gpt: rootfs partition (%d MiB) exceeds device (%d sectors)" - % (root_part_mb, disk_sectors)) + # (label, size in MiB); size 0 means "fill to last_usable" (the data partition). + specs = [("bootA", boot_mb), ("rootA", root_mb), + ("bootB", boot_mb), ("rootB", root_mb), ("data", 0)] + parts, cur = [], first_usable + for name, mb in specs: + first = cur + last = (first + mb * MiB - 1) if mb else last_usable + if first > last or last > last_usable: + sys.exit("mono_gpt: layout exceeds device (%d sectors): %s wants %d-%d, " + "last_usable %d" % (disk_sectors, name, first, last, last_usable)) + parts.append((name, first, last)) + cur = last + 1 def entry(pg, first, last, name): return (LINUX_FS + pg + struct.pack("