Skip to content

SynVirt Architecture

Synced read-only from /home/synnet/mirrors/synvirt-product/docs/architecture.md. Edit at the source, not here.

Version: 0.15.0 (0.15.0 line) Last updated: 2026-06-20

⚠️ Stale design snapshot. This document was written at the 0.7.6 (Hito 0.7) foundation and has not been re-baselined; significant parts no longer describe 0.15.0. The control plane is no longer a single monolithic daemon — data planes synvirt-network (R1) and synvirt-storaged (R2) are separate systemd services the daemon proxies over synvirt-ipc (UDS); the canonical dashboard is web-ux-v2 (not the legacy web-ux described below); disks are RAW ZFS zvols (not qcow2); and LINSTOR/DRBD is an unbuilt future tier, not the shipped storage. Storage-plane ownership was amended 2026-07-01 (backup-P3 signature): synvirt-storaged is the control plane for storage object lifecycle (pool create/delete/rename/list + registry); streaming data-plane work (zfs send/recv) executes in the owning daemon (migrator precedent, now synvirt-backupd) — see docs/BACKEND_CONVENTIONS.md §External command execution. For current state use CLAUDE.md §2/§5 and docs/architecture/refactor-roadmap.md.


  1. System Overview
  2. Operational Modes
  3. Daemon Design
  4. synvirt-libvirt crate
  5. Dashboard (web-ux) data flow
  6. Roadmap (Hitos)
  7. Technology Stack
  8. Network Topology
  9. Licensing Model

SynVirt is a hypervisor platform designed for SMBs and MSPs. Its architecture centers on a single Rust daemon (synvirt-daemon) that acts as the control plane, serving both the management API and the web interface over HTTPS from a single binary.

