Add automated PXE installation via ISO GRUB modification and auto-answer endpoint
Modifies uploaded ISO's GRUB config in-place to set timeout=0 and inject proxmox-start-auto-installer + answer-url kernel params, enabling fully hands-off installation. Adds /api/boot/auto-answer endpoint that identifies hosts by ARP-resolving the requester's IP to MAC address. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"provisioning/internal/config"
|
||||
@@ -169,6 +172,68 @@ func (a *BootAPI) PhoneHome(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func (a *BootAPI) AutoAnswer(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
if clientIP == "" {
|
||||
clientIP = r.RemoteAddr
|
||||
}
|
||||
|
||||
mac := macFromARP(clientIP)
|
||||
if mac == "" {
|
||||
log.Printf("auto-answer: no ARP entry for %s", clientIP)
|
||||
http.Error(w, "could not determine MAC for your IP", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
host, err := a.Hosts.GetByMAC(r.Context(), mac)
|
||||
if err != nil {
|
||||
log.Printf("auto-answer: MAC %s (IP %s) not registered", mac, clientIP)
|
||||
http.Error(w, "unknown host", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
st, ok := a.ServerTypes.Get(host.ServerType)
|
||||
if !ok {
|
||||
http.Error(w, "unknown server type", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if host.State == model.StatePXEBooted || host.State == model.StatePXEReady {
|
||||
a.Runner.Transition(r.Context(), host.ID, statemachine.TriggerPXEScriptServed)
|
||||
a.Runner.Transition(r.Context(), host.ID, statemachine.TriggerAnswerServed)
|
||||
a.Runner.LogActivity(r.Context(), host.ID, model.LogInfo, "pxe", "Auto-installer answer file served")
|
||||
}
|
||||
|
||||
_, pubKey, _ := a.Hosts.GetEphemeralKey(r.Context(), host.ID)
|
||||
if pubKey == "" {
|
||||
http.Error(w, "no ephemeral key for host", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("auto-answer: serving answer for %s (%s) from IP %s", host.Hostname, mac, clientIP)
|
||||
answer := pxe.GenerateAnswerFile(host, st, a.Config, pubKey)
|
||||
w.Header().Set("Content-Type", "application/toml")
|
||||
w.Write([]byte(answer))
|
||||
}
|
||||
|
||||
func macFromARP(ip string) string {
|
||||
f, err := os.Open("/proc/net/arp")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Scan() // skip header
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) >= 4 && fields[0] == ip && fields[3] != "00:00:00:00:00:00" {
|
||||
return strings.ToLower(fields[3])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeMAC(m string) string {
|
||||
m = strings.ToLower(strings.TrimSpace(m))
|
||||
m = strings.ReplaceAll(m, "-", ":")
|
||||
|
||||
Reference in New Issue
Block a user