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>
180 lines
7.5 KiB
Go
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
|
|
}
|