Files
rollcall/README.md
T
Ilker ManapandClaude Fable 5.1 cb7b6fd19e
CI / go (push) Successful in 35s
CI / agent (push) Successful in 4s
CI / site (push) Successful in 3s
Physical location: building / room / rack / unit / asset tag
- machines.building/room/rack/rack_unit/asset_tag, racks table (height, comment)
- API: POST /machines/{id}/location, POST /locations/import (CSV or JSON, matched by
  id/serial/mac/node_name, ?dry=1), GET/POST/DELETE /racks, ?building/?room/?rack filters
- CLI: rollcall location ..., location import, location racks/rack, list -rack/-room/-building
- UI: location column and building > room > rack chips, bulk Set location…, Racks page with
  elevation views and CSV import dialog, location in machine detail
- .Node.Building/Room/Rack/Unit/AssetTag in overlay templates and tag match rules

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-27 15:32:26 +01:00

287 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rollcall
Rollcall is a small, self-contained commissioning service for bare-metal servers,
in the spirit of MAAS enlistment and commissioning, without the rest of MAAS.
A server that PXE-boots for the first time gets an ephemeral Debian live image.
The image takes a full hardware inventory (DMI, DIMMs, CPUs, disks, NVMe, NICs,
InfiniBand HCAs, GPUs, PCI, LLDP neighbours, TPM), creates an administrator
account on the server's BMC through in-band IPMI, reports everything to the
Rollcall server and powers the machine off. From then on the server is known:
it shows up in the web UI and the CLI, its BMC can be driven through the API,
and it will not boot the commissioning image again unless you ask for it.
Everything server-side is **one static Go binary**: HTTP API, embedded web UI,
CLI, PostgreSQL schema management and the PXE bookkeeping.
## How it works
```
unknown server ──PXE──▶ dnsmasq (DHCP+TFTP) ──▶ pxelinux default menu
│ 8 s timeout
▼
Debian live image over HTTP (kernel+initrd via TFTP,
squashfs via HTTP, runs entirely from RAM)
│
rollcall-agent: inventory + BMC user + POST /api/v1/commission
│
rollcall server: PostgreSQL ◀─┘──▶ writes pxelinux.cfg/01-<mac>
(next boot: "commissioned → power off")
```
* **Enlist**: a machine is identified by its DMI system UUID (fallback: serial,
then boot MAC). First contact creates it in state `new`.
* **Commission**: the inventory upload moves it to `ready`. The BMC password
the agent generated is sealed with AES-256-GCM (key in
`/etc/rollcall/master.key`) before it touches the database. It never leaves
the server again: power and boot-device operations run `ipmitool` server-side.
* **Re-commission**: removes the per-MAC PXE override; optionally sets
bootdev=pxe and power-cycles the machine through its BMC.
## Repository layout
| Path | What |
|---|---|
| `cmd/rollcall/` | Go server + CLI + embedded web UI (`web/`) |
| `agent/` | `rollcall-agent.py` (runs inside the live image), its systemd unit, `build-image.sh` (live-build recipe) |
| `deploy/` | `install.sh`, `rollcall.service`, `config.example.json`, `pxelinux-default`, `os-sources.json`, sync/dnsmasq/controller units, `overlays/` |
| `images/` | stateless root image recipes (`build-rootfs.sh`, `compute-debian13/`) |
| `lab/` | Proxmox test lab: `pve-vbmc` (virtual IPMI BMC for VMs backed by the Proxmox API), cloud-init for the PXE server VM |
| `site/` | Project website (static) |
| `.gitea/workflows/` | CI (fmt/vet/build/tests), release on tags, deploy on push to `main` |
## Server
```sh
# build
cd cmd/rollcall && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o rollcall .
# PostgreSQL role + database
sudo -u postgres psql -c "CREATE ROLE rollcall LOGIN PASSWORD '...';" -c "CREATE DATABASE rollcall OWNER rollcall;"
# config (see deploy/config.example.json), tokens from `rollcall keygen`
sudo install -d -m 0750 /etc/rollcall && sudo install -m 0640 config.json /etc/rollcall/config.json
# install binary, systemd unit, pxelinux menu, tftp symlinks
cd deploy && sudo ./install.sh
```
`config.json` keys: `listen`, `image_dir` (served under `/rollcall/`), `tftp_root`,
`database_url`, `master_key_file`, `admin_token` (web UI + management API),
`agent_token` (what the live image presents), `commission_append` (kernel
command line baked into generated PXE files), `bmc_hints` (out-of-band BMC
mapping for machines without in-band IPMI, e.g. VMs).
### CLI (same binary)
```sh
rollcall list
rollcall show m-2ef1dab828c8 [-json]
rollcall events m-2ef1dab828c8
rollcall power m-2ef1dab828c8 status|on|off|soft|cycle|reset
rollcall bootdev m-2ef1dab828c8 pxe|disk|cdrom|bios|none
rollcall recommission m-2ef1dab828c8 [-now]
rollcall delete m-2ef1dab828c8
```
Reads `ROLLCALL_URL` / `ROLLCALL_TOKEN`, or the config file when readable.
### API
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | `/api/v1/commission` | agent token | inventory upload from the live image |
| GET | `/api/v1/bmc-hint?macs=` | agent token | out-of-band BMC hint for VMs |
| GET | `/api/v1/machines` | admin | list (summaries) |
| GET | `/api/v1/machines/{id}` | admin | full record: inventory, BMC (no secret), events |
| POST | `/api/v1/machines/{id}/power` | admin | `{"action":"on"}` etc., runs ipmitool server-side |
| POST | `/api/v1/machines/{id}/bootdev` | admin | `{"device":"pxe"}` |
| POST | `/api/v1/machines/{id}/recommission[?now=1]` | admin | boot the image again on next PXE (`now=1`: via BMC immediately) |
| DELETE | `/api/v1/machines/{id}` | admin | forget the machine |
| GET/POST/DELETE | `/api/v1/profiles[/{name}]` | admin | stateless profiles |
| POST | `/api/v1/machines/{id}/assign` `/unassign` | admin | stateless node assignment |
| GET | `/api/v1/os` | admin | OS image catalog |
| POST | `/api/v1/machines/{id}/deploy` | admin | `{"os":"rocky-9","disk":"/dev/sda","hostname":"","package_upgrade":false,"boot":true}` |
| POST | `/api/v1/machines/{id}/release` | admin | wipe job |
| GET / POST | `/api/v1/machines/{id}/jobs[/cancel]` | admin | job history / cancel |
| GET | `/api/v1/jobs/self`, POST `…/progress` | job token (`X-Rollcall-Job`) | deploy agent |
| POST | `/api/v1/phone-home/{token}` | job token in path | cloud-init phone_home |
| GET | `/os/*` | none | OS disk images |
| GET | `/rollcall/*` | none | image files (kernel, initrd, squashfs) |
| GET | `/` | token in browser | web UI |
Admin auth is `Authorization: Bearer <admin_token>`; the agent sends
`X-Rollcall-Token`.
## Commissioning image
`agent/build-image.sh` builds a Debian 13 live image with live-build (about
8 minutes, ~780 MB squashfs). It contains the full Debian kernel with non-free
firmware, `ipmitool`, `freeipmi`, `rdma-core`, `infiniband-diags`, `mstflint`,
`nvme-cli`, `smartmontools`, `lldpd`, `lshw` and an SSH server (root, key only)
for debugging. Output goes to `image_dir`.
Kernel parameters understood by the agent: `rollcall.server=URL` (required),
`rollcall.token=…`, `rollcall.bmcuser=NAME` (default `commissioner`),
`rollcall.nopoweroff` (stay up after reporting).
### BMC account
In-band (`/dev/ipmi0`): find the LAN channel, pick the existing `commissioner`
slot or the first empty user ID ≥ 3, set name/password, ADMINISTRATOR privilege,
channel access, enable, SOL payload. The password is random (16 chars) and is
reported once to the server. If no in-band IPMI exists the agent asks the server
for a hint (`bmc_hints`) and verifies it out-of-band.
Vendor quirks (iDRAC/iLO password policies, missing empty slots) are reported in
`bmc.errors` and visible in the UI.
## Stateless nodes (Warewulf-style)
For HPC compute nodes Rollcall can boot a root image straight into RAM and
dress it with per-node files, instead of installing an OS on disk.
* **Images** live in `images_dir/<name>/{vmlinuz,initrd.img,filesystem.squashfs}`
and are built with `images/build-rootfs.sh <name>` from
`images/<name>/{packages.list,hook.chroot,includes/}` (live-build, same
toolchain as the commissioning image). `images/compute-debian13` is a
Debian 13 compute node: slurmd, munge, rdma-core, NFS client, firmware.
* **Profiles** bind an image to overlays, extra kernel parameters, a node name
format (`node%03d`) and variables: `rollcall profile add compute -image compute-debian13 -overlays compute`.
* **Assigning** a commissioned machine to a profile allocates a node name and
IPs from `node_pools`, writes a per-MAC PXE entry that boots the image with
`hostname=`, `rollcall.server=` and a per-node token, and exports static
DHCP leases plus host names for dnsmasq (`dnsmasq_dir`, re-read on HUP via
`rollcall-dnsmasq.path`). `rollcall assign <id> compute -boot` also
power-cycles the machine through its BMC.
* **Overlays** are directory trees under `overlays_dir/<overlay>/{system,runtime}`;
files ending in `.tmpl` are Go templates rendered with the node, all nodes of
the cluster, the profile and `cluster_vars`. The `compute` overlay generates
`/etc/hosts`, `slurm.conf` (node lines from the commissioning inventory:
sockets, cores, threads, memory, GPUs), `gres.conf`, a persistent SSH host
key per node (sealed in the database), root `authorized_keys` and the munge
key. Inside the image `rollcall-overlay.service` applies the system overlay
before munge/slurmd/ssh start; a timer refreshes the runtime overlay every
five minutes, which doubles as the node heartbeat (`booted` state, last seen).
* The controller renders the same files: `rollcall render compute etc/slurm/slurm.conf > /etc/slurm/slurm.conf`.
States: `ready → stateless → booted`; `rollcall unassign` releases name and IPs.
## OS deployment (install to disk)
For nodes that should run an operating system from their own disk, Rollcall
writes a vendor cloud image onto the disk and lets cloud-init do first-boot
configuration. No installer, no kickstart/preseed: a deployment is a streamed
disk write and takes about as long as the image download.
```
rollcall deploy <id> rocky-9 -boot -watch
│ server: picks the disk (smallest that fits, or -disk), allocates hostname + IP,
│ creates a job with a one-time token, writes pxelinux.cfg/01-<mac>
│ ("deploy" entry = commissioning image with rollcall.mode=deploy), power-cycles via BMC
▼
commissioning image, deploy mode (agent/rollcall-deploy.py)
GET /api/v1/jobs/self → image URL, sha256, disk, rendered cloud-init seed
curl | tee sha256 | pv | zstd -d | dd of=/dev/sda (progress every 3 s)
mount root fs → /var/lib/cloud/seed/nocloud/{meta-data,user-data}, datasource_list: [NoCloud, None]
(SELinux images: /.autorelabel; UEFI hosts: efibootmgr entry for the ESP loader)
POST progress state=done → server: state deployed, PXE entry = local disk, reboot
▼
first boot: cloud-init sets hostname/fqdn, SSH keys (root + default user), optional
package_upgrade, then phone_home → POST /api/v1/phone-home/<token> (shown as "phoned home")
```
### Image catalog and freshness
```
os_dir/<name>/<version>/disk.raw.zst raw disk image, zstd
os_dir/<name>/<version>/meta.json sizes, sha256, upstream checksum, import time
os_dir/<name>/current -> <version> what new deployments use
```
`os_sources` (`deploy/os-sources.json`) lists upstream "latest" URLs with the
vendor checksum file for each: Debian 13, Ubuntu 24.04, Rocky 9, AlmaLinux 9,
CentOS Stream 9. `rollcall os sync` fetches the checksum file, and only when the
vendor checksum differs from every imported version does it download the image,
verify it, convert to raw (`qemu-img`), compress, write a new version directory
and flip `current` atomically. `rollcall-os-sync.timer` runs it nightly;
`keep` versions are retained per source, nothing younger than a day is pruned,
and a job always pins the exact version it started with. Sources with
`"track": "pinned"` still download new versions but keep `current` until
`rollcall os promote <name> <version>`. Local files go in with
`rollcall os import <name> <file.qcow2>`. Image freshness is monthly at best
upstream; the deploy option `-upgrade` (`package_upgrade`) pulls the rest on
first boot.
```sh
rollcall os list | sync [NAME...] [-force] | import NAME FILE [-family debian|rhel] | promote NAME VERSION | rm NAME VERSION
rollcall deploy ID OS [-version V] [-disk /dev/nvme0n1] [-hostname H] [-upgrade] [-packages a,b] [-user-data FILE] [-boot] [-watch]
rollcall release ID [-wipe quick|full] [-boot] [-watch] # wipe the disk, back to ready
rollcall jobs ID [-watch] | job cancel ID
```
States: `ready → deploying → deployed` (`deploy-failed` on error; the agent stays
up for ssh). A deployed machine's PXE entry defaults to local disk, so boot order
can stay "network first" on BIOS hosts; on UEFI hosts the agent adds a boot entry
for the installed loader. `rollcall recommission` or `release` take it back.
## Tags
Tags single out machines for special treatment, as in MAAS. A tag has a name
and optionally a **match rule**, a **cloud-config fragment** and **overlays**.
```sh
rollcall tags add gpu -match '{{ gt .Node.GPUs 0 }}' -cloud-config deploy/tags/gpu-nvidia.yaml -comment "NVIDIA nodes"
rollcall tags add rack-3 -comment "rack 3, row B" # manual only
rollcall tag m-2ef1dab828c8 +rack-3 +burn-in -staging # manual tags on one machine
rollcall list -tag gpu
rollcall deploy tag:gpu rocky-9 -boot # every gpu machine
rollcall power tag:rack-3 off
```
* **Auto tags** come from the match rule, a Go template over `.Node` (GPUs,
IB GUIDs, CPUs, memory, disks, NVMe, PCI vendor ids, manufacturer, product,
OS, existing tags). They are recomputed at every commissioning and whenever a
tag definition is saved; manual tags are never touched by rules.
* **At deployment** the fragments of the machine's tags are appended to the
generated user-data as MIME parts (`packages`, `runcmd`, `write_files` merge
additively; a `#!` fragment runs as a script). `deploy/tags/gpu-nvidia.yaml`
installs the NVIDIA driver on Debian, Ubuntu and the RHEL family and reboots.
* **For stateless nodes** a tag's overlays are applied after the profile's, so a
`gpu` tag can add `gres.conf` lines or driver modules without a second profile.
## Physical location
Every machine can carry building / room / rack / rack unit and an asset tag.
```sh
rollcall location m-2ef1dab828c8 -building DC1 -room "Hall A" -rack R12 -unit 24 -asset A-0042
rollcall location tag:gpu -building DC1 -room "Hall B" # bulk by tag
rollcall location import racks.csv -dry # from a DCIM / asset export
rollcall location racks | rack R12 -building DC1 -room "Hall A" -height 48
rollcall list -rack R12
```
The CSV needs a header; rows are matched by `id`, `serial`, `mac` or `node_name`
and may carry `building, room, rack, unit, asset_tag`. Racks are created on
first use (42U) and can be resized. The web UI shows a location column with
building › room › rack chips, a **Racks** page with elevation views (machines
drawn at their U, coloured by state) and bulk "Set location…". Templates and
tag rules see `.Node.Building`, `.Node.Room`, `.Node.Rack`, `.Node.Unit`, so a
rack can drive a tag (`{{ eq .Node.Rack "R12" }}`) or a Slurm `topology.conf`
overlay.
## Lab
`lab/` documents a Proxmox SDN test setup: an isolated vnet, a PXE server VM
running dnsmasq + Rollcall, two diskless VMs, and `pve-vbmc`, a tiny
IPMI-over-LAN BMC emulator that drives VMs through the Proxmox API so that
`ipmitool power on` and `chassis bootdev pxe` work against virtual machines.
## Status
Commissioning, stateless nodes and disk deployment work end to end in the lab
(virtual machines with 30 GB disks, SeaBIOS, out-of-band BMC hints). The
in-band BMC path and the UEFI boot-entry step are implemented but have not yet
been exercised on real hardware.