Initial implementation: host lifecycle + PXE + admin dashboard
Go service for Proxmox homelab cluster provisioning. Handles PXE boot, Proxmox autoinstall (answer file generation), cluster join via SSH, and Infrastructure API registration. - Host state machine (registered → pxe_ready → installing → ready) - dnsmasq supervisor with MAC-based allowlist - iPXE script and Proxmox answer file generation - First-boot phone-home → cluster join → infra registration - Operation locking with expiry (409 on conflict) - SSE event hub for real-time dashboard updates - Admin dashboard (host grid, detail, registration form) - Config-driven server types with hot-reload - Docker deployment (multi-stage fat image) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package statemachine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"provisioning/internal/model"
|
||||
)
|
||||
|
||||
type Trigger string
|
||||
|
||||
const (
|
||||
TriggerRebuildRequested Trigger = "RebuildRequested"
|
||||
TriggerPXEScriptServed Trigger = "PXEScriptServed"
|
||||
TriggerAnswerServed Trigger = "AnswerServed"
|
||||
TriggerInstallWebhook Trigger = "InstallWebhook"
|
||||
TriggerPhoneHome Trigger = "PhoneHome"
|
||||
TriggerClusterJoinStart Trigger = "ClusterJoinStarted"
|
||||
TriggerJoinComplete Trigger = "JoinComplete"
|
||||
TriggerFailed Trigger = "Failed"
|
||||
)
|
||||
|
||||
type transition struct {
|
||||
from []model.HostState
|
||||
to model.HostState
|
||||
}
|
||||
|
||||
var allActiveStates = []model.HostState{
|
||||
model.StatePXEReady,
|
||||
model.StatePXEBooted,
|
||||
model.StateInstalling,
|
||||
model.StateInstalled,
|
||||
model.StateFirstBoot,
|
||||
model.StateJoining,
|
||||
}
|
||||
|
||||
var table = map[Trigger]transition{
|
||||
TriggerRebuildRequested: {
|
||||
from: []model.HostState{model.StateRegistered, model.StateReady, model.StateFailed},
|
||||
to: model.StatePXEReady,
|
||||
},
|
||||
TriggerPXEScriptServed: {
|
||||
from: []model.HostState{model.StatePXEReady},
|
||||
to: model.StatePXEBooted,
|
||||
},
|
||||
TriggerAnswerServed: {
|
||||
from: []model.HostState{model.StatePXEBooted},
|
||||
to: model.StateInstalling,
|
||||
},
|
||||
TriggerInstallWebhook: {
|
||||
from: []model.HostState{model.StateInstalling},
|
||||
to: model.StateInstalled,
|
||||
},
|
||||
TriggerPhoneHome: {
|
||||
from: []model.HostState{model.StateInstalled},
|
||||
to: model.StateFirstBoot,
|
||||
},
|
||||
TriggerClusterJoinStart: {
|
||||
from: []model.HostState{model.StateFirstBoot},
|
||||
to: model.StateJoining,
|
||||
},
|
||||
TriggerJoinComplete: {
|
||||
from: []model.HostState{model.StateJoining},
|
||||
to: model.StateReady,
|
||||
},
|
||||
TriggerFailed: {
|
||||
from: allActiveStates,
|
||||
to: model.StateFailed,
|
||||
},
|
||||
}
|
||||
|
||||
func Next(current model.HostState, t Trigger) (model.HostState, error) {
|
||||
tr, ok := table[t]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unknown trigger %q", t)
|
||||
}
|
||||
for _, s := range tr.from {
|
||||
if s == current {
|
||||
return tr.to, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("trigger %q not allowed from state %q", t, current)
|
||||
}
|
||||
Reference in New Issue
Block a user