bda568b25c
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>
119 lines
2.4 KiB
Go
119 lines
2.4 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
|
|
"provisioning/internal/model"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type serverTypesFile struct {
|
|
ServerTypes map[string]model.ServerType `yaml:"server_types"`
|
|
}
|
|
|
|
type ServerTypeRegistry struct {
|
|
mu sync.RWMutex
|
|
types map[string]model.ServerType
|
|
path string
|
|
}
|
|
|
|
func LoadServerTypes(path string) (*ServerTypeRegistry, error) {
|
|
r := &ServerTypeRegistry{path: path}
|
|
if err := r.load(); err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (r *ServerTypeRegistry) Get(key string) (model.ServerType, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
st, ok := r.types[key]
|
|
return st, ok
|
|
}
|
|
|
|
func (r *ServerTypeRegistry) List() []model.ServerType {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]model.ServerType, 0, len(r.types))
|
|
for _, st := range r.types {
|
|
out = append(out, st)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *ServerTypeRegistry) Keys() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]string, 0, len(r.types))
|
|
for k := range r.types {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *ServerTypeRegistry) load() error {
|
|
data, err := os.ReadFile(r.path)
|
|
if err != nil {
|
|
return fmt.Errorf("read server types: %w", err)
|
|
}
|
|
var f serverTypesFile
|
|
if err := yaml.Unmarshal(data, &f); err != nil {
|
|
return fmt.Errorf("parse server types: %w", err)
|
|
}
|
|
if len(f.ServerTypes) == 0 {
|
|
return fmt.Errorf("server types file contains no types")
|
|
}
|
|
for k, st := range f.ServerTypes {
|
|
st.Key = k
|
|
f.ServerTypes[k] = st
|
|
}
|
|
r.mu.Lock()
|
|
r.types = f.ServerTypes
|
|
r.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (r *ServerTypeRegistry) Watch(stop <-chan struct{}) {
|
|
watcher, err := fsnotify.NewWatcher()
|
|
if err != nil {
|
|
log.Printf("server-types: fsnotify unavailable: %v", err)
|
|
return
|
|
}
|
|
if err := watcher.Add(r.path); err != nil {
|
|
log.Printf("server-types: watch failed: %v", err)
|
|
_ = watcher.Close()
|
|
return
|
|
}
|
|
go func() {
|
|
defer watcher.Close()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case ev, ok := <-watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if ev.Op&(fsnotify.Write|fsnotify.Create) != 0 {
|
|
if err := r.load(); err != nil {
|
|
log.Printf("server-types: hot-reload failed: %v (keeping previous config)", err)
|
|
} else {
|
|
log.Printf("server-types: reloaded %d types", len(r.types))
|
|
}
|
|
}
|
|
case err, ok := <-watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
log.Printf("server-types: watch error: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|