]> git.ipfire.org Git - thirdparty/openwrt.git/commitdiff
x86: add onie-installer image type
authorKeno Fischer <keno@juliahub.com>
Wed, 22 Apr 2026 14:10:37 +0000 (14:10 +0000)
committerJonas Jelonek <jelonek.jonas@gmail.com>
Sat, 23 May 2026 15:23:53 +0000 (17:23 +0200)
The current documentation for using OpenWRT on Mellanox Spectrum
switches (https://openwrt.org/toh/mellanox/spectrum) suggests
reflashing the entire harddrive from the recovery USB. This is not
the most friendly way to install a new OS on these switches. From
factory, they come with ONIE (Open Network Install Environment),
which is a linux-based preboot environment for fetching an OS
image from the network and installing it on disk. The installer
is a self-executing bash script that executes inside the ONIE
environment. The installer is expected to preserve the ONIE partition
for use as recovery environement. To be a better citizen on
these platforms, it would be preferrable to provide OpenWRT as
an ONIE-compatible installer.

This PR adds an ONIE_INSTALLER_IMAGES build option that produces
an ONIE compatible .bin. The generated .bin follows the ONIE demo
installer pattern [1]: it creates a new GPT partition
labelled OPENWRT-ROOT on the ONIE install device, formats ext4, extracts
the OpenWrt rootfs and kernel into it, installs GRUB into the existing
UEFI ESP under bootloader-id "OpenWrt", and adds a NVRAM boot entry via
efibootmgr.  ONIE-BOOT is preserved so ONIE rescue remains available.

Tested with the config at [2] on a Mellanox Spectrum SN3800 to produce
a booting OpenWRT install.

[1] https://github.com/opencomputeproject/onie/demo/installer/grub-arch/install.sh
[2] https://gist.github.com/Keno/abc8c5b72645e73fadd1ff0d9616b23d

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Keno Fischer <keno@juliahub.com>
Link: https://github.com/openwrt/openwrt/pull/23062
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
config/Config-images.in
target/linux/x86/image/Makefile
target/linux/x86/image/onie-install.sh.in [new file with mode: 0644]

index fcc5fa52cbf0c0a95a1cdbea1c8d41d51fd1dc96..d683e9512e542b3da8b4f872b397f228dc1ee750 100644 (file)
@@ -309,6 +309,21 @@ menu "Target Images"
                depends on GRUB_IMAGES || GRUB_EFI_IMAGES
                select PACKAGE_kmod-e1000
 
+       config ONIE_INSTALLER_IMAGES
+               bool "Build ONIE installer image (self-extracting .bin)"
+               depends on TARGET_x86
+               depends on GRUB_EFI_IMAGES
+               help
+                 Build a self-extracting ONIE installer .bin for installing
+                 OpenWrt on switches that ship with the Open Network Install
+                 Environment (e.g. Mellanox/NVIDIA Spectrum-based SN series).
+
+                 The resulting .bin wraps a tar payload (rootfs + kernel) in a
+                 shell script that follows the ONIE demo installer pattern:
+                 a new GPT partition labelled OPENWRT-ROOT is created for
+                 OpenWrt, and the existing ONIE-BOOT partition is preserved so
+                 ONIE rescue remains bootable.
+
        config TARGET_SERIAL
                string "Serial port device"
                depends on TARGET_x86 || TARGET_armsr || TARGET_loongarch64
index 29bebeb7489019df44e956119451614e6f910962..d99b9e6e350baf07abbdc47b5751f6d9325e4e24 100644 (file)
@@ -79,6 +79,29 @@ define Build/grub-install
                $@
 endef
 
+define Build/onie-installer
+       rm -rf $@.onie
+       mkdir -p $@.onie/payload
+       $(TAR) --numeric-owner --owner=0 --group=0 --sort=name \
+               $(if $(SOURCE_DATE_EPOCH),--mtime=@$(SOURCE_DATE_EPOCH)) \
+               -C $(TARGET_DIR)/ -cf - . | gzip -9n > $@.onie/payload/rootfs.tar.gz
+       $(CP) $(KDIR)/$(KERNEL_NAME) $@.onie/payload/vmlinuz
+       $(TAR) --numeric-owner --owner=0 --group=0 --sort=name \
+               $(if $(SOURCE_DATE_EPOCH),--mtime=@$(SOURCE_DATE_EPOCH)) \
+               -C $@.onie/payload -cf $@.onie/payload.tar .
+       sed \
+               -e 's#@SERIAL_CONFIG@#$(strip $(GRUB_SERIAL_CONFIG))#g' \
+               -e 's#@TERMINAL_CONFIG@#$(strip $(GRUB_TERMINAL_CONFIG))#g' \
+               -e 's#@CMDLINE@#$(strip $(BOOTOPTS) $(GRUB_CONSOLE_CMDLINE))#g' \
+               -e 's#@TITLE@#$(GRUB_TITLE)#g' \
+               -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
+       cat $@.onie/installer.sh $@.onie/payload.tar > $@
+       chmod +x $@
+endef
+
 define Build/iso
        $(CP) $(KDIR)/$(KERNEL_NAME) $@.boot/boot/vmlinuz
        cat \
@@ -114,6 +137,7 @@ define Device/Default
   IMAGE/combined-efi.vdi := grub-config efi | combined efi | grub-install efi | qemu-image vdi
   IMAGE/combined-efi.vmdk := grub-config efi | combined efi | grub-install efi | qemu-image vmdk
   IMAGE/combined-efi.vhdx := grub-config efi | combined efi | grub-install efi | qemu-image vhdx -o subformat=dynamic
+  ARTIFACT/onie-installer.bin := onie-installer
   ifeq ($(CONFIG_TARGET_IMAGES_GZIP),y)
     IMAGES-y := rootfs.img.gz
     IMAGES-$$(CONFIG_GRUB_IMAGES) += combined.img.gz
@@ -130,6 +154,7 @@ define Device/Default
     ARTIFACTS-$$(CONFIG_GRUB_IMAGES) += image.iso
     ARTIFACTS-$$(CONFIG_GRUB_EFI_IMAGES) += image-efi.iso
   endif
+  ARTIFACTS-$$(CONFIG_ONIE_INSTALLER_IMAGES) += onie-installer.bin
   ifeq ($(CONFIG_VDI_IMAGES),y)
     IMAGES-$$(CONFIG_GRUB_IMAGES) += combined.vdi
     IMAGES-$$(CONFIG_GRUB_EFI_IMAGES) += combined-efi.vdi
diff --git a/target/linux/x86/image/onie-install.sh.in b/target/linux/x86/image/onie-install.sh.in
new file mode 100644 (file)
index 0000000..cb22f0d
--- /dev/null
@@ -0,0 +1,190 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# OpenWrt ONIE installer -- self-extracting shell archive.
+# Wrapped around a tar payload that contains:
+#     rootfs.tar.gz   -- OpenWrt root tree
+#     vmlinuz         -- OpenWrt kernel
+# Appended at PAYLOAD_OFFSET (filled in by image.mk at build time).
+#
+# Follows the ONIE demo installer pattern:
+#   https://github.com/opencomputeproject/onie/blob/master/demo/installer/grub-arch/install.sh
+# The existing ESP and ONIE-BOOT partition are preserved; a new GPT
+# partition labelled OPENWRT-ROOT is created for OpenWrt.
+
+set -e
+
+# 10-char zero-padded byte offset of the tar payload. Length-preserving
+# substitution: __PYLOAD__ (10 chars) -> "NNNNNNNNNN" at build time.
+PAYLOAD_OFFSET=__PYLOAD__
+
+# Substituted by image.mk:
+GRUB_CMDLINE='@CMDLINE@'
+GRUB_SERIAL_CONFIG='@SERIAL_CONFIG@'
+GRUB_TERMINAL_CONFIG='@TERMINAL_CONFIG@'
+GRUB_TITLE='@TITLE@'
+GRUB_TIMEOUT='@TIMEOUT@'
+
+OPENWRT_VOLUME_LABEL="OPENWRT-ROOT"
+OPENWRT_PART_SIZE=512   # MiB -- OpenWrt x86 default is 104; 512 gives headroom
+
+echo ""
+echo "=========================================================="
+echo " OpenWrt ONIE installer"
+echo " $GRUB_TITLE"
+echo "=========================================================="
+echo ""
+
+cd "$(dirname "$0")"
+
+lib_dir="/lib/onie"
+# shellcheck disable=SC1090,SC1091
+[ -r "$lib_dir/onie-blkdev-common" ] && . "$lib_dir/onie-blkdev-common"
+[ -r /etc/machine.conf ] && . /etc/machine.conf
+
+# Install on the same disk that holds ONIE-BOOT (demo-installer idiom)
+blk_dev=$(blkid | awk -F: '/ONIE-BOOT/ {print $1; exit}' \
+            | sed -e 's/[0-9]*$//' -e 's/p$//')
+[ -b "$blk_dev" ] || {
+    echo "ERROR: unable to determine ONIE install block device" >&2
+    exit 1
+}
+echo "Install device    : $blk_dev"
+
+if [ -d /sys/firmware/efi/efivars ]; then
+    firmware="uefi"
+else
+    firmware="bios"
+fi
+echo "Firmware mode     : $firmware"
+
+blk_suffix=
+case "$blk_dev" in
+    *mmcblk*|*nvme*) blk_suffix="p" ;;
+esac
+
+# ------------------------------------------------ extract the payload ---
+payload_dir=$(mktemp -d)
+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"
+[ -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; }
+
+# --------------------------------------------- create target partition ---
+prev_part=$(sgdisk -p "$blk_dev" | awk -v lbl="$OPENWRT_VOLUME_LABEL" '$NF==lbl {print $1}')
+if [ -n "$prev_part" ]; then
+    echo "Removing previous $OPENWRT_VOLUME_LABEL partition (#$prev_part)..."
+    sgdisk -d "$prev_part" "$blk_dev" >/dev/null
+    partprobe "$blk_dev" || true
+fi
+
+last_part=$(sgdisk -p "$blk_dev" | awk '/^ *[0-9]+ /{p=$1} END{print p+0}')
+openwrt_part=$(( last_part + 1 ))
+echo "Creating ${blk_dev}${blk_suffix}${openwrt_part} (${OPENWRT_PART_SIZE} MiB, label $OPENWRT_VOLUME_LABEL)..."
+sgdisk --new=${openwrt_part}::+${OPENWRT_PART_SIZE}M \
+       --attributes=${openwrt_part}:=:0x0 \
+       --change-name=${openwrt_part}:"$OPENWRT_VOLUME_LABEL" \
+       "$blk_dev" >/dev/null
+partprobe "$blk_dev" || blockdev --rereadpt "$blk_dev" || true
+sleep 1
+
+openwrt_dev="${blk_dev}${blk_suffix}${openwrt_part}"
+mkfs.ext4 -F -q -L "$OPENWRT_VOLUME_LABEL" "$openwrt_dev"
+mount -t ext4 -o defaults,rw "$openwrt_dev" "$openwrt_mnt"
+
+# ----------------------------------------------- populate the rootfs ---
+echo "Extracting OpenWrt root filesystem..."
+tar -xzf "$payload_dir/rootfs.tar.gz" -C "$openwrt_mnt"
+
+echo "Installing kernel..."
+mkdir -p "$openwrt_mnt/boot"
+cp "$payload_dir/vmlinuz" "$openwrt_mnt/boot/vmlinuz"
+
+# ------------------------------------------------ install GRUB to ESP ---
+if [ "$firmware" = "uefi" ]; then
+    uefi_part=0
+    for p in $(seq 1 16); do
+        if sgdisk -i "$p" "$blk_dev" 2>/dev/null \
+                | grep -q C12A7328-F81F-11D2-BA4B-00A0C93EC93B; then
+            uefi_part=$p; break
+        fi
+    done
+    [ "$uefi_part" -ne 0 ] || { echo "ERROR: cannot find UEFI ESP on $blk_dev" >&2; exit 1; }
+    echo "Reusing ESP       : ${blk_dev}${blk_suffix}${uefi_part}"
+
+    echo "Installing GRUB (x86_64-efi)..."
+    grub-install \
+        --no-nvram \
+        --bootloader-id="OpenWrt" \
+        --efi-directory="/boot/efi" \
+        --boot-directory="$openwrt_mnt/boot" \
+        --recheck "$blk_dev" >/dev/null
+
+    for b in $(efibootmgr | awk '/OpenWrt/ {gsub("Boot",""); gsub("\\*",""); print $1}'); do
+        efibootmgr -b "$b" -B >/dev/null 2>&1 || true
+    done
+    efibootmgr --quiet --create \
+        --label "OpenWrt" \
+        --disk  "$blk_dev" --part "$uefi_part" \
+        --loader "/EFI/OpenWrt/grubx64.efi"
+else
+    echo "Installing GRUB (i386-pc, BIOS)..."
+    grub-install \
+        --target="i386-pc" \
+        --boot-directory="$openwrt_mnt/boot" \
+        --recheck "$blk_dev" >/dev/null
+fi
+
+# ---------------------------------------------- write target grub.cfg ---
+# The kernel built-in root parser handles root=PARTUUID=... but not
+# root=LABEL=... -- query the real PARTUUID and bake it in. We read it
+# from the GPT via sgdisk since busybox blkid in ONIE doesn't support
+# `-s PARTUUID -o value` and returns the device path instead.
+rootpart_partuuid=$(sgdisk -i "$openwrt_part" "$blk_dev" \
+    | awk '/Partition unique GUID:/ {print tolower($NF)}')
+[ -n "$rootpart_partuuid" ] || { echo "ERROR: cannot read PARTUUID of $openwrt_dev" >&2; exit 1; }
+echo "Root PARTUUID     : $rootpart_partuuid"
+
+mkdir -p "$openwrt_mnt/boot/grub"
+cat > "$openwrt_mnt/boot/grub/grub.cfg" <<EOF
+$GRUB_SERIAL_CONFIG
+$GRUB_TERMINAL_CONFIG
+
+set default="0"
+set timeout="$GRUB_TIMEOUT"
+
+# GRUB finds this filesystem by ext4 label (set by mkfs.ext4 -L).
+search --no-floppy --label --set=root $OPENWRT_VOLUME_LABEL
+
+menuentry "$GRUB_TITLE" {
+    linux /boot/vmlinuz root=PARTUUID=$rootpart_partuuid rootwait $GRUB_CMDLINE noinitrd
+}
+menuentry "$GRUB_TITLE (failsafe mode)" {
+    linux /boot/vmlinuz failsafe=true root=PARTUUID=$rootpart_partuuid rootwait $GRUB_CMDLINE noinitrd
+}
+EOF
+
+onie_grub_frag="${onie_root_dir:-/mnt/onie-boot/onie}/grub.d/50_onie_grub"
+if [ -x "$onie_grub_frag" ]; then
+    echo "Appending ONIE boot entry to grub.cfg..."
+    "$onie_grub_frag" >> "$openwrt_mnt/boot/grub/grub.cfg" 2>/dev/null || true
+fi
+
+# --------------------------------------------------- finish and exit ---
+sync
+umount "$openwrt_mnt"
+trap - EXIT
+rm -rf "$payload_dir" "$openwrt_mnt"
+
+if [ -x /bin/onie-nos-mode ]; then
+    /bin/onie-nos-mode -s
+fi
+
+echo ""
+echo "=========================================================="
+echo " OpenWrt installed. ONIE will reboot into it now."
+echo "=========================================================="
+exit 0