506c856046
Previously the orchestrator ran a full DHCP server on a dedicated br-vetting bridge (10.77.0.0/24), which required a hypervisor-level bridge + physical cabling onto that bridge for every repaired host. Real-world bite: the LXC's br-vetting had no L2 path to the target host's PXE NIC, so DHCPDISCOVERs never reached eth1 and PXE silently timed out. dnsmasq's proxy-DHCP mode is the idiomatic answer: it coexists with the LAN's existing DHCP server (UniFi, etc.), never assigns an IP itself, and only supplements the PXE options. No dedicated bridge, no VLAN, no cabling changes \u2014 dnsmasq binds to the LAN interface and layers option 66/67 + the PXE BINL on top of the real DHCP exchange. The MAC allowlist still gates replies, so random LAN clients booting from network get nothing. Template switches dhcp-range=<start,end,lease> to dhcp-range=<cidr>,proxy and replaces dhcp-boot= for first-boot ROM clients with pxe-service= directives (the correct proxy-mode chainload form). Validation drops the dhcp_range regex for a net.ParseCIDR check on pxe.subnet. Config, production/example yaml, and pxe-setup.sh swap --dhcp-range for --subnet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Server Server `yaml:"server"`
|
|
Database Database `yaml:"database"`
|
|
Artifacts Artifacts `yaml:"artifacts"`
|
|
Logs Logs `yaml:"logs"`
|
|
Dispatcher Dispatcher `yaml:"dispatcher"`
|
|
Janitor Janitor `yaml:"janitor"`
|
|
PXE PXE `yaml:"pxe"`
|
|
Network Network `yaml:"network"`
|
|
Agent Agent `yaml:"agent"`
|
|
Notifiers []Notifier `yaml:"notifiers"`
|
|
Routes []Route `yaml:"routes"`
|
|
}
|
|
|
|
type Server struct {
|
|
Bind string `yaml:"bind"`
|
|
PublicURL string `yaml:"public_url"` // user-visible base URL, e.g. https://vetting.lan:8443; used in notification click-throughs
|
|
TLS TLS `yaml:"tls"`
|
|
}
|
|
|
|
type TLS struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
CertFile string `yaml:"cert_file"`
|
|
KeyFile string `yaml:"key_file"`
|
|
}
|
|
|
|
type Database struct {
|
|
Path string `yaml:"path"`
|
|
}
|
|
|
|
type Artifacts struct {
|
|
Dir string `yaml:"dir"`
|
|
RetentionDays int `yaml:"retention_days"` // 0 = keep forever
|
|
}
|
|
|
|
type Logs struct {
|
|
Dir string `yaml:"dir"`
|
|
RetentionDays int `yaml:"retention_days"` // 0 = keep forever
|
|
}
|
|
|
|
type Janitor struct {
|
|
IntervalMinutes int `yaml:"interval_minutes"` // 0 = 60
|
|
}
|
|
|
|
type Dispatcher struct {
|
|
MaxConcurrentRuns int `yaml:"max_concurrent_runs"`
|
|
}
|
|
|
|
type Network struct {
|
|
IperfPort int `yaml:"iperf_port"`
|
|
}
|
|
|
|
// PXE / Notifier / Route are declared up front so the config file is
|
|
// forward-compatible across phases. Phase 1 does not act on these.
|
|
|
|
type PXE struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Interface string `yaml:"interface"`
|
|
Subnet string `yaml:"subnet"` // LAN CIDR, e.g. "192.168.1.0/24"; dnsmasq runs in proxy-DHCP mode scoped to this subnet
|
|
OrchestratorURL string `yaml:"orchestrator_url"`
|
|
TFTPRoot string `yaml:"tftp_root"` // holds ipxe.efi + undionly.kpxe
|
|
LiveDir string `yaml:"live_dir"` // holds vmlinuz + initrd.img; served at /live
|
|
}
|
|
|
|
// Agent holds settings related to the host-mode vetting-agent binary
|
|
// that operators install on their hosts. AssetDir is served at
|
|
// /assets/*, which is where the quick-register script downloads
|
|
// `vetting-agent-linux-amd64` from.
|
|
type Agent struct {
|
|
AssetDir string `yaml:"asset_dir"` // directory containing vetting-agent-linux-amd64; "" disables /assets
|
|
}
|
|
|
|
type Notifier struct {
|
|
Name string `yaml:"name"`
|
|
Type string `yaml:"type"`
|
|
Topic string `yaml:"topic,omitempty"`
|
|
Server string `yaml:"server,omitempty"`
|
|
WebhookURL string `yaml:"webhook_url,omitempty"`
|
|
SMTP SMTP `yaml:"smtp,omitempty"`
|
|
}
|
|
|
|
type SMTP struct {
|
|
Host string `yaml:"host,omitempty"`
|
|
Port int `yaml:"port,omitempty"`
|
|
From string `yaml:"from,omitempty"`
|
|
To []string `yaml:"to,omitempty"`
|
|
}
|
|
|
|
type Route struct {
|
|
MatchKind []string `yaml:"match_kind"`
|
|
MatchSeverity []string `yaml:"match_severity,omitempty"`
|
|
Notifier string `yaml:"notifier"`
|
|
}
|
|
|
|
func Load(path string) (*Config, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config: %w", err)
|
|
}
|
|
var c Config
|
|
if err := yaml.Unmarshal(b, &c); err != nil {
|
|
return nil, fmt.Errorf("parse config: %w", err)
|
|
}
|
|
if c.Server.Bind == "" {
|
|
c.Server.Bind = "127.0.0.1:8080"
|
|
}
|
|
if c.Database.Path == "" {
|
|
c.Database.Path = "./var/vetting.db"
|
|
}
|
|
if c.Artifacts.Dir == "" {
|
|
c.Artifacts.Dir = "./var/artifacts"
|
|
}
|
|
if c.Logs.Dir == "" {
|
|
c.Logs.Dir = "./var/logs"
|
|
}
|
|
if c.Dispatcher.MaxConcurrentRuns == 0 {
|
|
c.Dispatcher.MaxConcurrentRuns = 3
|
|
}
|
|
// Default the agent asset dir alongside the database file so upgrades
|
|
// from configs predating the agent.asset_dir field pick up a sensible
|
|
// location automatically — install.sh creates exactly this directory
|
|
// and drops vetting-agent-linux-amd64 into it, so /assets/* serves
|
|
// without the operator having to touch vetting.yaml.
|
|
if c.Agent.AssetDir == "" {
|
|
c.Agent.AssetDir = filepath.Join(filepath.Dir(c.Database.Path), "assets")
|
|
}
|
|
return &c, nil
|
|
}
|