Files
rollcall/cmd/rollcall/main.go
T
Ilker ManapandClaude Opus 5.5 a495c33b66
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 41s
Rack certificate renewal: rack-owned state, atomic, grace period
Renewal could not work: rack join writes the certificate and key as
root, and the rack (user rollcall) failed to overwrite them. A lost
renewal answer also locked the rack out, since the region accepted only
the new serial.

- the renewed certificate and key go into one file in
  /var/lib/rollcall/rack-cert (cert_state_dir), written atomically, and
  are preferred at start when they are this rack's, from the CA, newer
- after a renewal the region accepts the previous serial for 7 days; a
  new join does not keep it
- rollcall rack renew renews now (run as the service user)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:22:20 +01:00

180 lines
7.5 KiB
Go

// rollcall: MAAS-like commissioning server, web UI and CLI in one binary.
//
// rollcall serve [-config /etc/rollcall/config.json]
// rollcall keygen print a random token (admin_token / agent_token)
// rollcall list | show ID [-json] | events ID
// rollcall power ID status|on|off|soft|cycle|reset
// rollcall bootdev ID pxe|disk|cdrom|bios|none
// rollcall recommission ID [-now] (-now: bootdev pxe + power cycle via BMC)
// rollcall delete ID
//
// Client commands read ROLLCALL_URL / ROLLCALL_TOKEN, falling back to the
// config file when it is readable (so root on the server needs no setup).
package main
import (
"flag"
"fmt"
"log"
"os"
)
// version is set at build time: -ldflags "-X main.version=v1.2.3" (release workflow).
var version = "dev"
func usage() {
_, _ = os.Stderr.WriteString(`usage:
rollcall in a terminal: the full-screen console
rollcall tui [-dump WxH -page P -machine ID -plain]
rollcall serve [-config PATH] region: API, database, web UI, embedded rack controller
rollcall doctor [-deep] [-no-dhcp-probe] check this installation and say what to fix
rollcall net discover [-json] what each interface of this host sees (LLDP, DHCP, neighbours)
rollcall rack [-config /etc/rollcall/rack.json] a rack controller next to the machines
rollcall rack join CODE -url http://ADDR [-name N] [-listen ADDR:80] enroll a rack controller (writes rack.json + certificate)
rollcall ca show | issue NAME -host h1,h2 [-client] [-out DIR] the installation's certificate authority (region host)
rollcall ca intermediate NAME -permit DOMAIN,CIDR [-csr FILE] [-days 365] | rotate -yes delegate to an ACME CA / replace the root
rollcall version
rollcall keygen
rollcall list
rollcall show ID [-json]
rollcall events ID
rollcall power ID status|on|off|soft|cycle|reset
rollcall bmc ID [-driver D] [-address A] [-user U] [-password P|-] [-set K=V] [-verify] [-probe] | bmc drivers
rollcall bootdev ID pxe|disk|cdrom|bios|none
rollcall recommission ID [-now]
rollcall delete ID
rollcall images
rollcall profile list | add NAME -image IMG [-append ARGS] [-overlays a,b] [-name-format node%03d] [-vars JSON] | rm NAME
rollcall assign ID PROFILE [-name node001] [-boot]
rollcall unassign ID
rollcall overlay ID [-stage system|runtime] [-extract DIR]
rollcall render PROFILE PATH [-stage system|runtime]
rollcall firmware list | import FILE|URL -vendor V -component C -version X [-models M,..] [-sha256 S] | rm PKG
rollcall firmware show MACHINE [-refresh] | update MACHINE|tag:T PKG [-apply OnReset|Immediate] [-reboot] [-method M] [-force] [-watch]
rollcall os list | sync [NAME...] [-force] | import NAME FILE [-family F] [-version V] | promote NAME VERSION | rm NAME VERSION
rollcall deploy ID OS [-version V] [-disk /dev/sdX] [-hostname H] [-upgrade] [-user-data FILE] [-boot] [-watch]
rollcall release ID [-boot] [-watch]
rollcall jobs ID [-watch] | job cancel ID
rollcall tag ID +NAME... -NAME... manual tags on a machine
rollcall tags list | add NAME [-match TMPL] [-overlays a,b] [-cloud-config FILE] [-comment C] | show NAME | rm NAME
rollcall location ID [-building B] [-room R] [-rack K] [-unit N] [-asset T] (no flags: show)
rollcall location import FILE.csv [-dry] columns: id|serial|mac|node_name, building, room, rack, unit, asset_tag
rollcall location racks | rack NAME -building B -room R [-height 42]
rollcall list [-tag T] [-building B] [-room R] [-rack K]
rollcall fabric list | add NAME [-kind ethernet|infiniband] | rm NAME
rollcall vlan list | add FABRIC VID [-dhcp | -proxy-dhcp] [-rack R] [-secondary R] [-mtu N] [-name N] | rm FABRIC VID
rollcall subnet list | show CIDR | add CIDR -fabric F [-vid N] [-purpose P] [-gateway G] [-dns a,b] [-domain D] | rm CIDR [-force]
rollcall range add CIDR dynamic|reserved|static START END | rm ID
rollcall ip list [-subnet CIDR] | reserve IP [-name N] | release IP
rollcall admin-cert request NAME | list | revoke NAME|SERIAL admin client certificate (browser/CLI without the token)
rollcall dns [status] | sync [-dry] DNS records written directly (PowerDNS API / RFC 2136)
rollcall ipam [status] | sync [-dry] | prefixes | import external IPAM (NetBox): state, reconcile, delegated prefixes
rollcall layout list | show NAME | add NAME FILE.json | rm NAME
rollcall net show ID | set ID [-layout L] [-rack R] [-static IFACE=IP] [-unstatic IP]
rollcall controllers | controller rm NAME | controller join-code [-name N] [-ttl 60] [-addr HOST:443] | dhcp CONTROLLER
(deploy, power and location take tag:NAME instead of ID to act on every machine with that tag)
env: ROLLCALL_URL (default from config listen), ROLLCALL_TOKEN, ROLLCALL_CONFIG;
away from the server the console saves URL and token in ~/.config/rollcall/client.json
`)
os.Exit(2)
}
func newFlags(name string) *flag.FlagSet { return flag.NewFlagSet(name, flag.ExitOnError) }
func main() {
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
if len(os.Args) > 2 && os.Args[1] == "net" && os.Args[2] == "discover" {
os.Args = append([]string{os.Args[0], "net-discover"}, os.Args[3:]...)
}
if len(os.Args) < 2 {
// A bare `rollcall` in a terminal opens the console; in a pipe or a
// script it prints its help, as it always did.
if !interactive() {
usage()
}
if err := runTUI(); err != nil {
log.Fatal(err)
}
return
}
cmd, args := os.Args[1], os.Args[2:]
switch cmd {
case "rack":
if len(args) > 0 && args[0] == "renew" {
if err := rackRenew(args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
return
}
if len(args) > 0 && args[0] == "join" {
if err := rackJoin(args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
return
}
if err := runRack(args); err != nil {
log.Fatal(err)
}
case "ca":
if err := caCmd(args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
case "net-discover":
if err := discoverCmd(args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
case "doctor":
if err := doctorCmd(args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
case "tui", "console":
if err := tuiDump(args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
case "serve":
fs := flag.NewFlagSet("serve", flag.ExitOnError)
cfg := fs.String("config", envOr("ROLLCALL_CONFIG", "/etc/rollcall/config.json"), "config file")
_ = fs.Parse(args)
if err := serve(*cfg); err != nil {
log.Fatal(err)
}
case "version", "-v", "--version":
fmt.Println("rollcall", version)
case "keygen":
fmt.Println(randomToken(24))
case "list", "show", "events", "power", "bootdev", "recommission", "delete",
"images", "profile", "assign", "unassign", "overlay", "render",
"deploy", "release", "jobs", "job", "tag", "tags", "location",
"bmc", "fabric", "vlan", "subnet", "range", "ip", "layout", "net", "controllers", "controller", "dhcp", "ipam", "dns", "admin-cert":
if err := runClient(cmd, args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
case "firmware":
if err := firmwareCmd(args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
case "os":
if err := osCmd(args); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
default:
usage()
}
}
func envOr(k, d string) string {
if v := os.Getenv(k); v != "" {
return v
}
return d
}