From c7e8cffcb273e89336517cb6549a245bcf781e3c Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Wed, 22 Apr 2026 14:10:37 +0000 Subject: [PATCH] x86: add onie-installer image type 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 Signed-off-by: Keno Fischer Link: https://github.com/openwrt/openwrt/pull/23062 Signed-off-by: Jonas Jelonek --- config/Config-images.in | 15 ++ target/linux/x86/image/Makefile | 25 +++ target/linux/x86/image/onie-install.sh.in | 190 ++++++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 target/linux/x86/image/onie-install.sh.in diff --git a/config/Config-images.in b/config/Config-images.in index fcc5fa52cb..d683e9512e 100644 --- a/config/Config-images.in +++ b/config/Config-images.in @@ -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 diff --git a/target/linux/x86/image/Makefile b/target/linux/x86/image/Makefile index 29bebeb748..d99b9e6e35 100644 --- a/target/linux/x86/image/Makefile +++ b/target/linux/x86/image/Makefile @@ -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 index 0000000000..cb22f0de67 --- /dev/null +++ b/target/linux/x86/image/onie-install.sh.in @@ -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" <> "$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