The platform ships as a live ISO built with live-build on top of Debian 13 Trixie (kernel 6.12 LTS). On boot the kernel runs the system in RAM with synvirt-daemon already listening on port 443; the operator drives the entire install from a web wizard talking to /api/install/*, then the same daemon continues into post-install service without a handoff. The daemon manages the full lifecycle of the host node: install orchestration, health reporting, VM orchestration (Hito 2+), storage management (3 configurable modes today; first-class module in Hito 3), and cluster coordination (Twin / Cluster from October 2026 onward).

Browser / API client
|
| HTTPS :443 (prod) / :8443 (dev)
v
+------------------+
| synvirt-daemon | <-- single Rust binary
| |
| Axum router |
| Rustls TLS |
| Static assets |
| REST API |
+--------+---------+
|
| (future phases)
v
+------------------+ +------------------+
| KVM / Cloud | | LINSTOR + DRBD |
| Hypervisor | | over ZFS zvols |
+------------------+ +------------------+

SynVirt supports three modes. The upgrade path between them is non-destructive: an existing Standalone node can be promoted to Twin or Cluster without reinstalling.

A single node with no replication or HA. Intended for development, testing, and single-site SMB deployments where downtime is acceptable.

  • 1 node
  • Storage: operator-selected at install (single_disk_zfs / multi_disk); persisted in /etc/synvirt/storage-mode
  • No live migration
  • No quorum required

Two nodes plus a lightweight witness. VMs replicate synchronously via DRBD. If one node fails, the other takes over with minimal interruption.

  • 2 active nodes + 1 witness (can be a small VM or cloud instance)
  • Synchronous DRBD replication
  • Live migration between the two nodes
  • Witness provides quorum tie-breaking
  • Intended for: 2-server SMB setups, primary + DR scenarios
Node 01 <---DRBD sync---> Node 02
| |
+----------+----------+---+
|
Witness
(quorum only)

Three or more nodes with etcd providing distributed consensus. The scheduler places VMs across nodes based on resource availability. Any minority of nodes can fail while the cluster remains operational (N/2+1 quorum).

  • 3+ nodes (odd number recommended for quorum)
  • etcd Raft for cluster state and leader election
  • Distributed VM scheduling
  • Full HA: any single node failure is transparent
  • Intended for: MSPs, multi-tenant environments, larger SMBs
+--------+ +--------+ +--------+
| Node 1 | | Node 2 | | Node 3 |
| | | | | |
| etcd | | etcd | | etcd |
| daemon | | daemon | | daemon |
+---+----+ +---+----+ +---+----+
| | |
+------etcd Raft quorum-----+
| | |
+-------DRBD replication----+
Standalone --> Twin --> Cluster
(1 node) (2+witness) (3+ nodes)
Add nodes and reconfigure; no reinstall required.

synvirt-daemon is a single compiled Rust binary. It embeds the HTTP router, TLS termination, and all API handlers. There is no separate web server process.

TLS is handled natively by Rustls (no OpenSSL dependency). A self-signed certificate is generated at daemon startup and stored in /etc/synvirt/tls/ (prod) or ~/.synvirt/tls/ (dev). The certificate’s SANs cover hostname, localhost, and all global IPv4 addresses detected on the host. In production deployments, operators can supply their own certificate files post-setup.

  • Dev mode: self-signed, regenerated on each start if absent
  • Prod mode: certificate and key paths configurable via config file or env vars
Method Path Description
GET / Web installer wizard (pre-setup) or dashboard (post-setup), chosen by root-selection logic
GET /api/v1/health JSON health report
GET /api/v1/info Node info (hostname, version, mode)
GET /api/v1/setup/status Setup phase state (needs_setup / has_admin / has_mode)
POST /api/v1/setup/auth Exchange 4-digit setup token for a Setup session
POST /api/v1/setup/complete Mark setup done, write /etc/synvirt/setup.completed, mint Admin session
GET /api/hardware, /api/disks, /api/nics Discovery for the wizard
POST /api/install/start Kick off install timeline with storage_mode + config
GET /api/install/status Poll install stage + progress
POST /api/install/reboot Reboot into the newly installed system
GET /healthz Kubernetes-style liveness probe
GET /readyz Kubernetes-style readiness probe

Configuration is resolved in this order (later overrides earlier):

  1. Compiled-in defaults
  2. /etc/synvirt/synvirt.toml (system config file)
  3. Environment variables (SYNVIRT_*)
  4. Command-line flags (--dev, --port, etc.)

When started with --dev:

  • Listens on port 8443 instead of 443
  • Serves static assets from ./web/ (relative to binary) instead of /opt/synvirt/web/
  • Enables pretty (human-readable) log formatting
  • Relaxes TLS certificate validation for localhost

The daemon picks one of three web roots at startup based on install phase (crates/daemon/src/main.rs):

Phase Marker Web root
Dev mode (--dev) SYNVIRT_DEV=1 or CLI flag daemon/assets (relative)
Post-setup /etc/synvirt/setup.completed exists and /opt/synvirt/web-ux/ staged /opt/synvirt/web-ux/ (dashboard)
Pre-install / pre-setup /opt/synvirt/web-installer/ staged /opt/synvirt/web-installer/ (wizard)
Legacy fallback Neither staged /opt/synvirt/web/

Assets today are plain HTML + vanilla JS + CSS. The Next.js 15 migration is scheduled for Hito 1 (late April 2026) and will replace /opt/synvirt/web-ux/ first, leaving the wizard on vanilla JS until it burns.

The daemon ships a hardened systemd unit:

[Service]
Type=simple
ExecStart=/opt/synvirt/bin/synvirt-daemon
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/etc/synvirt /var/lib/synvirt /var/log/synvirt

Authoritative schedule with deliverables and dates: roadmap.md. Short summary from the architecture perspective:

  • Hito 0 / 0.2 / 0.3 (completed) — foundation MVP, Ratatui TUI, Debian 13 Trixie migration with ext4 rpool + ZFS data pool.
  • Hito 0.4 (completed, April 2026) — unattended debconf apply, OVS br-mgmt, Direct Console UI, setup wizard on /api/v1/setup/*, basic VM CRUD via virsh.
  • Hito 0.5 (completed, April 2026) — live-build ISO replaces preseed + TUI; daemon drives install via /api/install/*.
  • Hito 0.6 (completed, April 2026) — 2 storage modes (single_disk_zfs / multi_disk), verbose streaming installer, netplan DHCP fallback for uplink-less boots, diagnostic synvirt/debug user with auto-cleanup, UX dev server with fixtures, dashboard stub.
  • Hito 1 (late April 2026) — Next.js 15 dashboard, admin login, session management; wizard stays vanilla JS for now.
  • Hito 2 (May 2026)module-virt with KVM lifecycle + console.
  • Hito 3 (June 2026)module-storage with ZFS + LINSTOR + ZFS-on-root with boot environments (see Issue 001).
  • Hito 4 (July 2026)module-network with OVN + nftables.
  • Phase 1 complete (Aug–Sep 2026) — hardening pass, CLI, local auth, audit log, operator docs.
  • Twin mode (October 2026) — 2 nodes + witness + DRBD over zvols, live migration.
  • Cluster mode (November–December 2026) — 3+ nodes, embedded etcd Raft, distributed scheduler.
  • GA (Q1 2027) — multi-tenancy, LDAP/AD, RBAC, commercial support tier.

Component Technology Rationale
Base OS Debian 13 Trixie minimal (kernel 6.12 LTS) LTS, strong KVM ecosystem, OpenZFS 2.4.x from backports
Control plane Rust 1.95 (stable) Memory safety, no GC pauses, single binary
HTTP framework Axum Async, ergonomic, first-class Rustls support
Async runtime Tokio De-facto standard for async Rust
TLS Rustls Pure Rust, no OpenSSL dependency
Web UI (current) Plain HTML + vanilla JS Zero build toolchain, wizard iterable via Python dev server
Web UI (Hito 1+) Next.js 15 + shadcn/ui Dashboard rewrite; wizard migrates later when it burns
Hypervisor KVM + Cloud Hypervisor (Hito 2) Linux-native, open standard, no license cost
Block replication DRBD + LINSTOR (Twin mode, Oct 2026) Battle-tested synchronous replication
Local data pool ZFS (operator-configurable: single_disk_zfs / multi_disk) CoW snapshots, checksums, RAID-Z, feeds DRBD zvols
Boot environments ZFS-on-root (Hito 3, see Issue 001) Atomic rollback on failed OS upgrades
Cluster consensus etcd (Cluster mode, Nov–Dec 2026) Proven Raft implementation
SDN OVN + nftables (Hito 4) Open standard, native Linux networking
ISO builder live-build + xorriso Live ISO with daemon running in RAM; wizard drives install via HTTPS
Dev build env Debian 13 native + Rust toolchain Same distro as the shipping live-ISO runtime

Internet / LAN
|
[ Management NIC ]
|
synvirt-daemon :443
|
Web browser

Each node will have at least two physical NICs:

NIC Purpose Notes
eth0 (management) HTTPS management access, VM traffic Public-facing
eth1 (storage/sync) DRBD replication, live migration Private, dedicated link

In Cluster mode a third NIC for etcd heartbeat traffic is recommended to isolate consensus traffic from VM data traffic.


SynVirt is licensed commercially, tiered by CPU socket count.

Tier Sockets Target
Starter 1 socket Small office, single server
Professional 2 sockets SMB primary + DR
Business Up to 8 sockets Growing SMB, small MSP
Enterprise Unlimited sockets MSP, multi-tenant

Licenses are node-bound and activated online. Offline activation is available for Enterprise tier. Trial licenses (30 days, Starter limits) are available through the SYNNET portal at synnet.mx.


Introduced in 0.7.0 as the canonical wrapper around libvirt/QEMU-KVM for every internal consumer (daemon REST, Console TUI VM screens in future releases, CLI, VirtDCM). Replaces ad-hoc virsh calls scattered across the daemon.

crates/synvirt-libvirt/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs public surface re-exports
├── error.rs Error enum (thiserror, coded variants)
├── client.rs Client handle (qemu:///system URI)
├── vm.rs VM lifecycle (create/start/stop/delete/list)
├── disk.rs qcow2 disk allocation via qemu-img
├── nic.rs virtio NIC attach on OVS bridge br-vm
├── host.rs host inspection (capabilities, CPU, RAM)
├── xml.rs domain XML renderer (q35, virtio, VNC)
├── virsh.rs subprocess runner with timeout + parsing
└── strings.rs user-facing error and log strings

Backend: virsh subprocess invoked via tokio::process::Command with a 30-second timeout per call. Chosen for transparency (every action is traceable in journalctl) and zero native build dependencies. A future migration to the virt FFI crate is possible behind the same public API without breaking consumers.

Public surface is async; all blocking work runs inside tokio::task::spawn_blocking. Every public function emits a tracing::instrument(skip_all) span with structured fields (vm_name, uuid, phase) so a request can be followed across the daemon → crate → virsh boundary.


The post-install dashboard is a single-page Vue 3 app served from /opt/synvirt/web-ux/. All network I/O goes through api.js, which attaches the Basic Auth header from sessionStorage and routes every response through the error translator in assets/error-messages.json.

Browser (VmCreateModal.js)
→ api.createVm({name, cpu, ram, disk_gb, iso, network})
→ POST /api/v1/vms (Basic Auth)
→ daemon::api::vms::create
→ synvirt_libvirt::vm::create_vm
→ disk::allocate_qcow2 (qemu-img create -f qcow2)
→ xml::render (q35 + virtio + OVS + VNC)
→ virsh::define (virsh define <xml>)
→ virsh::start (optional) (virsh start <name>)
← Vm { name, uuid, state, ... }
← 201 Created + Vm body
→ store.vms.push(vm) + toast "VM created"
Browser (VmDetail.js: Start button)
→ api.startVm(name)
→ POST /api/v1/vms/:name/start
→ daemon::api::vms::start
→ synvirt_libvirt::vm::start (virsh start)
← 200 OK
→ optimistic state flip to "running"
→ 2s poller confirms via GET /api/v1/vms/:name

On mount, DashboardHome.js fires three parallel GETs:

Promise.all([
api.health(), // GET /api/v1/health -> uptime, cpu, ram
api.listVms(), // GET /api/v1/vms -> VM count + states
api.listPools(), // GET /api/v1/storage -> pool usage
])

All three are gated by Basic Auth middleware. A 401 on any of them clears sessionStorage and redirects to #/login. The five-second rule (see docs/FRONTEND_PRINCIPLES.md) requires that all three cards reach “ready” or “failed” state within 5 seconds of mount; slower responses render a skeleton placeholder per the loading-latency tiers.

The Settings module is the host-configuration surface on the v2 dashboard. It is rolled out in phases — each section ships as an independently mergeable bundle that replaces a PlaceholderPanel stub on the /settings view.

  • HTTP shape. Every endpoint lives under /api/v1/settings/... and returns a { data, warnings, meta } envelope (see crates/daemon/src/settings/envelope.rs). Errors keep the daemon-wide ErrorEnvelope shape with stable E_* codes.
  • Auth. PAM-backed Basic Auth via the existing daemon middleware.
  • Tracing. Targets are synvirt.settings.<section> for runtime events and synvirt.migration.<section> for first-boot migrations.
  • Audit. Mutating calls write to the sled tree audit/settings (DB at /var/lib/synvirt/settings-audit.db). Each record is { ts_ms, actor, endpoint, body_hash, outcome, before, after }; the constant synvirt_time::audit::AUDIT_TREE is shared by every settings section so phase 3+ writes land in the same place.
Endpoint Purpose
GET /api/v1/settings/time Snapshot: timezone, sync flag, sources, chrony tracking, host wall-clock as host_unix_ts_ms.
PUT /api/v1/settings/time Transactional apply with rollback.
POST /api/v1/settings/time/manual Set the wall clock when sync is disabled. Surfaces a W_TIME_LARGE_JUMP warning when `
POST /api/v1/settings/time/refresh chronyc -a 'burst 4/4' then makestep.
  • Daemon. chrony only (no timesyncd). Subprocess calls are abstracted behind synvirt_time::exec::Exec so the apply path is unit-testable; the production impl is TokioExec shelling out to chronyc and timedatectl.
  • Persistence. Source of truth for NTP sources, timezone, and the sync flag is the new [time] section of /etc/synvirt/synvirt.toml. The daemon renders /etc/chrony/sources.d/synvirt.sources on every apply and on first-boot migration. Sled is not used for time config — only for the audit trail.
  • Defaults. time.cloudflare.com (server) + pool.ntp.org (pool), both with iburst; sync enabled; timezone UTC. The defaults stay until the operator removes them through the UI.
  • Live host clock on the UI. The browser computes offset = host_unix_ts_ms - Date.now() once on mount and on every mutation response, then renders new Date(Date.now() + offset) on a 1Hz interval. Visibility-aware: paused while the tab is hidden, re- syncs when it becomes visible again.
  • Migration. Idempotent first-boot step (synvirt_time::migrate::run):
    1. Stop / disable / mask systemd-timesyncd.service if active.
    2. Enable + start chrony.service.
    3. Ensure /etc/chrony/chrony.conf carries sourcedir /etc/chrony/sources.d.
    4. Seed [time] from existing chrony sources or fall back to the defaults.
    5. Render /etc/chrony/sources.d/synvirt.sources.
    6. chronyc reload sources (best effort).
Endpoint Purpose
GET /api/v1/settings/reservation Snapshot: mode, reserved + total memory/CPU, recommended values, manual-mode limits.
PUT /api/v1/settings/reservation Switch mode and (when manual) store explicit memory/CPU values. Validated against 1 GiB / 0.5 GHz floors and the 50% host-total ceiling.
POST /api/v1/settings/reservation/auto Convenience: equivalent to PUT { "mode": "auto" }.
  • Crate. synvirt-resources provides the auto formula, the manual-mode bounds, the /proc/meminfo + /proc/cpuinfo probes, and the CephProbe trait (dormant until storage-ceph). The auto formula is max(2 GiB, 6% RAM) + 1 GiB × osd_count for memory and max(1.0 GHz, 5% CPU) + 0.5 GHz × osd_count for CPU.
  • Persistence. [resources.reserved] in /etc/synvirt/synvirt.toml. No rendered drop-ins, no daemon restarts. The mode = "auto" form strips manual keys on save so the file stays minimal.
  • Hardware probe. Cached in a tokio::sync::OnceCell for the daemon’s lifetime; CPU hotplug is ignored in 0.x. Test seam: HardwareProbe trait + StaticProbe for unit tests.
  • Audit. Reuses the audit/settings sled tree opened in main.rs; no second DB.
  • Out of scope. Scheduler enforcement (lands with synvirt-vm), live Ceph OSD probe (stub returns 0), NUMA-aware reservation, per-pool storage reservation.
Endpoint Purpose
GET /api/v1/settings/services Curated catalog with full per-unit state.
GET /api/v1/settings/services/:unit Single-unit detail. 404 E_SERVICES_NOT_MANAGED for off-allowlist names.
GET /api/v1/settings/services/:unit/journal?lines=<n> Tail (1-1000) of journald log, scoped to the unit.
  • Allowlist. Hardcoded in synvirt-services::allowlist. Adding a unit requires a code review. Order matters — the UI renders the list in declaration order. ceph.target is gated by edition and hidden in standalone.
  • D-Bus. zbus::Connection::system() opens once in ServicesService::connect() at daemon startup and is shared across calls. The trait SystemdClient exposes unit_state(spec); production impl ZbusSystemdClient calls LoadUnit + GetAll against org.freedesktop.systemd1.{Manager,Unit,Service}. A D-Bus failure on a single unit degrades that row to load_state = "not-found" without failing the whole list.
  • Capabilities (can_*). Pure function over (spec, raw_state). synvirt-api.service always denies stop and disable (recovery path uses restart). Targets do not enable / disable. Not-installed units have every capability off.
  • Journal. Subprocess journalctl --no-pager --output=json --unit <u> --lines <n> via the existing Exec trait. MESSAGE fields that arrive as byte arrays (non-UTF-8 originals) are coerced to UTF-8 lossily so a single odd entry does not break the parser. Live tailing is out of scope for 4a.
  • Edition. ServicesService::new(..., edition) filters the allowlist at construction. The licensing endpoint (Phase 9) rebuilds the service when the edition flips.
  • Mutations (Phase 4b). Five action endpoints on /api/v1/settings/services/:unit/{start,stop,restart,enable,disable} plus a read-only restart-impact pre-check. Each mutation is blocking (30 s wall clock) with the subscribe-first / issue-action / wait pattern against Manager.JobRemoved. synvirt-api.service always denies stop / disable (defense-in-depth) and routes restart through the oneshot helper synvirt-self-restart.service so the daemon never kills its own HTTP response. Idempotent paths short-circuit without a D-Bus call. High-impact units (libvirtd.service, openvswitch-switch.service, synvirt-api.service restart, synvirt-console-gateway.service with active sessions) require typed confirmation (RESTART / STOP / DISABLE). Mutating calls write full before/after UnitState snapshots to the shared audit/settings sled tree.
  • Out of scope for 4a/4b. Live journal streaming, live unit-state subscription via PropertiesChanged (we re-fetch on each refresh), cgroup v1 fallback (assume v2 — Debian 12+), service templates, mass actions, mutation queueing.
Endpoint Purpose
GET /api/v1/settings/firewall Snapshot: enable flag, default policies, system rules, user rules, per-rule counters.
PUT /api/v1/settings/firewall Toggle enable/disable. Disable requires confirmed: "DISABLE FIREWALL" server-side.
POST /api/v1/settings/firewall/rules Add a user rule (server-assigned ULID).
PUT /api/v1/settings/firewall/rules/:id Update a user rule.
DELETE /api/v1/settings/firewall/rules/:id Remove a user rule.
POST /api/v1/settings/firewall/rules/:id/move Move up or down within the user-rules array; clamped at boundaries.
POST /api/v1/settings/firewall/test Dry-run validation of a hypothetical config.
  • Backend. synvirt-firewall owns the nftables table inet synvirt. Every apply is transactional: render → nft -c -f <tmp> (validate-only) → nft -f <tmp> (atomic kernel commit) → rename tmp into place. nftables transactions are atomic in the kernel, so there is no race window where the host is exposed.
  • System vs user rules. System rules are hardcoded by edition
    • storage-ceph state and rendered before user rules. The list is surfaced to the operator (read-only) so they understand what is protecting them. User rules are CRUD + move; comment + enabled flag are excluded from the duplicate-detection identity tuple so toggling enabled does not collide with itself.
  • Counters. nft list ruleset -j parses the per-rule counter expressions; matched by the comment slot which carries the sys-* id (system rules) or the ULID (user rules). nft -j failures degrade to zero counters without failing the GET.
  • Disable. Allowed under both editions; phrase always required server-side. The standalone UI submits the phrase silently from a simple confirm modal; the enterprise UI uses a type-to-confirm field. Disabled state persists across reboots (/etc/nftables.d/synvirt.nft becomes a placeholder, the kernel table is dropped).
  • Recovery. From SSH: nft delete table inet synvirt. The daemon does not need to be involved.
  • Out of scope. Forward-chain rules (lands with synvirt-net), NAT, masquerade, mark / conntrack matchers, per-VM firewall (libvirt nwfilter via synvirt-vm), rate limiting, geo-IP.
Endpoint Purpose
GET /api/v1/settings/swap Snapshot: enabled flag, swappiness, devices from /proc/swaps, size limits derived from host RAM.
POST /api/v1/settings/swap/create Allocate, format, and activate the swap file.
PUT /api/v1/settings/swap Adjust swappiness only.
DELETE /api/v1/settings/swap swapoff (5-minute wall timeout) + remove file + drop fstab/sysctl fragments.
  • Default state. Disabled. Fresh installs do not create a swap file.
  • Backend. synvirt-swap reuses the shared Exec trait. Create flow runs fallocate -l (falls back to dd if=/dev/zero on filesystems that reject preallocation), chmod 600, mkswap, swapon, then writes the fstab fragment and the sysctl drop-in. Each step is reversed on failure (swapoff, unlink, fragment removal).
  • Persistence. synvirt.toml [swap] is canonical (enabled, size_bytes, swappiness, path). The fstab fragment at /etc/fstab.d/synvirt-swap.fstab and the sysctl drop-in at /etc/sysctl.d/99-synvirt-swap.conf are rendered from it; both files are removed on disable.
  • Limits. 1 GiB floor, 16 GiB ceiling, hard reject above 50% of host RAM. Recommended swappiness when enabled is 1 (aggressive avoidance for hypervisor workloads); the kernel default of 60 is restored on disable.
  • Disable. swapoff is wall-clock-bounded at 5 minutes. On timeout the daemon returns 504 and leaves the file + fragments in place — partial cleanup is worse than no cleanup; the operator can retry or reboot.
  • Out of scope. zswap / zram, partition-backed swap, multiple swap devices, encrypted swap, hibernation integration, live resize without disable.
Endpoint Purpose
GET /api/v1/settings/packages Curated package state (updates available + counts + reboot flag).
POST /api/v1/settings/packages/refresh Run apt-get update (5-minute throttle). Returns 202 + change_id.
POST /api/v1/settings/packages/upgrade Targeted or security-only upgrade. Returns 202 + change_id.
GET /api/v1/changes/:change_id/stream Cross-cutting SSE for any long-running operation.
  • Allowlistsynvirt_packages::allowlist::PATTERNS. Matches by exact name or trailing * wildcard. Filters what is displayed and what an operator can explicitly request; APT is free to upgrade transitive dependencies.
  • Streamingapt-get is spawned with piped stdout/stderr, each line classified by the pure synvirt_packages::parser::classify into one of update, download, install, cleanup, done, then broadcast to every subscriber on the change’s tokio::sync::broadcast channel. Tail of the last 50 lines is preserved on the CompleteEvent for failure surfacing.
  • ConcurrencyChangeRegistry keeps a single in-flight slot; a second start while the first is running returns E_PACKAGES_UPGRADE_IN_PROGRESS with the existing change_id in the problem detail. The second client can subscribe to that id and tap the live stream.
  • Maintenance window — non-security upgrades refuse to start while any VM is in a transitional state (migrating / snapshotting). Security-only upgrades proceed regardless.
  • Reboot detection — the daemon checks /var/run/reboot-required on every GET; the dashboard’s RebootRequiredBanner (mounted in AppShell) polls and surfaces a sticky banner across every page when set.
  • Out of scope. APT source / repo management, third-party trust workflows, package holds / pinning, dist-upgrade / release upgrades, downgrade / rollback, in-daemon reboot orchestration, multi-host coordinated upgrades (VirtDCM 0.12+), reconnect to an in-progress operation after browser disconnect.
Endpoint Purpose
GET /api/v1/settings/bmc Snapshot: detection (nested / vendor / firmware), configuration, last connection state.
PUT /api/v1/settings/bmc Save configuration. Tests credentials before persist; rejects with 400 if the test fails.
POST /api/v1/settings/bmc/test Test credentials without persisting. Always returns 200 with the outcome enum.
DELETE /api/v1/settings/bmc Remove the encrypted credential row.
  • Detection. Nested check first (systemd-detect-virt + DMI product name); on bare metal, ipmitool mc info is parsed and the Manufacturer ID decimal is mapped against the IANA Private Enterprise Number registry (HPE 11, Dell 674, Lenovo 19046, Supermicro 47488 / 10876, Cisco 9, Huawei 2011, Fujitsu 211, Inspur 37945; legacy IBM 2 / 20301 alias to Lenovo). The result is cached for the daemon’s lifetime in an Arc<RwLock<Option<DetectionResult>>>.
  • Connection test. Redfish first (GET https://<ip>/redfish/v1/ with Basic Auth, 5 s timeout, danger_accept_invalid_certs driven by the tls_verify flag), IPMI LAN as fallback (ipmitool -I lan -H <ip> -U <u> -P <p> mc info). Both paths classify the failure into unreachable, auth_failed, timeout, tls_error, or unknown.
  • Storage. Credentials live in a sled tree secrets/bmc (one row, key active). Plaintext is the JSON shape { bmc_ip, username, password, protocol, tls_verify }; the encrypted blob is nonce || ciphertext under AES-256-GCM. The 32-byte AES key is derived as SHA-256("synvirt-bmc-key-v1\0" || machine_id || "\0" || keyfile). The keyfile at /etc/synvirt/secrets.key is auto-created with random bytes on first use, mode 0600.
  • Audit. Save / delete write before / after snapshots with the password redacted as ***. Connection tests do not audit.
  • Out of scope for 8a. Hardware inventory (chassis / BIOS / CPU / RAM / PSU / fans), live sensors (temperature / RPM / voltage / watts), vendor-specific data normalization, power actions (on / off / cycle), multiple BMCs, KCS device file detection beyond ipmitool’s own check. All of the above land in Phase 8b.

Out of scope for the Settings module (deferred across phases)

Section titled “Out of scope for the Settings module (deferred across phases)”
  • Cluster-wide time consensus and inter-node drift detection — the cluster_drift_warning field in GET /settings/time is reserved for the cluster crate (0.10).
  • VirtDCM-driven time policy and RBAC beyond authenticated admin — arrives with VirtDCM enrolment (0.12).