# Operations Center Guide for IncusOS This guide covers deploying and using Operations Center (OC) by FuturFusion -- a centralized management layer for IncusOS/Incus deployments. OC handles provisioning, clustering, inventory, and updates through a REST API, CLI, and web UI. All commands tested with Operations Center v0.3.0 and IncusOS build 202602200553 on Proxmox VE 9.1.5 (nested virtualization on Intel). --- ## Overview Operations Center is fundamentally different from managing Incus clusters manually via `incus` CLI remotes. Key differences: | | Manual (incus CLI) | Operations Center | |---|---|---| | Node provisioning | `incusos-proxmox` + seed archives | OC-provisioned ISOs + `incusos-proxmox --iso` (hybrid) | | Cluster formation | `incus cluster enable` + `incus cluster join` | OC orchestrates via CLI/API/web UI | | Node registration | Manual remote add | Automatic self-registration on first boot | | Inventory | `incus list` per remote | Centralized view across all clusters | | Updates | Manual per-node | OC manages update channels and rollouts | | Brownfield adoption | N/A (nodes are standalone) | **Not supported** -- nodes must be OC-provisioned | **Key constraint**: OC only manages nodes it has provisioned. There is no "adopt existing node" workflow. Nodes must boot from an OC-generated ISO containing a provisioning token, then self-register on first boot. --- ## Prerequisites ### Install OC CLI ```bash # Check for latest release at https://github.com/FuturFusion/operations-center/releases wget https://github.com/FuturFusion/operations-center/releases/download/v0.3.0/operations-center_linux_x86_64 chmod +x operations-center_linux_x86_64 sudo mv operations-center_linux_x86_64 /usr/local/bin/operations-center operations-center --version # Note: use --version, not 'version' subcommand ``` ### Set up OC client certificates OC uses the same TLS client certificate format as Incus: ```bash mkdir -p ~/.config/operations-center cp ~/.config/incus/client.crt ~/.config/operations-center/ cp ~/.config/incus/client.key ~/.config/operations-center/ ``` ### Generate PKCS#12 cert for browser access The OC web UI requires a client certificate for authentication: ```bash openssl pkcs12 -export \ -inkey ~/.config/incus/client.key \ -in ~/.config/incus/client.crt \ -out ~/.config/incus/client.pfx # Import client.pfx into Firefox: Settings -> Certificates -> Your Certificates -> Import ``` ### Verify with doctor check ```bash ./incusos/incusos-proxmox --doctor # Should report OC CLI as installed ``` --- ## Part 1: Deploy Operations Center Server ### Deployment config Use `incusos/examples/lab-oc-deploy.yaml` -- deploys only the OC server (Incus nodes will be provisioned by OC itself): ```yaml defaults: cores: 2 memory: 4096 disk: 50 start_vmid: 920 vms: - name: oc-server app: operations-center apply_defaults: true ``` ### Deploy ```bash ./incusos/incusos-proxmox --dry-run incusos/examples/lab-oc-deploy.yaml ./incusos/incusos-proxmox --yes incusos/examples/lab-oc-deploy.yaml ./incusos/incusos-proxmox --status incusos/examples/lab-oc-deploy.yaml ``` Note the OC IP address from `--status` output. ### Set up OC CLI remote ```bash operations-center remote add oc-lab https://:8443 --auth-type tls operations-center remote switch oc-lab operations-center remote list ``` ### Verify browser access Navigate to `https://:8443` in Firefox (with client.pfx imported). The OC web UI should load and show the dashboard. ### Wait for updates to download OC automatically downloads IncusOS update packages from upstream. At least one update must be in `ready` state before provisioned ISOs can be generated: ```bash operations-center provisioning update list # Wait until at least one update shows "ready" status # This may take several minutes on first boot ``` Also via REST API: ```bash curl -sk --cert ~/.config/incus/client.crt \ --key ~/.config/incus/client.key \ https://:8443/1.0/provisioning/updates ``` --- ## Part 2: Provision Incus Node ISOs ### Create a provisioning token ```bash # CLI operations-center provisioning token add \ --description "Lab cluster nodes" \ --uses 5 # Note the token UUID from output operations-center provisioning token list operations-center provisioning token show ``` Via REST API: ```bash curl -sk --cert ~/.config/incus/client.crt \ --key ~/.config/incus/client.key \ -X POST https://:8443/1.0/provisioning/tokens \ -d '{"description": "Lab cluster nodes (API)", "uses": 5}' ``` ### Create a token seed for Proxmox VMs (tested) OC supports **token seeds** -- named, reusable pre-seed configs attached to a token. The YAML format uses section-level keys (`install:`, `applications:`, etc.) -- NOT the flat format from standard seed archives. ```yaml # /tmp/oc-preseed.yaml -- structured format required for token seeds install: version: "1" force_install: true force_reboot: true ``` **Important**: a flat format (`version: "1"` at root) maps all fields to empty `{}` -- they don't get assigned to any section. The structured format with `install:` as the top-level key is required. ```bash operations-center provisioning token seed add proxmox-preseed \ /tmp/oc-preseed.yaml \ --description "Force reboot for Proxmox VMs" operations-center provisioning token seed list operations-center provisioning token seed show proxmox-preseed ``` ### Download the OC-provisioned ISO (tested) Use `token seed get-image` for the cleanest approach (pre-seed embedded in ISO): ```bash operations-center provisioning token seed get-image proxmox-preseed \ /tmp/IncusOS-oc.iso --type iso --architecture x86_64 ls -lh /tmp/IncusOS-oc.iso # ~3.4 GB ``` Alternative without token seeds (inline pre-seed): ```bash operations-center provisioning token get-image /tmp/IncusOS-oc.iso \ /tmp/oc-preseed.yaml --type iso --architecture x86_64 ``` --- ## Part 3: Deploy Incus Nodes (Hybrid Approach -- Tested) **The hybrid approach** uses `incusos-proxmox --iso` to deploy nodes from an OC-provisioned ISO. This combines the best of both worlds: - **OC**: auto-registration token embedded in the boot ISO - **incusos-proxmox**: VM creation, per-node SEED_DATA (hostname, force_reboot), install monitoring, media cleanup, IP detection, remote setup The OC token lives in the boot ISO (ide2). Our SEED_DATA (ide3) provides force_reboot, hostname, and app selection. IncusOS reads both sources and merges them -- they coexist without conflict. ### Deployment config ```yaml # incusos/examples/lab-oc-nodes.yaml defaults: cores: 4 memory: 8192 disk: 50 start_vmid: 400 # Use high VMIDs to avoid collisions with VMs outside pool vms: - name: oc-node-01 app: incus apply_defaults: false # OC handles storage/network via Terraform - name: oc-node-02 app: incus apply_defaults: false - name: oc-node-03 app: incus apply_defaults: false ``` **VMID note**: if your Proxmox API token is scoped to a resource pool, it cannot see VMs outside that pool. Use `start_vmid` above existing VM ranges to avoid VMID collisions with invisible VMs. ### Deploy (tested) ```bash ./incusos/incusos-proxmox --iso /tmp/IncusOS-oc.iso --dry-run \ incusos/examples/lab-oc-nodes.yaml ./incusos/incusos-proxmox --iso /tmp/IncusOS-oc.iso --yes \ incusos/examples/lab-oc-nodes.yaml ./incusos/incusos-proxmox --status incusos/examples/lab-oc-nodes.yaml ``` `incusos-proxmox` handles everything: VM creation, ISO upload, SEED_DATA generation (force_reboot + hostname per node), install monitoring via blockstat polling, media removal, boot order switch, IP detection, and incus remote setup. ### Verify OC auto-registration (tested) After nodes boot from disk, they auto-register with OC within ~30 seconds: ```bash operations-center provisioning server list ``` Nodes register with their hostname (set via SEED_DATA), so no renaming needed. Each node appears with type `incus`, status `ready`, and its connection URL. **Tested result**: all 3 nodes auto-registered with correct hostnames, correct IPs, type `incus`, status `ready`. No manual intervention needed. --- ## Part 4: Form Cluster via Operations Center ### Prepare application config Create an Incus preseed that injects the client certificate into the cluster (so the `incus` CLI can connect directly): ```yaml # /tmp/oc-app-config.yaml certificates: - type: client name: lab-client-cert certificate: |- -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- ``` ### Form cluster via OC CLI (tested) ```bash operations-center provisioning cluster add oc-cluster \ https://:8443 \ --server-names oc-node-01,oc-node-02,oc-node-03 \ --server-type incus \ --application-seed-config /tmp/oc-app-config.yaml ``` OC orchestrates the full cluster formation: 1. Sets `core.https_address` to each node's specific IP 2. Enables clustering on the first node 3. Joins remaining nodes (handles storage pool + network creation) 4. Applies the application seed (injects client cert) 5. Runs Terraform/OpenTofu for post-cluster configuration ### apply_defaults: false (tested, recommended) **Use `apply_defaults: false`** for OC-managed nodes. This is now the tested and recommended approach. With `apply_defaults: false`: - Nodes still listen on port 8443 and trust preseed certificates - The underlying ZFS dataset (`local/incus`) still exists - But no Incus storage pool, network, or profile devices are created - OC's Terraform handles all resource creation cleanly during cluster formation **Tested result**: cluster forms cleanly with `apply_defaults: false`. OC's Terraform creates the storage pool (`local`), networks (`incusbr0`, `meshbr0`), and injects the client certificate via `--application-seed-config`. The only Terraform error is "Certificate already in trust store" when the cert was also injected via SEED_DATA -- avoid this by only injecting the cert through the application seed config, not the SEED_DATA. ### apply_defaults: true (also works, with caveats) When using `apply_defaults: true`, each node already has a `local` ZFS storage pool, `incusbr0` network bridge, and the client certificate. OC's Terraform then fails with: - "Certificate already in trust store" - "Network is not in pending state" - "Storage pool is not in pending state" **The cluster itself forms successfully** despite these Terraform errors. However, `cluster artifact list` will be empty since Terraform didn't complete. ### Verify cluster (tested) ```bash # Via OC operations-center provisioning cluster list operations-center provisioning cluster show oc-cluster # Via Incus (direct cluster access) # NOTE: clustering regenerates TLS certificates, so re-add remotes: incus remote remove oc-node-01 # if existing remote has wrong cert incus remote add oc-node-01 https://:8443 --accept-certificate incus cluster list oc-node-01: ``` ### Cluster artifacts OC generates Terraform/OpenTofu artifacts on successful cluster formation: ```bash operations-center provisioning cluster artifact list oc-cluster # Empty if Terraform failed (see apply_defaults conflict above) ``` --- ## Part 5: OC Management Capabilities (Tested) ### Inventory browsing OC inventory is **not real-time** -- it requires an explicit `cluster resync` to pick up changes made via the `incus` CLI: ```bash # Force sync first operations-center provisioning cluster resync oc-cluster # Then query (use UUIDs for instance show, not names) operations-center inventory instance list operations-center inventory storage-pool list operations-center inventory network list operations-center inventory profile list operations-center inventory project list operations-center inventory image list ``` **Tested**: after creating workloads via `incus launch`, OC inventory was empty until `cluster resync` was run. After resync, all instances, storage pools, networks, profiles, projects, and images appeared correctly. ### Inventory sync latency - Creating workloads via `incus` CLI: not visible in OC until resync - After resync: ~5 seconds for inventory to populate - Live migration: location change not reflected until resync - Evacuation: instance relocation not reflected until resync - OC `inventory instance show ` shows: name, project, cluster, server, timestamp ### Server management (tested) ```bash operations-center provisioning server list # Lists all servers + OC itself operations-center provisioning server show operations-center provisioning server resync operations-center provisioning server system reboot # WARNING: see below operations-center provisioning server system poweroff ``` **WARNING -- OC reboot breaks OC-managed nodes on Proxmox**: the `system reboot` command sends a guest-level (ACPI) reboot. On standalone IncusOS nodes, guest reboot works perfectly (tested: simultaneous 3-node reboot, all recover in ~50s with containers and data intact). The failure is **OC-specific**: the OC agent runs on every boot and can fail with: - "failed to parse update frequency - invalid crontab expression" (OC pushed invalid update config) - "error from operations center: forbidden: failed creating server: constraint violation" (OC trying to re-register an existing server) - Incus stuck at "starting application name=incus" (cluster peers are in boot loops, so quorum cannot form) These failures cause boot loops or hung services. Even Proxmox stop/start does not recover because the OC state on disk remains broken. The only fix is to destroy and redeploy the VM. **Safe reboot**: Proxmox stop/start and guest reboot both work on standalone (non-OC) IncusOS nodes. Avoid OC `system reboot` on OC-managed nodes. The proper lifecycle for maintenance is: 1. `incus cluster evacuate : --force` 2. Proxmox stop the VM 3. Proxmox start the VM 4. Wait for IncusOS to boot (~60-90s) 5. `incus cluster restore : --force` ### IncusOS scrub_schedule fix via API (tested) IncusOS has an intermittent bug (~50% of deploys) where `state.txt` is written with an empty `scrub_schedule` field. The `registerJobs()` function calls `gocron.IsValid("")` which fails, crashing the daemon in a loop. The REST API is briefly reachable during each crash cycle (Incus starts before the scheduler). **Fix via IncusOS REST API** (through the Incus proxy at `/os/` prefix): ```bash # Check current storage config curl -sk --cert ~/.config/incus/client.crt \ --key ~/.config/incus/client.key \ https://:8443/os/1.0/system/storage # If scrub_schedule is empty, fix it curl -sk --cert ~/.config/incus/client.crt \ --key ~/.config/incus/client.key \ -X PUT -H "Content-Type: application/json" \ -d '{"config":{"scrub_schedule":"0 4 * * 0"}}' \ https://:8443/os/1.0/system/storage ``` **Key details**: - The `/os/` prefix is required — this proxies to the IncusOS daemon API - PATCH returns 501 (not implemented); use PUT instead - `incus admin os system storage edit` fails because it reads the current (corrupt) config and validates it before allowing edits - `incusos-proxmox` now includes `fix_scrub_schedule()` which proactively fixes this on every deployed node (safe — returns early if already set) - Filed upstream as IncusOS issue #843 ### Node failure and recovery (tested) Proxmox hard-stop simulates node crash. Incus cluster detects failure via heartbeat timeout (~30-40s). Node auto-rejoins after Proxmox restart (~60s boot): ```bash # Simulate crash: Proxmox API stop curl -sk -X POST ".../nodes/pve/qemu//status/stop" ... # After ~40s: incus cluster list shows OFFLINE with heartbeat info # Restart: Proxmox API start curl -sk -X POST ".../nodes/pve/qemu//status/start" ... # After ~60s boot: node auto-rejoins cluster (ONLINE) # Containers on the node auto-start after recovery ``` **OC does not detect node failures**: `provisioning server show` still shows `Status: ready` for an OFFLINE/crashed node. `Last Seen` timestamp is stale. **OC does not track Incus cluster member state**: the server list always shows `ready` status even when the Incus cluster reports `EVACUATED`. ### Server edit commands Network and storage have `edit` subcommands (no `show`): ```bash operations-center provisioning server system network edit operations-center provisioning server system storage edit ``` ### Update management (tested) ```bash operations-center provisioning update list # Shows all available IncusOS versions operations-center provisioning update show # Full file list with sizes operations-center provisioning update refresh # Check for new updates ``` Updates show comprehensive file lists including sysexts: incus, operations-center, gpu-support, incus-ceph, incus-linstor, migration-manager, debug. ### System configuration (tested) ```bash operations-center system settings show # log_level operations-center system security show # OIDC, OpenFGA, ACME, trusted certs operations-center system updates show # Update source, filter expressions, signature CA ``` Note: `system certificate` only has `set` (no `show`). `system network` has `show` and `edit`. ### Admin / IncusOS management (tested) ```bash operations-center admin os show # hostname, os_version, uptime operations-center admin os application list operations-center admin os application show operations-center admin os service list # iscsi, lvm, multipath, nvme, ovn, tailscale, usbip operations-center admin os debug log ``` ### Cluster operations (tested) ```bash operations-center provisioning cluster resync operations-center provisioning cluster rename operations-center provisioning cluster update --connection-url operations-center provisioning cluster update-certificate operations-center provisioning cluster artifact list operations-center provisioning cluster factory-reset ``` ### Cluster templates ```bash operations-center provisioning cluster-template add --description "..." operations-center provisioning cluster-template list operations-center provisioning cluster-template show operations-center provisioning cluster-template remove ``` --- ## Part 6: Cluster Lifecycle (Tested) ### Live migration visibility through OC (tested) ```bash # Prepare VM for live migration incus stop oc-node-01:test-vm-01 incus config set oc-node-01:test-vm-01 migration.stateful=true limits.cpu=0-1 incus config device add oc-node-01:test-vm-01 root disk path=/ pool=local size.state=2GiB incus start oc-node-01:test-vm-01 # Migrate incus move oc-node-01:test-vm-01 --target oc-node-03 # OC does NOT auto-update -- resync required operations-center provisioning cluster resync oc-cluster operations-center inventory instance list # now shows new location ``` **Tested**: ~141 MB/s migration speed. OC inventory reflected the location change only after explicit `cluster resync`. ### Cluster evacuation through OC (tested) ```bash incus cluster evacuate oc-node-01:oc-node-01 --force # OC server list still shows oc-node-01 as "ready" (doesn't track EVACUATED state) operations-center provisioning cluster resync oc-cluster operations-center inventory instance list # shows workloads moved to other nodes incus cluster restore oc-node-01:oc-node-01 --force ``` ### Node replacement lifecycle (tested) Full procedure tested after OC reboot broke a node: ```bash # 1. Force-remove dead node from Incus cluster printf "yes\n" | incus cluster remove oc-node-01:oc-node-03 --force # 2. Destroy broken VM via Proxmox (stop + delete) # 3. Redeploy via incusos-proxmox --iso (single-node config) # Use apply_defaults: false for OC-managed nodes ./incusos/incusos-proxmox --iso /tmp/IncusOS-oc.iso --yes /tmp/redeploy-node-03.yaml # 4. New node auto-registers with OC (same hostname, new IP) # NOTE: OC shows stale entry for old node-03. Stale entry can't be removed # while OC thinks it's in a cluster. Resync doesn't fix stale entries. # 5. Manual cluster join (since node was force-removed) incus config set oc-node-03: core.https_address :8443 # With apply_defaults: false, no cleanup needed (no pool/network to delete) # Generate token and join incus cluster add oc-node-01:oc-node-03 printf '\n\nyes\nlocal/incus\nlocal/incus\n' | incus cluster join oc-node-01: oc-node-03: # Note: extra \n for meshbr0 tunnel.mesh.interface prompt (added by OC clustering) # 6. Fix remote after cert change incus remote remove oc-node-03 incus remote add oc-node-03 https://:8443 --accept-certificate ``` ### Node failure simulation (tested) Proxmox hard-stop simulates node crash. Incus detects via heartbeat (~40s): ```bash # Simulate crash curl -sk -X POST ".../nodes/pve/qemu//status/stop" ... # incus cluster list shows OFFLINE after ~40s # Restart curl -sk -X POST ".../nodes/pve/qemu//status/start" ... # Node auto-rejoins after ~60s boot. Containers auto-start. # OC still shows "ready" (does not detect node failures) ``` ### Factory reset (not tested) ```bash # WARNING: destructive operations-center provisioning cluster factory-reset oc-cluster ``` --- ## OC CLI Command Reference ### Remote management | Command | Description | |---------|-------------| | `operations-center remote add --auth-type tls` | Add OC remote | | `operations-center remote switch ` | Set default remote | | `operations-center remote list` | List configured remotes | | `operations-center remote remove ` | Remove a remote | ### Provisioning tokens | Command | Description | |---------|-------------| | `provisioning token add --description "..." --uses N --lifetime 168h` | Create token | | `provisioning token list` | List tokens | | `provisioning token show ` | Token details | | `provisioning token remove ` | Delete token | | `provisioning token seed add --description "..."` | Attach named seed | | `provisioning token seed list ` | List token seeds | | `provisioning token seed show ` | Show seed details | | `provisioning token seed get-image --type iso --architecture x86_64` | ISO from seed | | `provisioning token get-image [preseed] --type iso --architecture x86_64` | ISO from inline preseed | ### Provisioning servers | Command | Description | |---------|-------------| | `provisioning server list` | List registered servers (includes OC itself) | | `provisioning server show ` | Server details + certificate | | `provisioning server rename ` | Rename server | | `provisioning server resync ` | Resync single server | | `provisioning server remove ` | Remove (fails if in cluster) | | `provisioning server edit ` | Edit server (interactive) | | `provisioning server system reboot ` | Guest reboot (DANGEROUS on Proxmox) | | `provisioning server system poweroff ` | Guest poweroff | | `provisioning server system network edit ` | Edit network config | | `provisioning server system storage edit ` | Edit storage config | ### Provisioning clusters | Command | Description | |---------|-------------| | `provisioning cluster add --server-names ... --server-type incus -a ` | Form cluster | | `provisioning cluster list` | List clusters | | `provisioning cluster show ` | Cluster details + certificate | | `provisioning cluster resync ` | Force inventory resync | | `provisioning cluster rename ` | Rename cluster | | `provisioning cluster update --connection-url ` | Update cluster URL | | `provisioning cluster update-certificate ` | Update cluster cert | | `provisioning cluster remove ` | Delete cluster | | `provisioning cluster factory-reset ` | Factory reset | | `provisioning cluster artifact list ` | List artifacts | | `provisioning cluster artifact archive ` | Download artifact | ### Cluster templates | Command | Description | |---------|-------------| | `provisioning cluster-template add --description "..."` | Create template | | `provisioning cluster-template list` | List templates | | `provisioning cluster-template show ` | Show template | | `provisioning cluster-template remove ` | Delete template | ### Inventory | Command | Description | |---------|-------------| | `inventory instance list` | List all instances | | `inventory instance show ` | Instance details (by UUID, not name) | | `inventory storage-pool list` | List storage pools | | `inventory storage-volume list` | List storage volumes | | `inventory storage-bucket list` | List storage buckets | | `inventory network list` | List networks | | `inventory network-acl list` | List network ACLs | | `inventory network-address-set list` | List network address sets | | `inventory network-forward list` | List network forwards | | `inventory network-integration list` | List network integrations | | `inventory network-load-balancer list` | List network load balancers | | `inventory network-peer list` | List network peers | | `inventory network-zone list` | List network zones | | `inventory profile list` | List profiles | | `inventory project list` | List projects | | `inventory image list` | List images | | `inventory query --cluster ` | Cross-resource tree view (v0.3.0+) | ### Updates | Command | Description | |---------|-------------| | `provisioning update list` | List available IncusOS versions | | `provisioning update show ` | Version details + file list | | `provisioning update refresh` | Check for new updates | ### System | Command | Description | |---------|-------------| | `system settings show` | System settings (log_level) | | `system security show` | OIDC, OpenFGA, ACME, trusted certs | | `system updates show` | Update source, filter, signature CA | | `system network show` | Network config | | `system certificate set ` | Set server certificate | ### Admin | Command | Description | |---------|-------------| | `admin os show` | IncusOS hostname, version, uptime | | `admin os application list` | List applications | | `admin os application show ` | Application state | | `admin os service list` | List services (iscsi, lvm, nvme, ovn, tailscale, ...) | | `admin os service show ` | Service config | | `admin os debug log` | Debug logs | | `admin os debug processes` | System process list | | `admin os debug secureboot` | Secure Boot debug info | All commands prefixed with `operations-center`. --- ## REST API Reference OC listens on port **8443** (same as Incus on IncusOS). All endpoints require TLS client certificate authentication. ### Common curl pattern ```bash curl -sk --cert ~/.config/incus/client.crt \ --key ~/.config/incus/client.key \ https://:8443/1.0/ ``` ### Key endpoints | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/1.0` | Server info | | GET | `/1.0/provisioning/updates` | List update packages | | GET | `/1.0/provisioning/tokens` | List tokens | | POST | `/1.0/provisioning/tokens` | Create token | | GET | `/1.0/provisioning/servers` | List registered servers | | GET | `/1.0/provisioning/clusters` | List clusters | | POST | `/1.0/provisioning/clusters` | Form cluster | | GET | `/1.0/inventory/instances` | List all instances | | GET | `/1.0/inventory/instance/` | Instance details | | GET | `/1.0/provisioning/servers/` | Server details + hardware inventory | ### IncusOS REST API (via Incus proxy) IncusOS nodes expose their own REST API through the Incus proxy at the `/os/` prefix on port 8443: | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/os/1.0/system/storage` | Storage config (scrub_schedule) | | PUT | `/os/1.0/system/storage` | Update storage config | | GET | `/os/1.0` | IncusOS system info | --- ## Proxmox-Specific Considerations ### force_reboot is required IncusOS does not auto-halt after installation. Without `force_reboot: true` in the seed (or pre-seed for OC-provisioned ISOs), the installer waits at "please remove installation media" indefinitely. Options for OC-provisioned ISOs on Proxmox: 1. **Pre-seed file** passed to `get-image`: include `force_reboot: true` 2. **Fallback SEED_DATA ISO** on ide3: minimal `install.yaml` with `force_reboot: true` only ### VM creation settings OC-provisioned nodes use the same Proxmox VM settings as standard IncusOS deployments (see `incusos-proxmox`): - `bios=ovmf`, `machine=q35` -- UEFI boot required - `efidisk0`: `pre-enrolled-keys=0` -- IncusOS enrolls its own Secure Boot keys - `tpmstate0`: `version=v2.0` -- required for disk encryption - `cpu=host` -- needed for x86_64_v3 instruction set requirement - `scsihw=virtio-scsi-pci` + `scsi0` -- VirtIO-blk is broken with IncusOS - `balloon=0` -- IncusOS manages memory internally - Minimum 50 GiB disk, minimum 4096 MiB RAM ### Install media removal After installation completes, you **must** stop the VM and delete ide2 (and ide3 if used) before booting from disk. IncusOS refuses to start if install media is still attached. ### IP detection IncusOS is immutable and has no QEMU guest agent. Use ARP-based lookup to find node IPs: ```bash # Get MAC from Proxmox VM config MAC=$(curl -sk "https://:8006/api2/json/nodes/pve/qemu//config" \ -H "Authorization: PVEAPIToken=$TOKEN" | python3 -c " import sys, json d = json.load(sys.stdin)['data'] print(d.get('net0', '').split('=')[1].split(',')[0])") # ARP lookup ip neigh flush dev ping -b -c 3 2>/dev/null ip neigh show | grep -i "$MAC" | awk '{print $1}' ``` --- ## Known Limitations (Tested Findings) ### Confirmed limitations (v0.3.0) - **No brownfield adoption**: nodes must boot from OC-provisioned ISO - **No real-time inventory sync**: requires explicit `cluster resync` - **No cluster member state tracking**: OC always shows `ready`, even for EVACUATED or OFFLINE nodes - **Stale server entries**: out-of-band cluster changes (via `incus` CLI) create stale entries that `resync` doesn't fix - **OC reboot breaks OC-managed Proxmox VMs**: OC agent boot failures cause boot loops or hung services (not a dqlite or TPM issue -- standalone nodes reboot fine) - **Token seed format quirk**: requires structured YAML with section keys, not flat format - **Expired tokens not auto-cleaned**: remain in list after expiry - **No cluster resize**: cannot add/remove members from existing clusters ### Design constraints - OC runs on IncusOS (same immutable OS as Incus nodes) - Uses port 8443 for both API and web UI - Requires at least one trusted client certificate in seed - Provisioning tokens have configurable use count and lifetime - OC adds `meshbr0` network to clusters (not present in manual clustering) - REST API endpoint: `/1.0` (same pattern as Incus) - Auth methods: TLS client cert or OIDC --- ## Troubleshooting (Tested) | Symptom | Likely cause | Fix | |---------|-------------|-----| | `get-image` fails | No updates in `ready` state | Wait ~3 min for OC to download packages; check `provisioning update list` | | Node doesn't self-register | ISO not OC-provisioned, or token exhausted | Verify ISO from `get-image` with valid token; check `token list` for remaining uses | | Node stuck at "remove media" | Missing `force_reboot` in seed | Use token seed with `install: { force_reboot: true }` or SEED_DATA ISO via `incusos-proxmox` | | Token seed fields all `{}` | Flat YAML format | Use structured format with section keys: `install:`, `applications:`, etc. | | OC web UI returns 403 | Client cert not imported | Import client.pfx into browser certificates | | OC CLI "not authorized" | Wrong certs in `~/.config/operations-center/` | Copy `client.crt` and `client.key` from `~/.config/incus/` | | Cluster Terraform errors | `apply_defaults: true` pre-created resources | Non-fatal -- cluster forms despite "already in trust store" / "not in pending state" errors | | `incus` TLS error after clustering | Cluster cert regenerated | `incus remote remove ` then `incus remote add https://IP:8443 --accept-certificate` | | OC reboot kills OC-managed node | OC agent boot failures: invalid crontab, re-registration constraint violation, or cluster peers in boot loops | Destroy and redeploy the VM. Guest reboot is safe on standalone (non-OC) nodes. | | OC stale server entry after node replace | OC doesn't detect out-of-band cluster changes | `cluster resync` doesn't fix stale entries. Must manage cluster lifecycle through OC. | | OC shows old IP for replaced node | Re-registration blocked by existing entry | Remove stale entry (requires cluster dissociation first) | | Token expired but still listed | Tokens don't auto-clean | Remove with `provisioning token remove ` | | VMID collision (500/403 on create) | Pool-scoped token can't see VMs outside pool | Use high `start_vmid` (400+) to avoid collisions | | `cluster resync` socket error | Transient OC internal issue | Retry after a few seconds -- usually succeeds on second attempt | | Proxmox API empty response | Bash `!` in token username | Use `set +H` or store token in variable; avoid bash history expansion | | `server remove` fails | Server is part of a cluster | Must remove cluster first, or force-remove member from Incus cluster | | `cluster join` extra prompt | OC adds `meshbr0` network | Add extra `\n` for `tunnel.mesh.interface` prompt in automated joins | --- ## Comparison: Manual vs OC Cluster Workflow (Tested) ### Manual (incusos-proxmox + incus CLI) 1. Write YAML config with VM specs 2. `incusos-proxmox --yes config.yaml` creates VMs with SEED_DATA 3. Manually set `core.https_address` on each node 4. Delete storage pool/network on joining nodes 5. `incus cluster enable` + `incus cluster join` per node 6. Re-add remotes after certificate changes 7. ~30 minutes for 3-node cluster ### Hybrid (incusos-proxmox --iso + OC) 1. Deploy OC server via `incusos-proxmox --yes lab-oc-deploy.yaml` 2. Create provisioning token + token seed in OC 3. Download OC-provisioned ISO via `token seed get-image` 4. `incusos-proxmox --iso IncusOS-oc.iso --yes lab-oc-nodes.yaml` 5. Nodes auto-register with OC (~30s each) 6. `operations-center provisioning cluster add oc-cluster ...` 7. ~20 minutes for 3-node cluster (including OC deploy) ### What OC automates that we do manually | Step | Manual | OC | |------|--------|-----| | Set `core.https_address` | Per-node via `incus config set` | Automatic during cluster add | | Storage pool/network cleanup for join | Manual delete per joining node | Automatic (handles pending state) | | Cluster enable + join | Multi-step per node | Single `cluster add` command | | Client cert injection | Via SEED_DATA | Via `--application-seed-config` | | `meshbr0` network | Not created | Auto-created by OC clustering | | TLS cert re-add | Manual remote remove/add | Not needed (OC handles internally) | **Trade-offs**: - Manual: full control, no extra server, works with any Incus version - OC hybrid: auto-registration, single-command clustering, centralized inventory/updates, but requires OC server + OC-provisioned ISOs - OC reboot command is **dangerous** on Proxmox (OC agent fails on boot, causing loops -- standalone reboot is safe) - OC inventory requires manual resync (not real-time) - Out-of-band cluster changes (via `incus` CLI) create stale OC state --- ## Open Research Items - **OC reboot failure mechanism** (resolved): guest reboot is safe on standalone IncusOS nodes (tested: simultaneous 3-node reboot, all recover in ~50s). The failure is OC-specific: the OC agent pushes config via the IncusOS REST API that gets persisted to `state.txt`. On reboot, invalid values (e.g., cron expression where Go duration is expected) crash the daemon. Root cause is NOT dqlite corruption or TPM issues. - **apply_defaults conflict with OC Terraform** (resolved): `apply_defaults: false` is now the tested and recommended approach. OC's Terraform handles storage pool, network, and cert creation cleanly when nodes start without defaults. `apply_defaults: true` also works but produces non-fatal Terraform errors. - **IncusOS scrub_schedule bug** (workaround found, upstream filed as #843): ~50% of deploys hit an empty `scrub_schedule` in `state.txt`. Root cause is likely a race condition between REST API startup and `registerJobs()`, or a zero-value encoding bug in `encodeHelper()`. Workaround: `PUT /os/1.0/system/storage` with `{"config":{"scrub_schedule":"0 4 * * 0"}}`. `incusos-proxmox` now auto-heals this proactively on every deploy. - **Stale server entries after out-of-band changes**: when cluster members are removed via `incus cluster remove` (not through OC), stale entries persist in OC's server list. `cluster resync` doesn't fix them. The entry can't be removed while OC considers it part of a cluster. - **Inventory sync is manual**: OC inventory requires explicit `cluster resync` to pick up changes made via the `incus` CLI. No webhook or event-driven sync. This applies to instance creation, migration, evacuation, and deletion. - **Node failure detection**: OC does not detect node failures. `provisioning server show` continues to report `Status: ready` for OFFLINE/crashed nodes. Incus cluster detects failures via heartbeat (~40s), but OC has no equivalent. --- ## References - [Operations Center GitHub](https://github.com/FuturFusion/operations-center) - [Operations Center releases](https://github.com/FuturFusion/operations-center/releases) - [IncusOS GitHub](https://github.com/lxc/incus-os) - [Incus documentation](https://linuxcontainers.org/incus/docs/main/) - [Clustering guide](clustering-guide.md) (manual cluster formation reference)