Switch from sanboot to kernel/initrd boot with PXE overlay for ISO download
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 <noreply@anthropic.com>
This commit is contained in:
+42
-44
@@ -7,47 +7,60 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const pxeInitScript = `#!/bin/sh
|
const pxeInitScript = `#!/bin/sh
|
||||||
mkdir -p /dev
|
mount -t proc proc /proc 2>/dev/null
|
||||||
mount -t devtmpfs devtmpfs /dev 2>/dev/null
|
mount -t sysfs sys /sys 2>/dev/null
|
||||||
losetup /dev/loop0 /proxmox.iso 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 /dev 2>/dev/null
|
||||||
|
umount /sys 2>/dev/null
|
||||||
|
umount /proc 2>/dev/null
|
||||||
exec /init "$@"
|
exec /init "$@"
|
||||||
`
|
`
|
||||||
|
|
||||||
func CreatePXEInitrd(origInitrdPath, isoPath, outputPath string) error {
|
func CreatePXEOverlay(outputPath string) error {
|
||||||
out, err := os.Create(outputPath)
|
out, err := os.Create(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create output: %w", err)
|
return fmt.Errorf("create overlay: %w", err)
|
||||||
}
|
}
|
||||||
defer out.Close()
|
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 {
|
if err := writeCPIOEntry(out, "pxe-init", []byte(pxeInitScript), 0o100755, 1); err != nil {
|
||||||
return err
|
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)
|
return writeCPIOTrailer(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,21 +79,6 @@ func writeCPIOEntry(w io.Writer, name string, data []byte, mode uint32, ino uint
|
|||||||
return nil
|
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 {
|
func writeCPIOHeader(w io.Writer, name string, fileSize int64, mode uint32, ino uint32) error {
|
||||||
nameLen := len(name) + 1
|
nameLen := len(name) + 1
|
||||||
hdr := fmt.Sprintf("070701%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X",
|
hdr := fmt.Sprintf("070701%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X%08X",
|
||||||
|
|||||||
@@ -55,12 +55,13 @@ func (s *Service) Upload(ctx context.Context, p UploadParams) (*model.Image, err
|
|||||||
return nil, fmt.Errorf("extract ISO: %w", err)
|
return nil, fmt.Errorf("extract ISO: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.PublicURL != "" {
|
report := func(stage, detail string) {
|
||||||
report := func(stage, detail string) {
|
if p.OnProgress != nil {
|
||||||
if p.OnProgress != nil {
|
p.OnProgress(stage, detail)
|
||||||
p.OnProgress(stage, detail)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.PublicURL != "" {
|
||||||
report("packaging", "Configuring ISO for automated installation...")
|
report("packaging", "Configuring ISO for automated installation...")
|
||||||
isoFile := filepath.Join(destDir, result.ISOFilename)
|
isoFile := filepath.Join(destDir, result.ISOFilename)
|
||||||
answerURL := s.PublicURL + "/api/boot/auto-answer"
|
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)
|
kernelPath := filepath.Join(p.Name, result.KernelFilename)
|
||||||
initrdPath := filepath.Join(p.Name, result.InitrdFilename)
|
initrdPath := filepath.Join(p.Name, result.InitrdFilename)
|
||||||
isoPath := filepath.Join(p.Name, result.ISOFilename)
|
isoPath := filepath.Join(p.Name, result.ISOFilename)
|
||||||
|
|||||||
+14
-4
@@ -2,16 +2,26 @@ package pxe
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"provisioning/internal/model"
|
"provisioning/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BuildIPXEScript(publicURL string, img *model.Image, mac string) string {
|
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
|
return fmt.Sprintf(`#!ipxe
|
||||||
echo Provisioning: booting %s on ${mac}
|
echo Provisioning: booting %s on ${mac}
|
||||||
echo Booting ISO...
|
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
|
||||||
sanboot %s
|
initrd %s
|
||||||
`, img.Name, isoURL)
|
initrd %s
|
||||||
|
boot
|
||||||
|
`, img.Name, kernelURL, answerURL, isoURL, initrdURL, overlayURL)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user