1317ff636983ea9cdec6241aa55980fd4e98c75f
Operations are now clickable from the host detail page, linking to
/ops/{id} which shows the operation info, host link, duration, and
activity log filtered to that operation. Active operations can be
cancelled, which transitions the host to failed and releases the lock.
SSE activity events now include operation_id for real-time filtering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Provisioning
Central control plane for a Proxmox homelab cluster. Handles PXE booting bare metal, unattended Proxmox installation, cluster join, and host lifecycle management.
What it does
- Operator registers a host (MAC address + server type)
- Operator triggers "rebuild with Proxmox"
- Provisioning generates an ephemeral SSH key pair for this rebuild
- Host PXE boots → dnsmasq responds → iPXE chain-loads the Proxmox installer
- Installer fetches a per-host answer file (TOML) with the ephemeral public key
- Proxmox installs unattended → post-install webhook fires
- Host reboots → first-boot script phones home with IP + hardware ID
- Provisioning SSHes in using the ephemeral key →
pvecm addjoins the cluster - Ephemeral key removed from both the host and database
- Host registered in Infrastructure → marked ready
Admin dashboard shows real-time progress via SSE.
Host States
registered → pxe_ready → pxe_booted → installing → installed → first_boot → joining → ready
↓
failed
Deploy
Prerequisites
- Docker + Docker Compose on the target host
- Host must be on the same network as the bare-metal nodes (for PXE/DHCP)
- Registry access to
gitea.thewrightserver.net
No static SSH keys required — Provisioning generates ephemeral keys per rebuild automatically.
Setup
# Log in to the container registry
docker login gitea.thewrightserver.net
# Run the install script (creates /opt/provisioning with config templates)
curl -sf https://gitea.thewrightserver.net/josh/Provisioning/raw/branch/main/deploy/install.sh | bash
Or manually:
mkdir -p /opt/provisioning && cd /opt/provisioning
curl -sfO https://gitea.thewrightserver.net/josh/Provisioning/raw/branch/main/docker-compose.yml
curl -sf https://gitea.thewrightserver.net/josh/Provisioning/raw/branch/main/deploy/provisioning.example.yaml -o provisioning.yaml
curl -sf https://gitea.thewrightserver.net/josh/Provisioning/raw/branch/main/deploy/server-types.example.yaml -o server-types.yaml
Configure
Edit provisioning.yaml:
| Key | Description |
|---|---|
server.public_url |
LAN-reachable URL (e.g. http://192.168.1.100:8080) |
pxe.interface |
NIC name on the host (e.g. eth0, enp2s0) |
pxe.subnet |
LAN CIDR for proxy-DHCP |
proxmox.existing_node |
IP of any current cluster member |
proxmox.join_fingerprint |
From pvecm status on an existing node |
credentials.root_password_hash |
Generate with mkpasswd -m sha-512 |
infrastructure.base_url |
URL of the Infrastructure service |
infrastructure.server_type_map |
Maps local type keys to Infrastructure IDs |
Edit server-types.yaml with your actual hardware types:
server_types:
minisforum-ms-01:
display_name: "Minisforum MS-01"
boot_disk: "/dev/nvme0n1"
management_nic: "enp2s0"
gpu: false
hostname_prefix: "pve-ms"
minisforum-um790:
display_name: "Minisforum UM790 Pro"
boot_disk: "/dev/nvme0n1"
management_nic: "enp1s0"
gpu: true
hostname_prefix: "pve-um"
Run
docker compose up -d
Dashboard at http://<host>:8080.
Update
docker compose pull
docker compose up -d
API
Host Management
| Method | Path | Description |
|---|---|---|
| GET | /api/hosts |
List all hosts |
| GET | /api/hosts/{id} |
Get host details |
| POST | /api/hosts |
Register host (hostname, mac, server_type) |
| DELETE | /api/hosts/{id} |
Remove host |
| POST | /api/hosts/{id}/rebuild |
Start rebuild operation |
Boot Flow (called by PXE-booting hosts)
| Method | Path | Description |
|---|---|---|
| GET | /ipxe/{mac} |
iPXE boot script |
| POST | /api/boot/answer |
Proxmox answer file (TOML) |
| POST | /api/hosts/{id}/installed |
Post-install webhook |
| GET | /api/hosts/{id}/first-boot-script |
First-boot shell script |
| POST | /api/hosts/{id}/phone-home |
First-boot reports IP + hardware ID |
Dashboard
| Method | Path | Description |
|---|---|---|
| GET | / |
Host grid with live state tiles |
| GET | /hosts/{id} |
Host detail + operation history |
| GET | /events |
SSE stream |
Development
# Run tests
go test ./...
# Run locally (PXE disabled)
cp deploy/provisioning.example.yaml provisioning.yaml
cp deploy/server-types.example.yaml server-types.yaml
# Edit provisioning.yaml: set pxe.enabled=false, infrastructure.base_url=""
go run ./cmd/provisioning -config provisioning.yaml
# Build binary
make build
# Build Docker image locally
make docker
Architecture
cmd/provisioning/ Entry point, wiring, shutdown
internal/
config/ YAML config + hot-reloaded server types (fsnotify)
db/ SQLite (WAL mode, embedded migrations)
model/ Domain types (Host, Operation, Image, ServerType)
store/ SQL stores (hosts, operations, locks, images)
statemachine/ Table-driven host state machine
events/ SSE fan-out hub
pxe/ dnsmasq supervisor, iPXE scripts, answer files, first-boot
orchestrator/ Lifecycle driver (ephemeral keys, cluster join, infra registration)
infra/ Infrastructure API client
api/ HTTP handlers (JSON API + HTML dashboard)
httpserver/ chi router assembly
web/ Embedded static assets (CSS, JS)
CI/CD
Gitea Actions workflow (.gitea/workflows/build.yml):
- Runs
go testandgo veton every push to main - Builds Docker image and pushes to
gitea.thewrightserver.net/josh/provisioning:latest
Description
Languages
Go
87.5%
CSS
7.7%
JavaScript
3.5%
Shell
0.7%
Dockerfile
0.4%
Other
0.2%