From b1eab33b78fdb32991a559f8eaeff77fcae116b2 Mon Sep 17 00:00:00 2001 From: josh Date: Thu, 14 May 2026 18:57:43 -0400 Subject: [PATCH] Switch from sanboot to kernel/initrd boot with PXE overlay for ISO download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sanboot makes the ISO visible to UEFI but invisible to the Linux kernel after ExitBootServices(). Switch to direct kernel/initrd boot with a small CPIO overlay containing /pxe-init — a shell script that loads NIC drivers, configures DHCP, downloads the ISO via wget, and creates a loop device before handing off to the Proxmox installer init. Co-Authored-By: Claude Opus 4.6 --- internal/image/cpio.go | 86 +++++++++++++++++++-------------------- internal/image/service.go | 17 +++++--- internal/pxe/ipxe.go | 18 ++++++-- 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/internal/image/cpio.go b/internal/image/cpio.go index 2fb8b42..87ac709 100644 --- a/internal/image/cpio.go +++ b/internal/image/cpio.go @@ -7,47 +7,60 @@ import ( ) const pxeInitScript = `#!/bin/sh -mkdir -p /dev -mount -t devtmpfs devtmpfs /dev 2>/dev/null -losetup /dev/loop0 /proxmox.iso 2>/dev/null +mount -t proc proc /proc 2>/dev/null +mount -t sysfs sys /sys 2>/dev/null +mount -t devtmpfs dev /dev 2>/dev/null +mkdir -p /tmp + +ISO_URL="" +for param in $(cat /proc/cmdline); do + case $param in + prov.iso=*) ISO_URL="${param#prov.iso=}" ;; + esac +done + +if [ -n "$ISO_URL" ]; then + echo "=== PXE: Setting up network ===" + for mod in e1000 e1000e igb ixgbe i40e ice tg3 bnxt_en r8169 virtio_net; do + modprobe $mod 2>/dev/null + done + sleep 2 + + for iface in $(ls /sys/class/net/ 2>/dev/null | grep -v lo); do + ip link set "$iface" up 2>/dev/null + if udhcpc -i "$iface" -n -q -t 5 2>/dev/null; then + echo "PXE: Network up on $iface" + break + fi + done + + echo "=== PXE: Downloading ISO ===" + wget -O /tmp/proxmox.iso "$ISO_URL" 2>&1 + + if [ -s /tmp/proxmox.iso ]; then + losetup /dev/loop0 /tmp/proxmox.iso + echo "PXE: ISO on /dev/loop0" + else + echo "PXE: ERROR - download failed" + fi +fi + umount /dev 2>/dev/null +umount /sys 2>/dev/null +umount /proc 2>/dev/null exec /init "$@" ` -func CreatePXEInitrd(origInitrdPath, isoPath, outputPath string) error { +func CreatePXEOverlay(outputPath string) error { out, err := os.Create(outputPath) if err != nil { - return fmt.Errorf("create output: %w", err) + return fmt.Errorf("create overlay: %w", err) } defer out.Close() - orig, err := os.Open(origInitrdPath) - if err != nil { - return fmt.Errorf("open initrd: %w", err) - } - if _, err := io.Copy(out, orig); err != nil { - orig.Close() - return fmt.Errorf("copy initrd: %w", err) - } - orig.Close() - if err := writeCPIOEntry(out, "pxe-init", []byte(pxeInitScript), 0o100755, 1); err != nil { return err } - - isoStat, err := os.Stat(isoPath) - if err != nil { - return fmt.Errorf("stat iso: %w", err) - } - isoFile, err := os.Open(isoPath) - if err != nil { - return fmt.Errorf("open iso: %w", err) - } - defer isoFile.Close() - if err := writeCPIOEntryFromReader(out, "proxmox.iso", isoFile, isoStat.Size(), 0o100644, 2); err != nil { - return err - } - return writeCPIOTrailer(out) } @@ -66,21 +79,6 @@ func writeCPIOEntry(w io.Writer, name string, data []byte, mode uint32, ino uint return nil } -func writeCPIOEntryFromReader(w io.Writer, name string, r io.Reader, size int64, mode uint32, ino uint32) error { - if err := writeCPIOHeader(w, name, size, mode, ino); err != nil { - return err - } - if _, err := io.Copy(w, r); err != nil { - return err - } - if pad := (4 - int(size%4)) % 4; pad > 0 { - if _, err := w.Write(make([]byte, pad)); err != nil { - return err - } - } - return nil -} - func writeCPIOHeader(w io.Writer, name string, fileSize int64, mode uint32, ino uint32) error { nameLen := len(name) + 1 hdr := fmt.Sprintf("070701%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X", diff --git a/internal/image/service.go b/internal/image/service.go index c216033..5fd45b2 100644 --- a/internal/image/service.go +++ b/internal/image/service.go @@ -55,12 +55,13 @@ func (s *Service) Upload(ctx context.Context, p UploadParams) (*model.Image, err return nil, fmt.Errorf("extract ISO: %w", err) } - if s.PublicURL != "" { - report := func(stage, detail string) { - if p.OnProgress != nil { - p.OnProgress(stage, detail) - } + report := func(stage, detail string) { + if p.OnProgress != nil { + p.OnProgress(stage, detail) } + } + + if s.PublicURL != "" { report("packaging", "Configuring ISO for automated installation...") isoFile := filepath.Join(destDir, result.ISOFilename) answerURL := s.PublicURL + "/api/boot/auto-answer" @@ -69,6 +70,12 @@ func (s *Service) Upload(ctx context.Context, p UploadParams) (*model.Image, err } } + report("packaging", "Creating PXE boot overlay...") + overlayPath := filepath.Join(destDir, "pxe-overlay.img") + if err := CreatePXEOverlay(overlayPath); err != nil { + log.Printf("image: PXE overlay creation failed (non-fatal): %v", err) + } + kernelPath := filepath.Join(p.Name, result.KernelFilename) initrdPath := filepath.Join(p.Name, result.InitrdFilename) isoPath := filepath.Join(p.Name, result.ISOFilename) diff --git a/internal/pxe/ipxe.go b/internal/pxe/ipxe.go index 723b8b0..e155d50 100644 --- a/internal/pxe/ipxe.go +++ b/internal/pxe/ipxe.go @@ -2,16 +2,26 @@ package pxe import ( "fmt" + "path/filepath" + "strings" "provisioning/internal/model" ) func BuildIPXEScript(publicURL string, img *model.Image, mac string) string { - isoURL := fmt.Sprintf("%s/images/boot/%s", publicURL, img.ISOPath) + base := fmt.Sprintf("%s/images/boot", publicURL) + kernelURL := fmt.Sprintf("%s/%s", base, img.KernelPath) + initrdURL := fmt.Sprintf("%s/%s", base, img.InitrdPath) + isoURL := fmt.Sprintf("%s/%s", base, img.ISOPath) + overlayURL := fmt.Sprintf("%s/%s/pxe-overlay.img", base, + strings.Split(filepath.ToSlash(img.KernelPath), "/")[0]) + answerURL := fmt.Sprintf("%s/api/boot/auto-answer", publicURL) return fmt.Sprintf(`#!ipxe echo Provisioning: booting %s on ${mac} -echo Booting ISO... -sanboot %s -`, img.Name, isoURL) +kernel %s ro ramdisk_size=16777216 rw quiet splash=silent proxmox-start-auto-installer proxmox-auto-installer-answer-url=%s prov.iso=%s rdinit=/pxe-init +initrd %s +initrd %s +boot +`, img.Name, kernelURL, answerURL, isoURL, initrdURL, overlayURL) }