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>
119 lines
5.3 KiB
Go
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
|
|
}
|