SynVirt Backend Conventions
Synced read-only from
/home/synnet/mirrors/synvirt-product/docs/BACKEND_CONVENTIONS.md. Edit at the source, not here.
SynVirt Backend Conventions
Section titled “SynVirt Backend Conventions”This document governs every Rust crate in SynVirt. It exists so that a developer who has never seen the codebase can contribute a correct change within 1 hour of arrival, and so that AI agents (Claude Code and others) produce consistent output across generations. Deviations require a written justification in the commit message.
Repository layout
Section titled “Repository layout”/home/synvirt/dev/├── Cargo.toml workspace manifest├── docs/ canonical engineering docs (this file)│ ├── BACKEND_CONVENTIONS.md│ ├── FRONTEND_PRINCIPLES.md│ └── architecture.md├── crates/│ ├── daemon/ control-plane HTTP API│ ├── live/ boot-time TUI (ISO live session)│ ├── console/ post-install TUI (direct console)│ ├── boot-splash/ post-install boot progress UI│ ├── tui-theme/ shared TUI palette + layout helpers│ ├── web-installer/ pre-install browser wizard│ ├── web-ux/ post-install browser dashboard│ ├── synvirt-libvirt/ VM / domain / disk / NIC wrapper│ ├── synvirt-network/ OVS / vSwitch data-plane service (R1, shipped)│ ├── synvirt-storage/ backend-agnostic StorageBackend trait + types│ └── synvirt-zfs/ ZFS impl of StorageBackend (R2 service: synvirt-storaged)└── iso-builder/ live-build pipeline to produce the ISOCrate structure
Section titled “Crate structure”Every backend crate follows:
crates/<name>/├── Cargo.toml├── README.md one-page overview: what/why/how used by others└── src/ ├── lib.rs re-exports public types; never contains logic ├── error.rs ErrorKind enum with thiserror; one enum per crate ├── <concept>.rs one file per domain concept └── tests/ contract tests (integration-style)Error handling
Section titled “Error handling”- All error types derive
thiserror::Error. - One
Errorenum per crate, exposed aspub use error::Error. - Public APIs return
Result<T, crate::Error>, neveranyhow::Result. anyhow::Resultis only allowed inmainfunctions and tests.- Every variant has a machine-readable discriminant (e.g.
#[error("...", code = "E_VM_NOT_FOUND")]).
Do:
#[derive(thiserror::Error, Debug)]pub enum Error { #[error("VM {name} not found")] VmNotFound { name: String }, #[error("libvirt connection failed: {source}")] LibvirtConn { #[source] source: std::io::Error },}Don’t:
pub fn get_vm(name: &str) -> Result<Vm, String> { ... }Async / blocking
Section titled “Async / blocking”- Public APIs are
async fnunless the operation is trivially fast (<1ms). - Anything that shells out, hits disk, or waits on a socket runs via
tokio::task::spawn_blockingto avoid stalling the runtime. - Never block the tokio runtime with
std::process::Command::status()— usetokio::process::Commandorspawn_blocking.
Logging & observability
Section titled “Logging & observability”- Use the
tracingcrate. Neverprintln!, nevereprintln!in library code. - Annotate every public async fn with
#[tracing::instrument(skip_all)]and emitdebug!on entry,info!for business events,warn!for recoverable issues,error!only for non-recoverable failures. - Trace IDs propagate across daemon → synvirt-libvirt → external commands.
Example:
#[tracing::instrument(skip_all, fields(vm_name = %req.name))]pub async fn create_vm(req: CreateVmRequest) -> Result<Vm, Error> { debug!("entering create_vm"); let xml = xml::render(&req)?; info!("defining domain {}", req.name); ...}Testing
Section titled “Testing”- Contract tests in
crates/<name>/src/tests/exercise the public API only. - No tests of internal functions unless they encode non-trivial math.
- Integration tests that require a running service (libvirtd, zfs, ovs)
live under
crates/<name>/tests/integration_*.rsand are gated with#[ignore]by default; CI runs them withcargo test -- --ignored. - Use
mockallor hand-rolled trait mocks to isolate units from the filesystem and external processes in unit tests.
Naming
Section titled “Naming”Function verbs carry meaning:
| Prefix / pattern | Semantic |
|---|---|
get_* |
Fails if absent. Returns Result<T, Error::NotFound>. |
find_* |
Returns Option<T>. Never errors on absence. |
list_* |
Returns Vec<T>. Empty vec if none. |
ensure_* |
Idempotent. Creates if missing, no-op if exists. |
reconcile_* |
Idempotent convergence toward a declared state. |
apply_* |
Destructive / state-changing. Not idempotent. Logged as info. |
plan_* |
Pure, no side effects. Returns a plan object. |
Commits
Section titled “Commits”Conventional Commits with crate scope:
feat(synvirt-libvirt): add create_vm with cloud-init supportfix(daemon): handle 503 from upstream maintenance middlewarerefactor(web-ux): extract VmCard componentdocs(backend): clarify tracing conventionsBreaking changes: ! after the scope plus BREAKING CHANGE: footer.
Formatting & linting
Section titled “Formatting & linting”cargo fmtmust pass. The repo ships arustfmt.tomlusing default Rust style.cargo clippy -- -D warningsmust pass. Deny warnings in CI.cargo auditmust pass. No dependencies with known advisories.- No
unwrap()orexpect()in library code outside of test modules. Useok_or_else/?/ explicit match.
Documentation
Section titled “Documentation”- Every
pubitem gets rustdoc. No exceptions. - The first line of a rustdoc is a single sentence summary, complete, ending with a period. Subsequent paragraphs can elaborate.
- Public enums document each variant.
- Examples in rustdoc compile and run under
cargo test --doc.
Versioning
Section titled “Versioning”- Each crate has its own
versioninCargo.toml. - Bump patch on fix, minor on new API, major on breaking change.
- Root workspace version (the ISO VERSION file) is independent.
String externalization
Section titled “String externalization”All user-facing strings (logs emitted above debug level, error messages,
REST response bodies) live in crates/<name>/src/strings.rs as pub const
or in a strings/en_us.toml loaded at startup. This is the foundation for
i18n. en_us is the only catalog today. Do not hardcode strings inline in
business logic.
Synchronization & shared state
Section titled “Synchronization & shared state”- Prefer immutable data and message passing (
tokio::sync::mpsc) over shared mutable state. - When shared state is unavoidable, use
tokio::sync::RwLockfor read-heavy,tokio::sync::Mutexfor write-heavy. Avoidstd::sync::*inside async code. - Global singletons (e.g. config) use
once_cell::sync::Lazyinitialized at daemon startup, never at first access.
External command execution
Section titled “External command execution”Two approaches, in order of preference:
- Library binding (e.g.
virtcrate for libvirt,zfscrate for ZFS). Use when mature and available. Type-safe, faster. - Subprocess (
tokio::process::Command). Use when no good binding exists or when debugging ease is more important than performance.
If subprocess, always:
- Pass arguments as
&[&str], never build shell strings. - Capture stdout AND stderr separately.
- On non-zero exit, return
Errorwith the full stderr in the message. - Apply a timeout via
tokio::time::timeout— no external command runs unbounded.
ZFS execution ownership (doctrine amended 2026-07-01)
Section titled “ZFS execution ownership (doctrine amended 2026-07-01)”synvirt-storaged is the control plane for storage object lifecycle —
pool create/delete/rename/list and the PoolId ↔ PoolLabel registry. It is
NOT the sole executor of zfs: streaming data-plane work (zfs send /
zfs receive) and per-job snapshot handling execute in the daemon that owns
the job. The daemon snapshot stack and the migrator sink set the precedent;
synvirt-backupd follows it for backup/restore (backup-P3 signature).
Bounding rules:
- A data-path daemon runs
zfsonly against snapshots/datasets it owns. backupd enforces this in code: snapshot create/destroy only under thesynvirt-backup-*prefix,zfs receiveonly into its restore staging namespace, violations trip a typedE_BACKUP_SNAPSHOT_FORBIDDEN. - Pool lifecycle mutations (
zpoolcreate/destroy/rename, import/adopt) remain storaged-only. - Tracked deferral (TODO.md): funnel the scattered direct
zfscall sites across daemon/migrator/core through one shared, guard-capable helper.
Security
Section titled “Security”- No secrets in code, comments, or logs.
- No
unsafeblocks without a// SAFETY:comment explaining invariants. - All HTTP endpoints that mutate state require authentication middleware.
- Rate limit destructive endpoints (delete VM, factory reset) per IP.