Hard-cut banger away from source-checkout runtime bundles as an implicit source of\nimage and host defaults. Managed images now own their full boot set,\nimage build starts from an existing registered image, and daemon startup\nno longer synthesizes a default image from host paths.\n\nResolve Firecracker from PATH or firecracker_bin, make SSH keys config-owned\nwith an auto-managed XDG default, replace the external name generator and\npackage manifests with Go code, and keep the vsock helper as a companion\nbinary instead of a user-managed runtime asset.\n\nUpdate the manual scripts, web/CLI forms, config surface, and docs around\nthe new build/manual flow and explicit image registration semantics.\n\nValidation: GOCACHE=/tmp/banger-gocache go test ./..., bash -n scripts/*.sh,\nand make build.
178 lines
4.9 KiB
Go
178 lines
4.9 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
toml "github.com/pelletier/go-toml"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"banger/internal/model"
|
|
"banger/internal/paths"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
type fileConfig struct {
|
|
LogLevel string `toml:"log_level"`
|
|
WebListenAddr *string `toml:"web_listen_addr"`
|
|
FirecrackerBin string `toml:"firecracker_bin"`
|
|
SSHKeyPath string `toml:"ssh_key_path"`
|
|
DefaultImageName string `toml:"default_image_name"`
|
|
AutoStopStaleAfter string `toml:"auto_stop_stale_after"`
|
|
StatsPollInterval string `toml:"stats_poll_interval"`
|
|
MetricsPoll string `toml:"metrics_poll_interval"`
|
|
BridgeName string `toml:"bridge_name"`
|
|
BridgeIP string `toml:"bridge_ip"`
|
|
CIDR string `toml:"cidr"`
|
|
TapPoolSize int `toml:"tap_pool_size"`
|
|
DefaultDNS string `toml:"default_dns"`
|
|
}
|
|
|
|
func Load(layout paths.Layout) (model.DaemonConfig, error) {
|
|
cfg := model.DaemonConfig{
|
|
LogLevel: "info",
|
|
WebListenAddr: "127.0.0.1:7777",
|
|
AutoStopStaleAfter: 0,
|
|
StatsPollInterval: model.DefaultStatsPollInterval,
|
|
MetricsPollInterval: model.DefaultMetricsPollInterval,
|
|
BridgeName: model.DefaultBridgeName,
|
|
BridgeIP: model.DefaultBridgeIP,
|
|
CIDR: model.DefaultCIDR,
|
|
TapPoolSize: 4,
|
|
DefaultDNS: model.DefaultDNS,
|
|
DefaultImageName: "default",
|
|
}
|
|
|
|
var file fileConfig
|
|
configPath := filepath.Join(layout.ConfigDir, "config.toml")
|
|
if info, err := os.Stat(configPath); err == nil && !info.IsDir() {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
if err := toml.Unmarshal(data, &file); err != nil {
|
|
return cfg, err
|
|
}
|
|
} else if err != nil && !os.IsNotExist(err) {
|
|
return cfg, err
|
|
}
|
|
|
|
if value := strings.TrimSpace(file.LogLevel); value != "" {
|
|
cfg.LogLevel = value
|
|
}
|
|
if file.WebListenAddr != nil {
|
|
cfg.WebListenAddr = strings.TrimSpace(*file.WebListenAddr)
|
|
}
|
|
if value := strings.TrimSpace(file.FirecrackerBin); value != "" {
|
|
cfg.FirecrackerBin = value
|
|
} else if path, err := system.LookupExecutable("firecracker"); err == nil {
|
|
cfg.FirecrackerBin = path
|
|
}
|
|
if value := strings.TrimSpace(file.DefaultImageName); value != "" {
|
|
cfg.DefaultImageName = value
|
|
}
|
|
if value := strings.TrimSpace(file.BridgeName); value != "" {
|
|
cfg.BridgeName = value
|
|
}
|
|
if value := strings.TrimSpace(file.BridgeIP); value != "" {
|
|
cfg.BridgeIP = value
|
|
}
|
|
if value := strings.TrimSpace(file.CIDR); value != "" {
|
|
cfg.CIDR = value
|
|
}
|
|
if file.TapPoolSize > 0 {
|
|
cfg.TapPoolSize = file.TapPoolSize
|
|
}
|
|
if value := strings.TrimSpace(file.DefaultDNS); value != "" {
|
|
cfg.DefaultDNS = value
|
|
}
|
|
if value := strings.TrimSpace(file.AutoStopStaleAfter); value != "" {
|
|
duration, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
cfg.AutoStopStaleAfter = duration
|
|
}
|
|
if value := strings.TrimSpace(file.StatsPollInterval); value != "" {
|
|
duration, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
cfg.StatsPollInterval = duration
|
|
}
|
|
if value := strings.TrimSpace(file.MetricsPoll); value != "" {
|
|
duration, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
cfg.MetricsPollInterval = duration
|
|
}
|
|
if value := strings.TrimSpace(os.Getenv("BANGER_LOG_LEVEL")); value != "" {
|
|
cfg.LogLevel = value
|
|
}
|
|
|
|
sshKeyPath, err := resolveSSHKeyPath(layout, file.SSHKeyPath)
|
|
if err != nil {
|
|
return cfg, err
|
|
}
|
|
cfg.SSHKeyPath = sshKeyPath
|
|
return cfg, nil
|
|
}
|
|
|
|
func resolveSSHKeyPath(layout paths.Layout, configured string) (string, error) {
|
|
configured = strings.TrimSpace(configured)
|
|
if configured != "" {
|
|
return configured, nil
|
|
}
|
|
return ensureDefaultSSHKey(filepath.Join(layout.ConfigDir, "ssh", "id_ed25519"))
|
|
}
|
|
|
|
func ensureDefaultSSHKey(path string) (string, error) {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := os.Stat(path); err == nil {
|
|
if err := ensurePublicKeyFile(path); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
} else if !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
|
|
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
privatePEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
|
|
if err := os.WriteFile(path, privatePEM, 0o600); err != nil {
|
|
return "", err
|
|
}
|
|
if err := ensurePublicKeyFile(path); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func ensurePublicKeyFile(privateKeyPath string) error {
|
|
data, err := os.ReadFile(privateKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signer, err := ssh.ParsePrivateKey(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
publicKey := ssh.MarshalAuthorizedKey(signer.PublicKey())
|
|
return os.WriteFile(privateKeyPath+".pub", publicKey, 0o644)
|
|
}
|