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>
200 lines
14 KiB
HTML
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 & 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 && 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://<server>/</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 <admin_token></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>
|