Files
rollcall/cmd/rollcall/config.go
T
Ilker ManapandClaude Opus 5.5 0ff96d7e74
CI / agent (push) Successful in 2s
CI / site (push) Successful in 2s
CI / go (push) Successful in 36s
Day-0 install without a network plan; rollcall net discover
bootstrap.sh no longer needs the provisioning network: it installs and
starts the region on the management address; --iface later models the
subnet, serves DHCP and moves server_url. rollcall net discover shows what
each interface sees (LLDP, DHCP servers via a parallel probe, neighbours)
and suggests the next step. commission_append follows server_url. Doctor
treats a host that serves no DHCP yet as day 0. Fixes found on a clean VM:
os-release clobbered the rack name, install.sh not executable, slow DHCP
offers missed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:28:37 +01:00

119 lines
5.3 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strings"
)
// Config is /etc/rollcall/config.json.
type Config struct {
Listen string `json:"listen"` // "10.213.77.1:80"
ImageDir string `json:"image_dir"` // live image files (vmlinuz, initrd.img, filesystem.squashfs), served under /rollcall/
TFTPRoot string `json:"tftp_root"` // pxelinux.cfg/01-<mac> files are written here
DatabaseURL string `json:"database_url"` // postgres://user:pw@host/db
MasterKeyFile string `json:"master_key_file"` // 32-byte hex key for AES-GCM at-rest encryption
AdminToken string `json:"admin_token"` // bearer token for the management API / web UI
AgentToken string `json:"agent_token"` // X-Rollcall-Token for the agent endpoints ("" = open)
CommissionAppend string `json:"commission_append"` // kernel cmdline used in generated pxelinux files ("" = derived from server_url)
IPMITool string `json:"ipmitool"` // path, default "ipmitool"
BMCHints []BMCHint `json:"bmc_hints"` // out-of-band BMC mapping for machines without in-band IPMI
// stateless provisioning
ServerURL string `json:"server_url"` // how nodes reach this server, e.g. http://10.213.77.1
ImagesDir string `json:"images_dir"` // <images_dir>/<image>/{vmlinuz,initrd.img,filesystem.squashfs}, served under /images/
OverlaysDir string `json:"overlays_dir"` // <overlays_dir>/<overlay>/{system,runtime}/... (Go templates with .tmpl suffix)
DnsmasqDir string `json:"dnsmasq_dir"` // hosts.dhcp + hosts written here for dnsmasq (dhcp-hostsfile / addn-hosts); "" disables
NodePools map[string]string `json:"node_pools"` // {"mgmt": "10.0.0.50-10.0.0.99", "ib": "10.1.0.10-10.1.0.250"}
ClusterVars map[string]any `json:"cluster_vars"` // available to overlay templates as .Vars (profile vars override)
// disk-based deployment
OSDir string `json:"os_dir"` // <os_dir>/<name>/<version>/{disk.raw.zst,meta.json}, served under /os/
OSSources string `json:"os_sources"` // JSON list of upstream cloud images for `rollcall os sync`
// region/rack: this server runs an embedded rack controller named RackName;
// remote rack controllers (rollcall rack) authenticate with RackSecret
RackName string `json:"rack_name"`
RackSecret string `json:"rack_secret"` // legacy shared secret; rack controllers join with a code and mutual TLS now
// rollcall-linux-<arch> for rack controllers on another architecture than the region
RackBinaries string `json:"rack_binaries"`
// certificate authority and HTTPS
CADir string `json:"ca_dir"` // ca.pem, ca-key.pem, region.pem, region-key.pem (default /etc/rollcall/ca)
TLSListen string `json:"tls_listen"` // HTTPS listener, default ":443"; "off" disables
TLSNames []string `json:"tls_names"` // extra names/addresses for the region certificate
AdminHTTP string `json:"admin_http"` // admin API over plain HTTP: allow (default) | loopback | deny
// external IPAM authority (NetBox; Infoblox at sites that run it)
IPAM IPAMConfig `json:"ipam"`
}
// BMCHint maps a machine MAC to a known BMC (virtual lab nodes).
type BMCHint struct {
MAC string `json:"mac"`
Address string `json:"address"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
Comment string `json:"comment,omitempty"`
}
func loadConfig(path string) (*Config, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
c := &Config{Listen: "0.0.0.0:80", IPMITool: "ipmitool", MasterKeyFile: "/etc/rollcall/master.key"}
if err := json.Unmarshal(b, c); err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
for _, k := range []struct{ v, n string }{{c.ImageDir, "image_dir"}, {c.TFTPRoot, "tftp_root"}, {c.DatabaseURL, "database_url"}, {c.AdminToken, "admin_token"}} {
if k.v == "" {
return nil, fmt.Errorf("%s: %s is required", path, k.n)
}
}
if c.ServerURL == "" {
host, _, err := net.SplitHostPort(c.Listen)
if err != nil || host == "" || host == "0.0.0.0" || host == "::" {
return nil, fmt.Errorf("%s: server_url is required when listen is not a specific address", path)
}
c.ServerURL = "http://" + c.Listen // host:port, IPv6 already bracketed ([::1]:80)
}
c.ServerURL = strings.TrimRight(c.ServerURL, "/")
if c.CommissionAppend == "" { // follows server_url (it often changes once the provisioning network is known)
c.CommissionAppend = fmt.Sprintf("boot=live components fetch=%s/rollcall/filesystem.squashfs rollcall.server=%s noautologin rollcall.token=%s console=tty0 console=ttyS0,115200n8",
c.ServerURL, c.ServerURL, c.AgentToken)
}
if c.ImagesDir == "" {
c.ImagesDir = filepath.Join(filepath.Dir(c.ImageDir), "images")
}
if c.OverlaysDir == "" {
c.OverlaysDir = "/etc/rollcall/overlays"
}
if c.OSDir == "" {
c.OSDir = filepath.Join(filepath.Dir(c.ImageDir), "os")
}
if c.OSSources == "" {
c.OSSources = "/etc/rollcall/os-sources.json"
}
if c.RackName == "" {
c.RackName, _ = os.Hostname()
}
if c.CADir == "" {
c.CADir = "/etc/rollcall/ca"
}
switch c.TLSListen {
case "":
c.TLSListen = ":443"
case "off", "none":
c.TLSListen = ""
}
if c.AdminHTTP == "" {
c.AdminHTTP = "allow"
}
return c, nil
}