Files
rollcall/site/index.html
T
ilkerandClaude Fable 5.1 d9ae89b295
CI / agent (push) Successful in 3s
CI / site (push) Successful in 2s
CI / go (push) Successful in 1m15s
Rollcall: bare-metal commissioning server, agent, image build, lab and CI
Go server (API, embedded web UI, CLI) backed by PostgreSQL with AES-GCM
sealed BMC secrets; Debian live commissioning agent and live-build recipe;
Proxmox lab with pve-vbmc virtual BMCs; Gitea workflows for CI, releases
and deployment; project website.

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

200 lines
14 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rollcall</title>
<meta name="description" content="Rollcall: MAAS-style bare-metal commissioning in one static binary. PXE boot, inventory, BMC account, PostgreSQL, web UI, CLI.">
<style>
:root {
--bg: #f7f8fa; --panel: #ffffff; --fg: #15181e; --muted: #5f6b7a; --line: #e2e6eb;
--accent: #1d4ed8; --accent-soft: #e8efff; --ok: #15803d; --code-bg: #0f172a; --code-fg: #e2e8f0;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0d1117; --panel: #161b22; --fg: #e6edf3; --muted: #9aa7b4; --line: #2a323c;
--accent: #5b8def; --accent-soft: #15223d; --ok: #3fb950; --code-bg: #0a0f1a; --code-fg: #d7e0ea;
}
}
:root[data-theme="dark"] {
--bg: #0d1117; --panel: #161b22; --fg: #e6edf3; --muted: #9aa7b4; --line: #2a323c;
--accent: #5b8def; --accent-soft: #15223d; --ok: #3fb950; --code-bg: #0a0f1a; --code-fg: #d7e0ea;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 0 16px; }
nav { display: flex; align-items: center; justify-content: space-between; padding: 14px 0; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); z-index: 5; }
nav .brand { font-weight: 700; letter-spacing: .01em; color: var(--fg); display: flex; align-items: center; gap: 10px; }
nav .brand svg { width: 26px; height: 26px; }
nav ul { list-style: none; display: flex; gap: 18px; margin: 0; padding: 0; font-size: 14px; }
@media (max-width: 640px) { nav ul { display: none; } }
header.hero { padding: 64px 0 40px; }
.hero h1 { font-size: clamp(32px, 5vw, 48px); line-height: 1.1; margin: 0 0 14px; letter-spacing: -.02em; }
.hero p.lead { font-size: 19px; color: var(--muted); max-width: 720px; margin: 0 0 24px; }
.cta { display: flex; gap: 12px; flex-wrap: wrap; }
.btn { display: inline-block; padding: 10px 16px; border-radius: 8px; border: 1px solid var(--line); background: var(--panel); color: var(--fg); font-weight: 600; font-size: 15px; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn:hover { text-decoration: none; filter: brightness(1.05); }
.badges { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 20px; }
.badge { font-size: 12px; padding: 3px 9px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); font-weight: 600; }
section { padding: 36px 0; border-top: 1px solid var(--line); }
h2 { font-size: 24px; margin: 0 0 6px; letter-spacing: -.01em; }
.sub { color: var(--muted); margin: 0 0 22px; }
.flow { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; counter-reset: step; }
.flow .step { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px; position: relative; }
.flow .step::before { counter-increment: step; content: counter(step); position: absolute; top: -10px; left: 12px; background: var(--accent); color: #fff; font-size: 12px; font-weight: 700; width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; }
.flow .step h3 { margin: 6px 0 4px; font-size: 15px; } .flow .step p { margin: 0; color: var(--muted); font-size: 14px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px; }
.card h3 { margin: 0 0 6px; font-size: 16px; } .card p { margin: 0; color: var(--muted); font-size: 14.5px; }
pre { background: var(--code-bg); color: var(--code-fg); padding: 14px 16px; border-radius: 10px; overflow-x: auto; font: 13px/1.5 var(--mono); margin: 0; }
code { font-family: var(--mono); font-size: .92em; background: var(--accent-soft); padding: 1px 5px; border-radius: 4px; }
pre code { background: none; padding: 0; font-size: inherit; }
table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; font-size: 14px; }
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; }
td code { white-space: nowrap; }
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } @media (max-width: 760px) { .two { grid-template-columns: 1fr; } }
.note { border-left: 3px solid var(--accent); padding: 8px 14px; background: var(--accent-soft); border-radius: 0 8px 8px 0; font-size: 14.5px; }
footer { padding: 28px 0 40px; color: var(--muted); font-size: 14px; border-top: 1px solid var(--line); }
.term { font-family: var(--mono); font-size: 13px; }
.term .ok { color: var(--ok); }
</style>
</head>
<body>
<div class="wrap">
<nav>
<a class="brand" href="#">
<svg viewBox="0 0 32 32" aria-hidden="true"><rect x="3" y="4" width="26" height="6" rx="2" fill="var(--accent)"/><rect x="3" y="13" width="26" height="6" rx="2" fill="var(--accent)" opacity=".7"/><rect x="3" y="22" width="26" height="6" rx="2" fill="var(--accent)" opacity=".4"/><circle cx="8" cy="7" r="1.6" fill="#fff"/><circle cx="8" cy="16" r="1.6" fill="#fff"/><circle cx="8" cy="25" r="1.6" fill="#fff"/></svg>
Rollcall
</a>
<ul>
<li><a href="#how">How it works</a></li>
<li><a href="#features">Features</a></li>
<li><a href="#quickstart">Quick start</a></li>
<li><a href="#api">API</a></li>
<li><a href="https://gitea.mynodes.xyz/ilker/rollcall">Source</a></li>
</ul>
</nav>
<header class="hero">
<h1>Bare-metal commissioning,<br>one static binary.</h1>
<p class="lead">Rollcall does what MAAS does when a new server first shows up: PXE-boot an ephemeral image, take a full hardware inventory, create an administrator account on the BMC, and remember the machine. No cloud controller, no agents on your OS, no fleet of services.</p>
<div class="cta">
<a class="btn primary" href="#quickstart">Quick start</a>
<a class="btn" href="https://gitea.mynodes.xyz/ilker/rollcall">Repository</a>
</div>
<div class="badges">
<span class="badge">Go · single binary</span><span class="badge">PostgreSQL</span><span class="badge">Debian live image</span>
<span class="badge">IPMI / ipmitool</span><span class="badge">InfiniBand aware</span><span class="badge">Web UI + CLI + API</span>
</div>
</header>
<section id="how">
<h2>How it works</h2>
<p class="sub">A machine goes from unknown to <em>ready</em> in one boot, without anyone typing anything.</p>
<div class="flow">
<div class="step"><h3>PXE boot</h3><p>dnsmasq hands out DHCP and TFTP. Unknown MACs land on the default pxelinux menu, which starts commissioning after 8 seconds.</p></div>
<div class="step"><h3>Ephemeral image</h3><p>A Debian 13 live image (full kernel, non-free firmware, IB and NVMe tooling) is fetched over HTTP and runs from RAM. Disks are never touched.</p></div>
<div class="step"><h3>Inventory</h3><p>DMI, DIMMs, CPUs, disks with SMART, NVMe, NICs with firmware and link, InfiniBand HCAs, GPUs, every PCI device, LLDP switch ports, TPM.</p></div>
<div class="step"><h3>BMC account</h3><p>In-band <code>ipmitool</code> finds the LAN channel and creates <code>commissioner</code> with ADMINISTRATOR rights and a random password.</p></div>
<div class="step"><h3>Report &amp; power off</h3><p>Everything is posted to the server. The password is sealed with AES-256-GCM before it reaches PostgreSQL. The machine powers off.</p></div>
<div class="step"><h3>Known from now on</h3><p>A per-MAC PXE file stops future boots of the image. Power and boot device are controlled through the API, UI or CLI, with ipmitool running on the server.</p></div>
</div>
</section>
<section id="features">
<h2>Features</h2>
<p class="sub">Small surface, enterprise hardware in mind.</p>
<div class="grid">
<div class="card"><h3>One binary, everything inside</h3><p>HTTP API, embedded web UI, CLI subcommands, schema migrations and PXE bookkeeping ship as a single static Go executable. Deploy with <code>scp</code>.</p></div>
<div class="card"><h3>Secrets stay on the server</h3><p>BMC passwords are encrypted at rest with a local master key, never returned by the API, and only ever used by ipmitool inside the server process.</p></div>
<div class="card"><h3>Real hardware first</h3><p>The image carries firmware for Broadcom, QLogic, Intel and Mellanox NICs, <code>rdma-core</code>, <code>infiniband-diags</code>, <code>mstflint</code>, <code>nvme-cli</code>, <code>smartmontools</code>, <code>lldpd</code>.</p></div>
<div class="card"><h3>Virtual machines too</h3><p>No in-band IPMI? Rollcall can map MACs to known BMCs. The repo ships <code>pve-vbmc</code>, an IPMI BMC emulator for Proxmox VMs, so the whole flow runs in a lab.</p></div>
<div class="card"><h3>Two tokens, two audiences</h3><p>The live image authenticates with an agent token baked into the kernel command line; people and automation use a separate admin bearer token.</p></div>
<div class="card"><h3>Re-commission on demand</h3><p>Drop the PXE override, set <code>bootdev pxe</code> and power-cycle through the BMC with one click or <code>rollcall recommission ID -now</code>.</p></div>
</div>
</section>
<section id="quickstart">
<h2>Quick start</h2>
<p class="sub">On the host that already runs your DHCP/TFTP for the provisioning network.</p>
<div class="two">
<div>
<pre><code># 1. database
sudo -u postgres psql \
-c "CREATE ROLE rollcall LOGIN PASSWORD 'secret';" \
-c "CREATE DATABASE rollcall OWNER rollcall;"
# 2. config
rollcall keygen # admin_token
rollcall keygen # agent_token
sudo install -d -m 0750 /etc/rollcall
sudo cp config.json /etc/rollcall/config.json
# 3. install binary, unit, pxelinux menu
cd deploy &amp;&amp; sudo ./install.sh
# 4. build the live image (~8 min)
sudo ../agent/build-image.sh</code></pre>
</div>
<div>
<pre class="term"><code>$ rollcall list
ID STATE MODEL RAM NIC IB BMC
m-2ef1dab828c8 ready Dell PowerEdge R760 1.0T 4 2 10.4.0.21 (inband,ok)
m-9c01a3bd7e12 ready Supermicro SYS-421GE 2.0T 6 4 10.4.0.22 (inband,ok)
$ rollcall power m-2ef1dab828c8 status
<span class="ok">Chassis Power is on</span>
$ rollcall show m-2ef1dab828c8 | head -3
m-2ef1dab828c8 state=ready
Dell Inc. PowerEdge R760 S/N 7XK2Q94
CPU: Intel Xeon Platinum 8468 sockets=2 ...</code></pre>
<p class="note" style="margin-top:12px">Then point the UI at <code>http://&lt;server&gt;/</code> and paste the admin token. Machines appear as they boot.</p>
</div>
</div>
</section>
<section id="api">
<h2>API</h2>
<p class="sub">JSON over HTTP. Admin calls carry <code>Authorization: Bearer &lt;admin_token&gt;</code>; the agent sends <code>X-Rollcall-Token</code>.</p>
<table>
<thead><tr><th>Method</th><th>Path</th><th>Who</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td>POST</td><td><code>/api/v1/commission</code></td><td>agent</td><td>Inventory upload from the live image</td></tr>
<tr><td>GET</td><td><code>/api/v1/bmc-hint?macs=</code></td><td>agent</td><td>Out-of-band BMC hint for machines without in-band IPMI</td></tr>
<tr><td>GET</td><td><code>/api/v1/machines</code></td><td>admin</td><td>List with summaries (CPU, RAM, disks, NICs, IB, GPUs, BMC state)</td></tr>
<tr><td>GET</td><td><code>/api/v1/machines/{id}</code></td><td>admin</td><td>Full record: inventory, BMC (without secret), events</td></tr>
<tr><td>POST</td><td><code>/api/v1/machines/{id}/power</code></td><td>admin</td><td><code>{"action":"on|off|soft|cycle|reset|status"}</code></td></tr>
<tr><td>POST</td><td><code>/api/v1/machines/{id}/bootdev</code></td><td>admin</td><td><code>{"device":"pxe|disk|cdrom|bios|none"}</code></td></tr>
<tr><td>POST</td><td><code>/api/v1/machines/{id}/recommission[?now=1]</code></td><td>admin</td><td>Boot the image again on next PXE, or right now via the BMC</td></tr>
<tr><td>DELETE</td><td><code>/api/v1/machines/{id}</code></td><td>admin</td><td>Forget the machine</td></tr>
</tbody>
</table>
</section>
<section id="inventory">
<h2>What the inventory contains</h2>
<p class="sub">Collected by <code>rollcall-agent.py</code> inside the live image and stored as JSON per machine.</p>
<div class="grid">
<div class="card"><h3>System</h3><p>Manufacturer, product, serial, UUID, SKU, baseboard, chassis, asset tag, BIOS vendor/version/date, TPM.</p></div>
<div class="card"><h3>Compute</h3><p>CPU model, sockets, cores, threads, NUMA nodes, caches, virtualization flags; memory total plus every DIMM slot with size, type, speed, vendor, part and serial.</p></div>
<div class="card"><h3>Storage</h3><p>All block devices with model, serial, bus, rotational flag, sector sizes, SMART health and firmware, NVMe list, storage controllers from PCI.</p></div>
<div class="card"><h3>Network</h3><p>NICs with driver, firmware, speed, link state, permanent MAC, PCI address, NUMA node, SR-IOV capacity; LLDP neighbours to map switch ports.</p></div>
<div class="card"><h3>InfiniBand</h3><p>HCAs with node and port GUIDs, firmware, board id, per-port state, physical state, rate, LID and link layer.</p></div>
<div class="card"><h3>BMC</h3><p>LAN channel, IP source, MAC, gateway, VLAN, firmware, FRU, user table before commissioning, created account and verification result.</p></div>
</div>
</section>
<footer>
Rollcall is developed in the open at <a href="https://gitea.mynodes.xyz/ilker/rollcall">gitea.mynodes.xyz/ilker/rollcall</a>.
Built with Go, Debian live-build, dnsmasq and ipmitool.
</footer>
</div>
</body>
</html>