From 9a143bf7ffdbbbdcf79549d0428415721e104521 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 25 Apr 2026 11:07:31 +0000 Subject: [PATCH] x86: onie-installer: wire up sysupgrade via ONIE install mode This adds support for sysupgrade on ONIE-installed systems. The install is chained through ONIE (using the ONIE installer image), rather than attempting to manually upgrade the partition. The idea is to allow future OpenWRT installs flexibility to use a different partition table. By putting the installer in charge of setting up the file system partition, the upgrade process needs to have no knowledge of the internals of the image. Config preservation is accomplished by appending the sysupgrade .tar.gz to the ONIE installer image. Of course this also works for a clean install using a sysupgrade.tar.gz created via `sysupgrade -b`. Co-Authored-By: Claude Opus 4.7 Signed-off-by: Keno Fischer Link: https://github.com/openwrt/openwrt/pull/23062 Signed-off-by: Jonas Jelonek --- config/Config-images.in | 1 + .../x86/base-files/lib/upgrade/platform.sh | 102 +++++++++++++++++- target/linux/x86/image/Makefile | 5 +- target/linux/x86/image/onie-install.sh.in | 49 +++++++-- 4 files changed, 149 insertions(+), 8 deletions(-) diff --git a/config/Config-images.in b/config/Config-images.in index d683e9512e5..6b9901b65d0 100644 --- a/config/Config-images.in +++ b/config/Config-images.in @@ -313,6 +313,7 @@ menu "Target Images" bool "Build ONIE installer image (self-extracting .bin)" depends on TARGET_x86 depends on GRUB_EFI_IMAGES + select PACKAGE_grub2-editenv help Build a self-extracting ONIE installer .bin for installing OpenWrt on switches that ship with the Open Network Install diff --git a/target/linux/x86/base-files/lib/upgrade/platform.sh b/target/linux/x86/base-files/lib/upgrade/platform.sh index 5dad7a538a5..4d470699148 100644 --- a/target/linux/x86/base-files/lib/upgrade/platform.sh +++ b/target/linux/x86/base-files/lib/upgrade/platform.sh @@ -1,9 +1,101 @@ -RAMFS_COPY_BIN='grub-bios-setup' +RAMFS_COPY_BIN='grub-bios-setup grub-editenv' + +find_partname_dev() { + local partname="$1" + local uevent dev + for uevent in /sys/class/block/*/uevent; do + grep -q "^PARTNAME=$partname\$" "$uevent" 2>/dev/null || continue + dev=$(sed -n 's/^DEVNAME=//p' "$uevent") + [ -n "$dev" ] && { echo "/dev/$dev"; return 0; } + done + return 1 +} + +# True when the boot disk hosts ONIE: both ONIE-BOOT and OPENWRT-ROOT +# GPT-labelled partitions present. +is_onie_install() { + grep -q PARTNAME=ONIE-BOOT /sys/class/block/*/uevent 2>/dev/null || return 1 + grep -q PARTNAME=OPENWRT-ROOT /sys/class/block/*/uevent 2>/dev/null || return 1 + return 0 +} + +# Reinstall via ONIE: append the preserved-config tarball to the installer +# image (installer detects it via size check), drop the combined file on +# OPENWRT-ROOT as onie-installer-x86_64, and flip both grubenv files so the +# next boot chainloads ONIE in install mode. ONIE re-runs our installer, +# which recreates OPENWRT-ROOT, extracts the new rootfs, and stashes +# /sysupgrade.tgz for OpenWrt's preinit to restore. +platform_do_upgrade_onie() { + local image="$1" + local backup="$UPGRADE_BACKUP" + + local root_dev onie_dev + root_dev=$(find_partname_dev OPENWRT-ROOT) || { + v "ONIE upgrade: OPENWRT-ROOT not found"; return 1; + } + onie_dev=$(find_partname_dev ONIE-BOOT) || { + v "ONIE upgrade: ONIE-BOOT not found"; return 1; + } + + local root_mnt=/tmp/upgrade-root + local onie_mnt=/tmp/upgrade-onie + mkdir -p "$root_mnt" "$onie_mnt" + + mount -t ext4 -o rw,noatime "$root_dev" "$root_mnt" || { + v "ONIE upgrade: mount $root_dev failed" + return 1 + } + + local bundle="$root_mnt/onie-installer-x86_64" + v "ONIE upgrade: writing bundle to $bundle" + if [ -s "$backup" ]; then + cat "$image" "$backup" > "$bundle" + else + cp "$image" "$bundle" + fi + chmod +x "$bundle" + + # One-shot: OpenWrt's grub.cfg reads next_entry and chainloads ONIE. + grub-editenv "$root_mnt/boot/grub/grubenv" set next_entry=ONIE || { + v "ONIE upgrade: failed to set next_entry in OPENWRT-ROOT grubenv" + sync; umount "$root_mnt" + return 1 + } + sync + umount "$root_mnt" + + mount -o rw,noatime "$onie_dev" "$onie_mnt" || { + v "ONIE upgrade: mount $onie_dev failed" + return 1 + } + grub-editenv "$onie_mnt/grub/grubenv" set onie_mode=install || { + v "ONIE upgrade: failed to set onie_mode in ONIE-BOOT grubenv" + sync; umount "$onie_mnt" + return 1 + } + sync + umount "$onie_mnt" + + v "ONIE upgrade: ready; stage2 will reboot" + return 0 +} platform_check_image() { local diskdev partdev diff [ "$#" -gt 1 ] && return 1 + if is_onie_install; then + [ "$(get_magic_word "$1")" = "2321" ] || { + v "Invalid image: expected ONIE installer (shell script)" + return 1 + } + head -c 4096 "$1" | grep -q '^PAYLOAD_OFFSET=' || { + v "Invalid image: no PAYLOAD_OFFSET header" + return 1 + } + return 0 + fi + case "$(get_magic_word "$1")" in eb48|eb63) ;; *) @@ -39,6 +131,9 @@ platform_check_image() { platform_copy_config() { local partdev parttype=ext4 + # ONIE upgrade bundles sysupgrade.tgz into the installer itself. + is_onie_install && return 0 + if export_partdevice partdev 1; then part_magic_fat "/dev/$partdev" && parttype=vfat mount -t $parttype -o rw,noatime "/dev/$partdev" /mnt @@ -71,6 +166,11 @@ platform_do_bootloader_upgrade() { platform_do_upgrade() { local diskdev partdev diff + if is_onie_install; then + platform_do_upgrade_onie "$1" + return $? + fi + export_bootdevice && export_partdevice diskdev 0 || { v "Unable to determine upgrade device" return 1 diff --git a/target/linux/x86/image/Makefile b/target/linux/x86/image/Makefile index d99b9e6e350..87256c884d3 100644 --- a/target/linux/x86/image/Makefile +++ b/target/linux/x86/image/Makefile @@ -97,7 +97,10 @@ define Build/onie-installer -e 's#@TIMEOUT@#$(GRUB_TIMEOUT)#g' \ ./onie-install.sh.in > $@.onie/installer.sh script_len=$$(wc -c < $@.onie/installer.sh | tr -d ' '); \ - sed -i "s/__PYLOAD__/$$(printf '%010d' $$script_len)/" $@.onie/installer.sh + payload_len=$$(wc -c < $@.onie/payload.tar | tr -d ' '); \ + total_len=$$(( script_len + payload_len )); \ + sed -i "s/__PYLOAD__/$$(printf '%010d' $$script_len)/" $@.onie/installer.sh; \ + sed -i "s/__TOTLEN__/$$(printf '%010d' $$total_len)/" $@.onie/installer.sh cat $@.onie/installer.sh $@.onie/payload.tar > $@ chmod +x $@ endef diff --git a/target/linux/x86/image/onie-install.sh.in b/target/linux/x86/image/onie-install.sh.in index cb22f0de679..d79e7ac5a2d 100644 --- a/target/linux/x86/image/onie-install.sh.in +++ b/target/linux/x86/image/onie-install.sh.in @@ -14,9 +14,20 @@ set -e -# 10-char zero-padded byte offset of the tar payload. Length-preserving -# substitution: __PYLOAD__ (10 chars) -> "NNNNNNNNNN" at build time. +# 10-char zero-padded byte offsets, substituted length-preserving at build: +# __PYLOAD__ -> byte offset where payload.tar starts (== script size) +# __TOTLEN__ -> total file size at build time (script + payload) +# Sysupgrade appends the preserved-config tarball to the tail of the file; +# the installer detects it by comparing actual file size to BUILD_SIZE. PAYLOAD_OFFSET=__PYLOAD__ +BUILD_SIZE=__TOTLEN__ +# Strip leading zeros: shells (dash/ash) parse a leading 0 as octal, +# which breaks on digits >=8. Parameter expansion keeps us clear of `expr` +# (which returns exit 1 for "0" and trips `set -e`). +PAYLOAD_OFFSET=${PAYLOAD_OFFSET#${PAYLOAD_OFFSET%%[!0]*}} +BUILD_SIZE=${BUILD_SIZE#${BUILD_SIZE%%[!0]*}} +: "${PAYLOAD_OFFSET:=0}" +: "${BUILD_SIZE:=0}" # Substituted by image.mk: GRUB_CMDLINE='@CMDLINE@' @@ -69,7 +80,17 @@ openwrt_mnt=$(mktemp -d) trap 'cd /; umount "$openwrt_mnt" 2>/dev/null; rm -rf "$payload_dir" "$openwrt_mnt"' EXIT echo "Extracting installer payload..." -dd if="$0" bs="$PAYLOAD_OFFSET" skip=1 2>/dev/null | tar -x -C "$payload_dir" +actual_size=$(wc -c < "$0") +if [ "$actual_size" -gt "$BUILD_SIZE" ]; then + # Sysupgrade has appended sysupgrade.tgz past the build-time tail. + payload_size=$(( BUILD_SIZE - PAYLOAD_OFFSET )) + tail -c "+$(( PAYLOAD_OFFSET + 1 ))" "$0" | head -c "$payload_size" \ + | tar -x -C "$payload_dir" + tail -c "+$(( BUILD_SIZE + 1 ))" "$0" > "$payload_dir/sysupgrade.tgz" + echo "Preserved config : $(wc -c < "$payload_dir/sysupgrade.tgz") bytes" +else + dd if="$0" bs="$PAYLOAD_OFFSET" skip=1 2>/dev/null | tar -x -C "$payload_dir" +fi [ -f "$payload_dir/rootfs.tar.gz" ] || { echo "ERROR: rootfs.tar.gz missing" >&2; exit 1; } [ -f "$payload_dir/vmlinuz" ] || { echo "ERROR: vmlinuz missing" >&2; exit 1; } @@ -103,6 +124,13 @@ echo "Installing kernel..." mkdir -p "$openwrt_mnt/boot" cp "$payload_dir/vmlinuz" "$openwrt_mnt/boot/vmlinuz" +# Stash preserved config at rootfs root; OpenWrt's preinit +# (80_mount_root) extracts it on first boot and /etc/init.d/done +# removes it. +if [ -f "$payload_dir/sysupgrade.tgz" ]; then + cp "$payload_dir/sysupgrade.tgz" "$openwrt_mnt/sysupgrade.tgz" +fi + # ------------------------------------------------ install GRUB to ESP --- if [ "$firmware" = "uefi" ]; then uefi_part=0 @@ -153,12 +181,21 @@ cat > "$openwrt_mnt/boot/grub/grub.cfg" <