ipam.provider infoblox: one record:host per address, named by its DNS
name and configured for DNS in dns_view ("-" = IPAM only); ownership is
the rollcall_machine extensible attribute, changed with extattrs+ so a
host's other attributes stay. Anything else on the address (a host, a
lease, a fixed address, a DHCP or reserved range, network/broadcast) makes
it taken. Delegated networks carry the rollcall attribute. WAPI paging.
The reconcile no longer compares fields an authority does not keep
(Infoblox has no prefix length or status on a host). Built from the WAPI
reference and tested against a fake WAPI; no real grid yet.
Closes #11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
925 lines
47 KiB
Markdown
925 lines
47 KiB
Markdown
# 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` (region), `install-rack.sh` + `rack.example.json` (rack controller), units, `config.example.json`, `os-sources.json`, `tags/`, `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
|
||
|
||
New site: **[INSTALL.md](INSTALL.md)** (`deploy/bootstrap.sh`, from day 0 without a network plan to the first deployed machine). The steps below are the manual equivalent.
|
||
|
||
```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).
|
||
|
||
### Console (same binary)
|
||
|
||
A bare `rollcall` in a terminal opens a full-screen console, built like the
|
||
llmgw one (Bubble Tea): the fleet down the left (Overview, Machines, Racks,
|
||
Jobs, Events) and provisioning (Tags, Profiles, OS images), the chosen page on
|
||
the right, keys along the bottom. It is keyboard only, so it works on serial
|
||
consoles and BMC KVMs.
|
||
|
||
* Machines: `/` filter (name, serial, MAC, IP, rack, tag), `s` cycles the state
|
||
filter, `Space` selects, `*` selects everything shown, `Enter` opens a
|
||
machine, `a` opens the actions for the selection or the current row: power,
|
||
boot device, deploy OS, assign/unassign profile, tags, location,
|
||
re-commission, release, cancel job, delete. Destructive ones ask first.
|
||
* Racks: `Enter` shows the elevation; `Enter` on a unit opens that machine.
|
||
* Jobs and events open the machine they belong to; tags, profiles and OS
|
||
images show their definition. `r` refreshes (it also refreshes on its own,
|
||
every 3 s while a job runs), `?` lists the keys, `q` quits.
|
||
|
||
On the server as root it reads `/etc/rollcall/config.json`. Anywhere else it
|
||
asks for the server URL and admin token once and keeps them in
|
||
`~/.config/rollcall/client.json` (mode 0600), which the CLI then uses as well.
|
||
In a pipe or a script a bare `rollcall` prints its help as before.
|
||
`rollcall tui -dump 140x40 -page machines -plain` renders one screen to stdout.
|
||
|
||
### 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.
|
||
|
||
## Network and rack controllers
|
||
|
||
### Model
|
||
|
||
As in MAAS: **fabric → VLAN → subnet → IP ranges**.
|
||
|
||
* A **fabric** is a switched layer-2 domain (`ethernet` or `infiniband`).
|
||
* A **VLAN** has a VID (0 = untagged; a partition key on IB), an MTU, and
|
||
says whether rollcall serves DHCP/PXE on it and which **rack controller**
|
||
does (a second one splits the dynamic pool with it).
|
||
* A **subnet** has a purpose (provisioning, mgmt, bmc, storage, ib, other), a
|
||
gateway, DNS servers and a domain.
|
||
* **Ranges** inside a subnet are `dynamic` (the DHCP pool PXE and
|
||
commissioning use), `reserved` (never handed out: routers, switches,
|
||
controllers) and `static` (where machines get their addresses; without one,
|
||
anything outside dynamic/reserved).
|
||
* **IPAM** records every address handed out (`rollcall ip list`), pinned ones
|
||
(`rollcall net set ID -static ens18=10.0.0.60`) and reserved single
|
||
addresses (`rollcall ip reserve 10.0.0.5 -name switch-1`). Addresses a machine
|
||
already had are adopted when its subnet is defined.
|
||
|
||
```sh
|
||
rollcall fabric add hall-a
|
||
rollcall subnet add 10.50.0.0/24 -fabric hall-a -vid 0 -purpose provisioning -gateway 10.50.0.1 -dns 10.50.0.1 -domain dc.example
|
||
rollcall range add 10.50.0.0/24 reserved 10.50.0.1 10.50.0.20
|
||
rollcall range add 10.50.0.0/24 static 10.50.0.50 10.50.0.199
|
||
rollcall range add 10.50.0.0/24 dynamic 10.50.0.200 10.50.0.250
|
||
rollcall vlan add hall-a 0 -dhcp -rack hall-a-rack1 # this rack controller serves DHCP/PXE here
|
||
rollcall subnet add 10.60.0.0/24 -fabric hall-a -vid 200 -purpose storage
|
||
rollcall dhcp hall-a-rack1 # the dnsmasq configuration it generates
|
||
```
|
||
|
||
### Machine interfaces
|
||
|
||
A **layout** describes interfaces in terms that fit many machines: NICs are
|
||
chosen by selectors (`boot`, `name:X`, `mac:X`, `fastest:N`, `ib:N`, `all`),
|
||
not by MAC. Interfaces are `physical`, `bond` (802.3ad by default) or `vlan`;
|
||
each link names a subnet (CIDR, name, or `boot`) and a mode: `auto` (an address
|
||
from the subnet), `static` (a given one), `dhcp` or `link_up`.
|
||
|
||
```json
|
||
{"interfaces": [
|
||
{"type": "physical", "select": "boot", "links": [{"subnet": "boot", "mode": "auto", "default_gateway": true}]},
|
||
{"type": "bond", "name": "bond0", "members": ["fastest:2"], "mtu": 9000, "links": [{"mode": "link_up"}]},
|
||
{"type": "vlan", "parent": "bond0", "vid": 200, "links": [{"subnet": "10.60.0.0/24", "mode": "auto"}]},
|
||
{"type": "physical", "select": "ib:0", "links": [{"subnet": "ib0", "mode": "auto"}]}
|
||
]}
|
||
```
|
||
|
||
`rollcall layout add gpu-bond layout.json`, then either per machine
|
||
(`rollcall net set ID -layout gpu-bond`) or per tag (`rollcall tags add gpu
|
||
… -layout gpu-bond`). Without a layout the boot NIC gets an `auto` address on
|
||
its subnet; without any subnet model, deployed machines use DHCP as before.
|
||
|
||
At deployment the layout is resolved against the machine's inventory,
|
||
addresses are allocated, and two files go into the image next to the
|
||
cloud-init seed: `network-config` (cloud-init v2; cloud-init renders it for
|
||
netplan, NetworkManager or ifupdown) and a udev rule that pins each NIC's name
|
||
to its MAC, because RHEL-family cloud images boot with `net.ifnames=0`.
|
||
Release returns the allocated addresses. `rollcall net show ID` previews it all.
|
||
|
||
Verified in the lab: Debian 13 and Ubuntu 24.04 (netplan) and Rocky 9
|
||
(NetworkManager), with a static boot NIC and with an active-backup bond
|
||
carrying VLAN 200.
|
||
|
||
### Region and rack controllers
|
||
|
||
`rollcall serve` is the **region**: database, API, web UI, IPAM, image store.
|
||
**Rack controllers** sit next to the machines, one or two per network segment
|
||
(hall, building, site):
|
||
|
||
* they serve DHCP and TFTP with dnsmasq on the VLANs assigned to them; the
|
||
configuration, per-MAC PXE files and host files are computed by the region;
|
||
* they run BMC commands (BMC networks are rarely routed to the region);
|
||
* they serve boot and OS images from a local cache, filled from the region on
|
||
first use (kernels and initrds are prefetched, TFTP cannot wait);
|
||
* they pass the node API (commissioning, deploy jobs, overlays, phone-home)
|
||
through to the region and tell it which rack a machine came through.
|
||
|
||
A rack controller only needs to reach the region: it long-polls its desired
|
||
state and a command queue. The region host runs an **embedded** rack
|
||
controller (`rack_name`, default its hostname), which is all a single-server
|
||
installation needs. Remote ones:
|
||
|
||
```sh
|
||
rollcall controller join-code -name hall-b -addr 10.0.0.234:443 # on the region: one word, one use, 60 min
|
||
# on the rack host (Debian/Ubuntu), from deploy/ with the binary next to it:
|
||
./rollcall rack join rollcall-join1-... -url http://10.213.78.1 # writes /etc/rollcall/rack.json + certificate
|
||
sh install-rack.sh # dnsmasq, syslinux, cache dirs, rollcall-rack.service
|
||
rollcall controllers # on the region: it shows up online
|
||
rollcall vlan add hall-b 0 -dhcp -rack hall-b
|
||
```
|
||
|
||
dnsmasq is restarted only when its generated configuration changes, and only
|
||
if it comes back; otherwise the last working configuration is restored (a
|
||
hand-written DHCP configuration for the same interface is the usual cause:
|
||
`install.sh` points it out). A machine that moved to another segment gets
|
||
`rollcall net set ID -rack NAME`.
|
||
|
||
The lab runs both: `pxe-server` (embedded, 10.213.77.0/24) and `pxe-rack2`
|
||
(VM 213, remote, 10.213.78.0/24 on SDN vnet `pxenet2`), with node002 behind
|
||
pxe-rack2 and its virtual BMC on pxe-rack2. Rack ↔ region traffic is mutual
|
||
TLS (next section), so the BMC passwords of queued commands are encrypted.
|
||
|
||
|
||
### Agents over HTTPS
|
||
|
||
Agents talk to their rack controller over HTTPS: the commissioning agent,
|
||
the deploy agent and the image download. The region gives each rack a
|
||
server certificate for its own URL's host (`CN=rack-server:NAME`, server
|
||
use only, never one of the region's addresses). The rack serves the
|
||
agents' endpoints on `<host>:443` as well as on HTTP; `tls_listen` in
|
||
`rack.json` changes the port, `off` turns it off. The embedded rack uses
|
||
the region's own HTTPS listener.
|
||
|
||
The pxelinux command line carries `rollcall.tls=https://HOST` and
|
||
`rollcall.ca=<SHA-256 of the CA>`. An agent fetches `/ca.pem`, accepts it
|
||
only by that fingerprint, and from then on uses HTTPS with it. When TLS
|
||
was asked for and fails, it does not fall back to HTTP.
|
||
|
||
What stays on HTTP: live-boot fetching the commissioning squashfs, and
|
||
`/ca.pem` itself (checked by its fingerprint). This keeps BMC passwords
|
||
and seed files from anyone who only listens, and from an HTTP server
|
||
posing as the rack. TFTP and the kernel and initrd stay unauthenticated,
|
||
so an attacker who can rewrite boot files on the wire is not stopped;
|
||
that needs UEFI Secure Boot with signed images.
|
||
### DHCP already on the network: proxy DHCP
|
||
|
||
Where the site's DHCP server keeps handing out addresses (a corporate DHCP,
|
||
Infoblox, a router), a VLAN can be served in **proxy** mode: the rack
|
||
controller does not hand out addresses, it only answers PXE clients with where
|
||
to boot from (dnsmasq proxy DHCP), and TFTP and HTTP stay on the rack.
|
||
|
||
```
|
||
rollcall vlan add FABRIC VID -proxy-dhcp -rack RACK # web UI: subnet, DHCP / PXE: proxy
|
||
deploy/bootstrap.sh --iface eth1 --proxy-dhcp # the same at install time
|
||
```
|
||
|
||
- The subnet needs no ranges. Without a static range, deployed machines keep
|
||
getting their addresses from the site's server (`dhcp` links); set a static
|
||
range aside, outside the site's pool, to give them fixed ones.
|
||
- No `dhcp-host` lines are written for proxy subnets, and the router and DNS
|
||
options come from the site's server.
|
||
- `rollcall doctor` expects the foreign DHCP server here, and fails if none answers.
|
||
- dnsmasq (2.91, still in 2.93) loops forever in its proxy code when a proxy
|
||
range carries a `set:` tag: Rollcall's generated configuration leaves the tag
|
||
out and limits the PXE services by interface instead.
|
||
|
||
Verified in the lab with a separate DHCP server on the provisioning network:
|
||
commissioning and three deployments in a row through the proxy.
|
||
|
||
#### Fixed leases on the site's DHCP server (Kea, OpenWrt)
|
||
|
||
With a reservation driver, machines on proxy VLANs still get the addresses
|
||
Rollcall plans: every such address becomes a fixed lease (the MAC of its
|
||
interface, the address, the hostname) on the site's server. This is the
|
||
counterpart of Infoblox's `fixedaddress`. Leases are reconciled on every
|
||
address change and every `sync_interval`. Rollcall changes or deletes only
|
||
its own leases; one held by someone else (same MAC or address) is reported
|
||
as a conflict.
|
||
|
||
```json
|
||
"dhcp_reservations": {
|
||
"provider": "kea",
|
||
"url": "http://kea.example:8000",
|
||
"user": "rollcall",
|
||
"password_file": "/etc/rollcall/kea.pw"
|
||
}
|
||
```
|
||
|
||
- **kea:** the control API with the `host_cmds` hook, open source since Kea
|
||
3.0; Debian's Kea 2.6 lacks it and the driver says so.
|
||
- Ownership goes in `user-context {"rollcall": machine}`.
|
||
- With a hosts database the leases are kept there. Without one they go
|
||
into the running configuration, and `config-write` saves them
|
||
(`persist: false` turns that off).
|
||
- **openwrt:** rpcd over HTTP (`url: http://router`). Fixed leases are
|
||
`host` sections in `/etc/config/dhcp` with `option rollcall`, followed by
|
||
`uci commit` and a dnsmasq reload. Give Rollcall its own rpcd login with
|
||
only this:
|
||
|
||
```json
|
||
/usr/share/rpcd/acl.d/rollcall.json
|
||
{ "rollcall": { "description": "Rollcall fixed DHCP leases",
|
||
"read": { "ubus": { "uci": ["get"] }, "uci": ["dhcp"] },
|
||
"write": { "ubus": { "uci": ["add", "delete", "commit"], "rc": ["init"] }, "uci": ["dhcp"] } } }
|
||
```
|
||
|
||
and a `config login` in `/etc/config/rpcd` (username, an MD5-crypt
|
||
password, `list read/write 'rollcall'`).
|
||
|
||
```
|
||
rollcall reservations # provider, last sync, conflicts
|
||
rollcall reservations sync -dry
|
||
```
|
||
|
||
IPv6 addresses are skipped: DHCPv6 reservations are keyed on DUIDs, not
|
||
MACs. So are interfaces without a single MAC, such as bonds.
|
||
|
||
Verified in the lab against Kea 3.0.5 and OpenWrt 25.12.6: the machine got
|
||
its planned address from the site's server and not from the pool.
|
||
|
||
## Certificate authority and TLS
|
||
|
||
The region is the installation's certificate authority, as the llmgw control
|
||
plane is: `ca_dir` (default `/etc/rollcall/ca`) holds `ca.pem` and
|
||
`ca-key.pem` (ECDSA P-256, ten years), created on first start.
|
||
|
||
* **HTTPS.** The region serves API, UI and images on `tls_listen` (default
|
||
`:443`) with a certificate for every name and address it has plus
|
||
`tls_names`; it is reissued when they change or a month before it ends.
|
||
`admin_http` decides what plain HTTP may still do with the admin API:
|
||
`allow` (default, for compatibility), `loopback` (the CLI on the region
|
||
host) or `deny`. Nodes keep booting over HTTP: firmware and the live image
|
||
cannot check a certificate before they have one.
|
||
* **Rack controllers** join with a code from `rollcall controller join-code`:
|
||
one word carrying the first half of the CA's SHA-256, a one-time secret, an
|
||
expiry and the region's address. `rollcall rack join` checks the region
|
||
against the fingerprint, sends a CSR with the secret and gets a
|
||
client+server certificate (`rack:<name>`, a year). From then on every rack
|
||
call is mutual TLS and the region identifies the rack by its certificate;
|
||
`rollcall controller rm` revokes it at once. Racks renew a month before
|
||
expiry. The old `rack_secret` is still accepted when configured.
|
||
* **Updates.** Racks follow the region's version: the region names the
|
||
binary (version, architecture, SHA-256) in each rack's state and serves it
|
||
over the rack's mutual-TLS channel. The rack stages it in
|
||
`/var/lib/rollcall/update` after checking the checksum and that it reports
|
||
the expected version; `rollcall-rack-update.path` (root) installs it,
|
||
restarts the rack and puts the previous binary back if the rack does not
|
||
register again within a minute (`last-result` says what happened). Racks on
|
||
another architecture get `rack_binaries/rollcall-linux-<arch>`;
|
||
`"auto_update": false` in `rack.json` opts out. `rollcall controllers`
|
||
shows each rack's version.
|
||
* **Admin certificates.** Instead of the admin token, a person or laptop can
|
||
hold a client certificate: `rollcall admin-cert request NAME` (with the
|
||
token once) makes a key locally, sends only a CSR and gets
|
||
`admin:NAME` back; the CLI and console use it from `client.json`, and the
|
||
`.p12` it writes goes into the browser's keychain, after which the web UI
|
||
signs in by itself. `rollcall admin-cert list | revoke NAME` (revocation
|
||
takes effect within 30 seconds). Certificates are checked against the CA,
|
||
the client-auth usage and the serials the region recorded.
|
||
* **Clients.** On the region host the CLI reads the CA from `ca_dir`;
|
||
elsewhere `ROLLCALL_CA=ca.pem`, or the console shows the CA fingerprint on
|
||
first connect and keeps the CA in `~/.config/rollcall/client.json` once you
|
||
confirm it (`rollcall ca show` prints it on the region). `/ca.pem` serves
|
||
the certificate; the web login links to it.
|
||
* **Machines** trust it: deployments write the CA into the trust anchors of
|
||
both families (`/usr/local/share/ca-certificates`, `/etc/pki/ca-trust/source/anchors`)
|
||
and rebuild the store at every boot (`update-ca-certificates` or
|
||
`update-ca-trust`; cloud-init's `ca_certs` fails on RHEL-family systems);
|
||
stateless overlays get it as `.CA`. `rollcall ca issue NAME
|
||
-host h1,h2 [-client]` issues leaves for anything else (Slurm REST, a
|
||
registry, a web front end).
|
||
* **Delegation.** The root may sign one level of intermediate CAs, each
|
||
limited by name constraints to the domains and address ranges it serves:
|
||
`rollcall ca intermediate caddy -permit int.example.com,10.213.0.0/16
|
||
-csr caddy.csr` signs a CSR (the key stays on the host that made it). An ACME
|
||
server under that intermediate (Caddy's `acme_server`, step-ca) then issues
|
||
short-lived certificates to nodes with certbot or acme.sh; everything that
|
||
trusts the Rollcall CA trusts them, and nothing outside the permitted names
|
||
can be issued. Roots created before this (path length 0) cannot delegate:
|
||
`rollcall ca rotate -yes` replaces the root, keeping the old one as a
|
||
backup; rack controllers then join again and clients trust the new
|
||
fingerprint. `deploy/tags/acme-cert.yaml` is a tag that fetches a node
|
||
certificate at first boot and renews it.
|
||
|
||
## Power drivers
|
||
|
||
Every machine's BMC record names a driver; commands run on the rack controller
|
||
that reaches the BMC network.
|
||
|
||
| Driver | For |
|
||
|---|---|
|
||
| `ipmi` | IPMI 2.0 over LAN (ipmitool lanplus), any vendor; settings `cipher` (17 on newer BMCs), `privilege`, `interface` |
|
||
| `redfish` and the vendor drivers below | DMTF Redfish (see below) |
|
||
| `proxmox` | Proxmox VE VMs: API token (user = token id, password = secret), settings `node`, `vmid` |
|
||
| `manual` | no power control; actions say so |
|
||
|
||
### Redfish across vendors
|
||
|
||
There is one Redfish implementation of the DMTF standard. These rules hold for
|
||
every vendor:
|
||
|
||
- **Reset types and boot targets.** Read from what the BMC advertises
|
||
(`@Redfish.AllowableValues` or the action's ActionInfo), never assumed.
|
||
- **Boot override.** Target and enable go in one PATCH.
|
||
- **If-Match.** Sent with the resource's ETag (header or `@odata.etag`). On
|
||
412 it is retried with the strong form, then without; on 428 it is fetched
|
||
first.
|
||
- **Pending settings.** A BMC that keeps them elsewhere (`.../SD`,
|
||
`@Redfish.Settings`) gets the same PATCH there.
|
||
- **Busy answers.** Retried for up to a minute: the server is in POST, 409
|
||
right after a power change, 503, "Retry in N seconds".
|
||
- **202 answers.** Followed to their task monitor.
|
||
- **Reset on the chassis.** Used when the system has no reset of its own.
|
||
- **Missing power cycle.** ForceOff, wait for Off, then On.
|
||
- **Paths from the BMC.** Only followed if they stay on the BMC and under
|
||
`/redfish/`.
|
||
|
||
What a vendor documents differently is a profile in
|
||
`cmd/rollcall/redfish.go`, with its sources. The profile decides the driver
|
||
name, how the BMC is recognised, its usual system path, the boot mode rule, how
|
||
to clear an override and the login method.
|
||
|
||
| Driver | Vendor | Boot mode in an override | Notes |
|
||
|---|---|---|---|
|
||
| `idrac` | Dell iDRAC 8/9/10 | not sent: a change starts a BIOS job | `System.Embedded.1`; iDRAC 10 takes boot settings at `.../Settings` |
|
||
| `ilo` | HPE iLO 4 (2.30+), 5, 6 | current one, without it if refused | no PowerCycle (ForceRestart); no boot changes during POST (retried) |
|
||
| `xcc` | Lenovo XCC, XCC2 | current one, without it if refused | override is Once only |
|
||
| `supermicro` | Supermicro X11+, H12+ | always the current one | left out, some boards switch to Legacy |
|
||
| `xfusion` | xFusion / Huawei iBMC | current one, without it if refused | session login; If-Match on every PATCH; ForcePowerCycle |
|
||
| `kaytus` | KAYTUS / Inspur | current one, without it if refused | no public Redfish guide: reset may be on the chassis, a PATCH that worked can answer 400 |
|
||
| `irmc` | Fujitsu / Fsas iRMC S5, S6 | not sent | `Systems/0`; ETag from the body; clearing is Target None (never Enabled Disabled); no GracefulShutdown (power button) |
|
||
| `cimc` | Cisco UCS C-series CIMC | current one, without it if refused | `Systems/<serial>`, found through the collection |
|
||
| `gigabyte` | GIGABYTE (MegaRAC SP-X) | always the current one | `Systems/Self`; If-Match required; short reset list |
|
||
| `asrockrack` | ASRock Rack (MegaRAC) | always the current one | boot settings in `Systems/Self/SD` |
|
||
| `openbmc` | OpenBMC (bmcweb) | current one, without it if refused | `Systems/system` |
|
||
| `ami` | AMI MegaRAC SP-X boards | always the current one | `Systems/Self`; If-Match required |
|
||
|
||
Settings a machine can override:
|
||
|
||
| Setting | Effect |
|
||
|---|---|
|
||
| `system=` | the system path |
|
||
| `boot_mode=UEFI\|Legacy` | always send this mode |
|
||
| `auth=session\|basic` | login method |
|
||
| `verify_tls=true` | check the BMC's certificate |
|
||
|
||
Every profile is tested against a fake service that behaves as its sources
|
||
describe (`redfish_test.go`). The profiles have not yet run against the real
|
||
machines.
|
||
|
||
**HTTPS BMCs (Redfish, Proxmox) are trusted by their certificate.** Before
|
||
credentials first go to such a BMC, the rack controller reads its
|
||
certificate with a TLS handshake that sends nothing. The region pins the
|
||
SHA-256 on the BMC record as `extra tls_fingerprint`. A later connection
|
||
showing a different certificate sends no credentials: the command fails
|
||
with both fingerprints, and the machine gets the event
|
||
`bmc certificate changed`.
|
||
|
||
To trust a replaced BMC, clear the pin with `rollcall bmc ID -set
|
||
tls_fingerprint=`. Changing the address does the same, and so does a BMC
|
||
firmware update run by Rollcall. To check against the system's CAs
|
||
instead, set `verify_tls=true`.
|
||
|
||
After commissioning, a BMC still on IPMI is asked whether it speaks Redfish
|
||
with the same account; if it does, the driver for its vendor takes over
|
||
(event `bmc driver`). An operator's choice is never overridden.
|
||
|
||
```sh
|
||
rollcall bmc drivers
|
||
rollcall bmc m-1234 -driver idrac -address 10.0.5.21 -user root -password - -verify < pw
|
||
rollcall bmc m-1234 -driver redfish -set system=/redfish/v1/Systems/1 -set verify_tls=true
|
||
rollcall bmc m-1234 -probe # detect Redfish again
|
||
```
|
||
|
||
### Firmware updates (Redfish)
|
||
|
||
Rollcall keeps firmware packages in a repository on the region, reads each
|
||
machine's firmware inventory from its BMC, and runs updates as jobs.
|
||
|
||
```sh
|
||
# on the region: a file (or from anywhere: an https URL, which the region downloads)
|
||
sudo rollcall firmware import BIOS_R760_WN64_2.3.5.EXE -vendor idrac -component bios -version 2.3.5 -models R760 -sha256 ...
|
||
rollcall firmware list
|
||
rollcall firmware show m-1234 -refresh # what is installed, what the repository offers
|
||
rollcall firmware update m-1234 idrac-bios-2.3.5-2c49752f -reboot -watch
|
||
rollcall firmware update tag:r760 idrac-bmc-7.20.10.50-097a4a54
|
||
```
|
||
|
||
**The package.** A package names the BMC driver it is for (`idrac`, `ilo`,
|
||
`supermicro`, ...), a component (`bios`, `bmc`, `nic`, `raid`, `cpld`,
|
||
`psu`, `drive`, `other`), the version the component reports once installed,
|
||
and optionally the models it fits. An update refuses a package for another
|
||
vendor or model unless `-force`. The BMC checks the package itself:
|
||
signature, model and downgrade policy. An HPE `.fwpkg` is unpacked at
|
||
import to the iLO `.bin` or UEFI `.signed.flash` inside, because iLO's
|
||
SimpleUpdate accepts a `.fwpkg` and then flashes nothing.
|
||
|
||
**The job.** The region drives the update through the rack controller next
|
||
to the BMC:
|
||
|
||
1. It hands the package over: an upload, or the BMC downloads it from the
|
||
rack controller's `/firmware/`. Set `firmware_base=` on the BMC record
|
||
when BMCs reach another address.
|
||
2. It watches the BMC's task. A BMC that restarts after flashing itself is
|
||
waited for, up to 25 minutes.
|
||
3. It reads the inventory until the new version shows.
|
||
|
||
`-reboot` power-cycles the host when the update waits for a reset (BIOS on
|
||
most vendors), then verifies. Without it, a staged update ends as
|
||
"accepted, applies at the next reboot". A running update survives a region
|
||
restart. It cannot be cancelled once the BMC has the package.
|
||
|
||
**How each vendor takes a package.** Each vendor profile in
|
||
`cmd/rollcall/redfish_fwplans.go` carries its documented request, with
|
||
sources. Without a profile, the standard methods are tried in this order:
|
||
multipart push, SimpleUpdate pull, HttpPushUri.
|
||
|
||
| Driver | How | Packages | Notes |
|
||
|---|---|---|---|
|
||
| `idrac` | MultipartUpload (SimpleUpdate pull on older iDRAC 9) | DUP `.EXE`, `.d9` | OnReset by default; BIOS/RAID/NIC apply at the next reboot |
|
||
| `ilo` | SimpleUpdate: iLO pulls over HTTP | iLO `.bin`, UEFI `.signed.flash` (from `.fwpkg`) | components only an OS agent flashes need SUM/iSUT |
|
||
| `xcc` | `/mfwupdate`, after taking `HttpPushUriTargetsBusy` | `.uxz` | update XCC before UEFI |
|
||
| `supermicro` | `/UpdateService/upload` with targets and preserve options | `.bin` | licence SFT-OOB-LIC/SFT-DCMS-SINGLE; host powered off for BIOS |
|
||
| `gigabyte`, `ami`, `asrockrack` | `/UpdateService/upload` with `OemParameters` ImageType | `.ima`, `.ima_enc`, `.hpm`, `.bin` | the BMC restarts after a BMC flash; ASRock Rack unverified |
|
||
| `openbmc` | update-multipart (BMC Immediate, others OnReset) | signed `.tar` | |
|
||
| `xfusion` | upload to `/tmp/web`, then SimpleUpdate | `.hpm` | BMC, BIOS, CPLD |
|
||
| `kaytus` | upload, then OEM SimpleUpdate (FlashItem) | `.hpm`, `.bin` | from KAYTUS's own SDK; BMC and BIOS |
|
||
| `irmc` | OEM actions (FWUpdate, BiosUpdate) | iRMC `.bin`, BIOS `.UPC` | |
|
||
| `cimc` | not supported | | Cisco documents only SCP pulls |
|
||
|
||
Per machine, on the BMC record:
|
||
|
||
| Setting | Effect |
|
||
|---|---|
|
||
| `fw_method=multipart\|simple\|push` | skip the vendor plan and use this standard method |
|
||
| `fw_params={...}` | replace the plan's UpdateParameters (e.g. Supermicro Gen 13 BIOS preserve options) |
|
||
| `firmware_base=http://ADDR` | the address BMCs download from |
|
||
|
||
The lab Redfish emulator has an UpdateService of the same shape: iDRAC
|
||
profile multipart, iLO profile pull. The BMC version applies at once, the
|
||
BIOS at the next reset.
|
||
|
||
The lab has `lab/pve-vbmc/pve_redfish.py`, a Redfish service per Proxmox VM
|
||
with iDRAC, iLO and generic profiles, next to the IPMI emulator.
|
||
|
||
## IPv6
|
||
|
||
Subnets can be IPv6 (`rollcall subnet add fd00:50::/64 ...`). IPAM hands out
|
||
addresses from static ranges (walked from their start), deployments get them
|
||
in `network-config` with a separate default route per family, and the default
|
||
layout links every subnet of the boot NIC's VLAN, so an IPv6 prefix next to
|
||
the IPv4 one makes machines dual-stack. Interfaces with planned addresses
|
||
ignore router advertisements. dnsmasq serves router advertisements and
|
||
DHCPv6 on IPv6 subnets with dhcp on; the rack offers itself as default router
|
||
only when it is the subnet's gateway. Host files carry AAAA records, rack
|
||
controllers report their IPv6 addresses, the region certificate covers them,
|
||
and region, racks and clients work over IPv6 (`https://[fd00::1]`).
|
||
|
||
PXE stays IPv4: syslinux cannot netboot over IPv6, so the provisioning VLAN
|
||
needs an IPv4 subnet for commissioning and deployment.
|
||
|
||
Verified in the lab: Debian 13 (with a bond and VLAN), Rocky 9 and AlmaLinux 9
|
||
dual-stack on two segments, AAAA lookups through dnsmasq, the admin CLI over
|
||
`https://[fdc3:...]`.
|
||
|
||
## External IPAM (NetBox, Infoblox)
|
||
|
||
Rollcall works on its own, but at a site with an established network plan the
|
||
address authority is somewhere else. With `ipam` in `config.json` every
|
||
address Rollcall hands out is registered there first:
|
||
|
||
```json
|
||
"ipam": {
|
||
"provider": "netbox",
|
||
"url": "https://netbox.int.example.com",
|
||
"token_file": "/etc/rollcall/netbox.token",
|
||
"ca_file": "/etc/rollcall/netbox-ca.pem",
|
||
"dns_zone": "int.example.com",
|
||
"sync_interval": "5m"
|
||
}
|
||
```
|
||
|
||
- **Allocation asks the authority.** An address that exists in NetBox and is
|
||
not Rollcall's is skipped and the next candidate is tried, so Rollcall never
|
||
collides with a printer, a VIP or another team's reservation. If NetBox is
|
||
unreachable, allocation fails instead of guessing.
|
||
- **Ownership is explicit.** Rollcall's records carry the tag `rollcall` and the
|
||
machine ID in the custom field `rollcall_machine` (create both in NetBox;
|
||
on Infoblox these are an extensible attribute). Records without both are
|
||
never changed or deleted, only reported.
|
||
- **Releases reconcile.** Releasing, unassigning or deleting a machine frees
|
||
addresses locally and triggers a reconcile, which also runs every
|
||
`sync_interval`: missing records are created, names and owners fixed, and
|
||
Rollcall-owned records that are no longer in use deleted.
|
||
- **DNS follows the record.** `dns_name` is `<host>.<zone>` for a machine's
|
||
provisioning/mgmt address and `<host>-<iface>.<zone>` elsewhere (a subnet's
|
||
`domain` replaces the zone when it lies inside it; `pxe.local` stays local). A NetBox-to-DNS sync (PowerDNS here, a host
|
||
record on Infoblox) publishes it; Rollcall writes one object per address.
|
||
- **Prefixes are delegated in NetBox.** Tag a prefix `rollcall`;
|
||
`rollcall ipam prefixes` lists them and `rollcall ipam import` creates the
|
||
missing subnets (VLAN and rack are then set as usual).
|
||
|
||
```
|
||
rollcall ipam # provider, last reconcile
|
||
rollcall ipam sync -dry # what would change
|
||
rollcall ipam sync
|
||
rollcall ipam prefixes | import
|
||
```
|
||
|
||
### Devices in NetBox
|
||
|
||
With `device_sync` in the `ipam` block, the NetBox driver also keeps every
|
||
commissioned machine as a `dcim` device:
|
||
|
||
```json
|
||
"ipam": { "provider": "netbox", ..., "device_sync": { "site": "dc1", "role": "server" } }
|
||
```
|
||
|
||
- **What is written.** Name (node name, else the machine ID), serial, status
|
||
(deployed/booted/stateless are `active`, commissioned ones `staged`, failed
|
||
jobs `failed`), the device type from the DMI manufacturer and product, the
|
||
role (default `server`), interfaces with their MACs (NetBox 4.2+ MAC objects,
|
||
set as primary MAC), Rollcall's addresses assigned to those interfaces, and
|
||
the management address as primary IP. Machines still `new` are skipped.
|
||
- **Location comes from NetBox.** The machine's building, room and rack are
|
||
matched by name to a site, a location and a rack (in that site); the unit
|
||
becomes the rack position, the asset tag the device's. Rollcall never creates
|
||
sites, locations or racks; a machine whose building matches no site goes to
|
||
`device_sync.site`, which must exist. Manufacturers, device types and the role
|
||
are created when missing (a device needs them).
|
||
- **Ownership as for addresses.** Devices carry the tag `rollcall` and the
|
||
machine ID in `rollcall_machine` (enable the field for devices too). A device
|
||
of the same name without them is reported as a conflict and left alone;
|
||
Rollcall's devices whose machine is gone are deleted. Interfaces are removed
|
||
only when they carry the tag.
|
||
- **It runs after the address reconcile**, in the same loop; `rollcall ipam
|
||
sync` reports both (`devices` in the JSON).
|
||
|
||
The NetBox user needs `view/add/change/delete` on `dcim.device`,
|
||
`dcim.interface` and `dcim.macaddress`, `view/add` on `dcim.manufacturer`,
|
||
`dcim.devicetype` and `dcim.devicerole`, and `view` on `dcim.location` and
|
||
`dcim.rack`, on top of the IPAM permissions.
|
||
|
||
### Infoblox
|
||
|
||
The same contract over Infoblox WAPI (v2.12 by default; give a full
|
||
`.../wapi/vX.Y` URL for another):
|
||
|
||
```json
|
||
"ipam": {
|
||
"provider": "infoblox",
|
||
"url": "https://gm.example.com",
|
||
"user": "rollcall",
|
||
"password_file": "/etc/rollcall/infoblox.pw",
|
||
"ca_file": "/etc/rollcall/infoblox-ca.pem",
|
||
"dns_zone": "int.example.com",
|
||
"network_view": "default",
|
||
"dns_view": "default"
|
||
}
|
||
```
|
||
|
||
- **One host record per address.** Named by its DNS name (`rollcall-<address>`
|
||
without one) and configured for DNS in `dns_view`, so Infoblox publishes the
|
||
A/AAAA record itself; `"dns_view": "-"` keeps them IPAM-only. The zone must
|
||
exist in that view.
|
||
- **Ownership** is the extensible attribute `rollcall_machine` (define it in
|
||
the grid as String). Rollcall changes it with `extattrs+`, so a host's other
|
||
attributes stay; hosts without it are never changed or deleted.
|
||
- **Taken** is anything else on the address: another host, a lease, a fixed
|
||
address, a DHCP or reserved range, the network or broadcast address.
|
||
- **Delegated networks** carry the extensible attribute `rollcall` (any value;
|
||
define it as String); `Site` and `VLAN` attributes are shown when present.
|
||
- The WAPI user needs read/write on host records and read on networks and
|
||
IPv4/IPv6 addresses in the network view (and the DNS view when publishing).
|
||
|
||
Device sync (above) is NetBox-only.
|
||
|
||
The provider is an interface (reserve, owned records, update, delete,
|
||
delegated prefixes) with the NetBox and Infoblox drivers.
|
||
|
||
## DNS provider (PowerDNS, RFC 2136)
|
||
|
||
Without an IPAM authority whose DNS sync publishes names, Rollcall can write
|
||
its machines' A/AAAA records into the site's DNS itself. Names follow the
|
||
IPAM rule above (`<host>.<zone>`, `<host>-<iface>.<zone>`).
|
||
|
||
```json
|
||
"dns": {
|
||
"provider": "powerdns",
|
||
"zone": "int.example.com",
|
||
"url": "http://dns.int.example.com:8081",
|
||
"api_key_file": "/etc/rollcall/pdns.key"
|
||
}
|
||
```
|
||
|
||
```json
|
||
"dns": {
|
||
"provider": "rfc2136",
|
||
"zone": "int.example.com",
|
||
"server": "ns1.int.example.com:53",
|
||
"tsig_name": "rollcall-key",
|
||
"tsig_algorithm": "hmac-sha256",
|
||
"tsig_secret_file": "/etc/rollcall/tsig.secret"
|
||
}
|
||
```
|
||
|
||
- **powerdns** uses the HTTP API. Rollcall's RRsets carry a comment with
|
||
account `rollcall`; RRsets without it are never changed or deleted.
|
||
- **rfc2136** is a TSIG-signed dynamic update, for BIND, Knot, PowerDNS,
|
||
Windows DNS and Infoblox. Next to each name Rollcall writes a marker
|
||
`_rollcall.<name> TXT "heritage=rollcall"`. A change to an existing record
|
||
needs the marker as a prerequisite; a new record needs the name to be
|
||
free. The server checks both atomically, so a record someone else owns is
|
||
never overwritten. What Rollcall wrote is kept in the database, and each
|
||
sync queries those records. A record changed behind Rollcall's back is
|
||
repaired when the marker is still there. When someone else's record has
|
||
taken the name, it is reported as a conflict.
|
||
For BIND, grant the key the zone: `update-policy { grant rollcall-key zonesub ANY; };`.
|
||
|
||
A sync runs on every allocation and release and every `sync_interval`
|
||
(default 5m), with `ttl` defaulting to 300.
|
||
|
||
```
|
||
rollcall dns # provider, last sync, conflicts
|
||
rollcall dns sync -dry # what would change
|
||
rollcall dns sync
|
||
```
|
||
|
||
Conflicts and errors appear there, in the log and in `rollcall doctor`.
|
||
|
||
## 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.
|