Embed ISO in initrd for PXE boot with loop-mount wrapper
Instead of sanboot (which can't pass kernel params for automation), switch back to kernel/initrd boot. The ISO is embedded in the initrd as a CPIO append. A pxe-init wrapper script loop-mounts the ISO before handing off to the original init, so the installer finds it as a block device. Uses rdinit=/pxe-init kernel parameter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
package image
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pxeInitScript = `#!/bin/sh
|
||||||
|
mkdir -p /dev
|
||||||
|
mount -t devtmpfs devtmpfs /dev 2>/dev/null
|
||||||
|
losetup /dev/loop0 /proxmox.iso 2>/dev/null
|
||||||
|
umount /dev 2>/dev/null
|
||||||
|
exec /init "$@"
|
||||||
|
`
|
||||||
|
|
||||||
|
func CreatePXEInitrd(origInitrdPath, isoPath, outputPath string) error {
|
||||||
|
out, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create output: %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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCPIOEntry(w io.Writer, name string, data []byte, mode uint32, ino uint32) error {
|
||||||
|
if err := writeCPIOHeader(w, name, int64(len(data)), mode, ino); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pad := (4 - len(data)%4) % 4; pad > 0 {
|
||||||
|
if _, err := w.Write(make([]byte, pad)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
ino, // ino
|
||||||
|
mode, // mode
|
||||||
|
0, // uid
|
||||||
|
0, // gid
|
||||||
|
1, // nlink
|
||||||
|
0, // mtime
|
||||||
|
fileSize, // filesize
|
||||||
|
0, // devmajor
|
||||||
|
0, // devminor
|
||||||
|
0, // rdevmajor
|
||||||
|
0, // rdevminor
|
||||||
|
nameLen, // namesize
|
||||||
|
0, // check
|
||||||
|
)
|
||||||
|
if _, err := io.WriteString(w, hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte{0}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
totalHeader := 110 + nameLen
|
||||||
|
if pad := (4 - totalHeader%4) % 4; pad > 0 {
|
||||||
|
if _, err := w.Write(make([]byte, pad)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCPIOTrailer(w io.Writer) error {
|
||||||
|
return writeCPIOHeader(w, "TRAILER!!!", 0, 0, 0)
|
||||||
|
}
|
||||||
@@ -53,8 +53,24 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
report := func(stage, detail string) {
|
||||||
|
if p.OnProgress != nil {
|
||||||
|
p.OnProgress(stage, detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report("packaging", "Creating PXE boot image (embedding ISO in initrd)...")
|
||||||
|
|
||||||
|
origInitrdPath := filepath.Join(destDir, result.InitrdFilename)
|
||||||
|
origISOPath := filepath.Join(destDir, result.ISOFilename)
|
||||||
|
pxeInitrdFile := "initrd-pxe.img"
|
||||||
|
pxeInitrdPath := filepath.Join(destDir, pxeInitrdFile)
|
||||||
|
if err := CreatePXEInitrd(origInitrdPath, origISOPath, pxeInitrdPath); err != nil {
|
||||||
|
os.RemoveAll(destDir)
|
||||||
|
return nil, fmt.Errorf("create PXE initrd: %w", 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, pxeInitrdFile)
|
||||||
isoPath := filepath.Join(p.Name, result.ISOFilename)
|
isoPath := filepath.Join(p.Name, result.ISOFilename)
|
||||||
|
|
||||||
id, err := s.Store.Create(ctx, model.Image{
|
id, err := s.Store.Create(ctx, model.Image{
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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)
|
kernelURL := fmt.Sprintf("%s/images/boot/%s", publicURL, img.KernelPath)
|
||||||
|
initrdURL := fmt.Sprintf("%s/images/boot/%s", publicURL, img.InitrdPath)
|
||||||
|
|
||||||
return fmt.Sprintf(`#!ipxe
|
return fmt.Sprintf(`#!ipxe
|
||||||
echo Provisioning: booting %s on ${mac}
|
echo Provisioning: booting %s on ${mac}
|
||||||
echo Booting ISO via HTTP SAN...
|
echo Loading kernel...
|
||||||
sanboot %s
|
kernel %s ramdisk_size=16777216 vga=791 video=vesafb:lfb:on rw quiet splash=verbose rdinit=/pxe-init initrd=initrd
|
||||||
`, img.Name, isoURL)
|
echo Loading initrd (this may take a few minutes)...
|
||||||
|
initrd --name initrd %s
|
||||||
|
echo Booting...
|
||||||
|
boot
|
||||||
|
`, img.Name, kernelURL, initrdURL)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user