vmsig: management daemon, runtime endpoint lifecycle, roster, discovery, in-tree drivers, packaging

- core: runtime attach/detach of a per-endpoint adapter trio (runtime-safe add_adapter + vmsig_core_detach_endpoint, deferred reap)
- roster: VMSIG_EV_ROSTER + CAP_ROSTER, retained per-endpoint and replayed to late subscribers
- discovery: inotify trigger dir, vmid/endpoint slot allocator, host probe; vmsigd daemon with config + per-uid admission
- input driver and vgpu perception built in-tree; vgpu perception as a separate library
- memctx: own the supplied ro_fd (closed at detach)
- deb packaging: install rules, systemd unit, tmpfiles, default config
This commit is contained in:
2026-06-22 17:25:06 +03:00
parent 0d387a4249
commit 9bde398b6c
55 changed files with 4703 additions and 61 deletions
+46
View File
@@ -0,0 +1,46 @@
#ifndef VMSIG_DISCOVERY_H
#define VMSIG_DISCOVERY_H
#include "vmsig_core.h"
#include "host_probe.h"
/* discovery.h — runtime VM discovery (private to the discovery module).
*
* Watches a tmpfs trigger dir for "vm-<vmid>-ram" files, corroborates each candidate via the
* host-probe seam, assigns a stable endpoint slot, hot-plugs the VM (sink), and publishes the
* roster. The state machine + slot allocation are decoupled from actuation by a sink seam, so
* the orchestration is unit-testable without armed adapters. */
typedef struct vmsig_discovery vmsig_discovery;
/* Actuation seam: bring a discovered VM up / tear it down. Default (NULL) wires the core
* adapter trio (memctx+vmhost+input via vmsig_core_add_adapter) and detach_endpoint. A test
* injects a recording sink to verify the state machine without real adapters. Roster publish
* is owned by discovery (not the sink): ATTACH after a successful attach, DETACH before tear-down. */
typedef struct {
int (*attach)(void* ud, vmsig_core* core, uint32_t vmid, uint32_t endpoint,
const vmsig_host_facts* f); /* 0 = up, -1 = failed (slot freed) */
void (*detach)(void* ud, vmsig_core* core, uint32_t vmid, uint32_t endpoint);
void* ud;
} vmsig_discovery_sink;
/* Create discovery over `core`. `watch_dir` (e.g. /dev/shm/vmsig) is scanned once and
* inotify-watched. `probe` NULL => default Proxmox probe over (watch_dir, pve_conf, qmp_dir);
* `sink` NULL => default core trio; `slots_path` NULL => no persistence. Registers the inotify
* + retry-timer loop sources and runs a bootstrap scan. The core owns the lifetime (freed at
* vmsig_core_free via the source on_free). NULL on error. */
vmsig_discovery* vmsig_discovery_new(vmsig_core* core,
const char* watch_dir, const char* pve_conf,
const char* qmp_dir, const char* slots_path,
const vmsig_host_probe* probe,
const vmsig_discovery_sink* sink);
/* Resolve vmid -> endpoint for the admission policy (WS4); -1 if not currently attached. */
int vmsig_discovery_slot_of_vmid(vmsig_discovery* d, uint32_t vmid);
/* TEST-ONLY: drive a file appear(present=1)/gone(present=0) directly, bypassing inotify; and
* force a re-probe of every probing candidate, bypassing the retry timer. Lets the state
* machine be unit-tested deterministically without threads/timers. */
void vmsig_discovery_feed(vmsig_discovery* d, uint32_t vmid, int present);
void vmsig_discovery_tick(vmsig_discovery* d);
#endif /* VMSIG_DISCOVERY_H */
+48
View File
@@ -0,0 +1,48 @@
#ifndef VMSIG_HOST_PROBE_H
#define VMSIG_HOST_PROBE_H
#include <stdint.h>
/* host_probe.h — the platform-coupled discovery seam (private to the discovery module).
*
* This is the ONLY surface that knows the host's config convention (/etc/pve/qemu-server),
* the QMP socket path convention, and the `info mtree` text. It produces a NEUTRAL facts
* struct; discovery.c consumes ONLY that and never names a path convention. A non-Proxmox
* host (or a unit test) injects its own vmsig_host_probe with the same two-stage contract. */
#define VMSIG_HF_NAME_MAX 32
#define VMSIG_HF_PATH_MAX 128
typedef struct {
uint32_t vmid;
char name[VMSIG_HF_NAME_MAX]; /* host VM name (truncated) */
char ram_path[VMSIG_HF_PATH_MAX]; /* guest-RAM backing file (the trigger) */
char qmp_path[VMSIG_HF_PATH_MAX]; /* QMP socket ('@' prefix => abstract) */
uint64_t cfg_ram_bytes; /* RAM size from host config (sanity) */
uint64_t low; /* below-4G split (memctx locator); 0=unknown */
int vm_state; /* VMSIG_VM_* from the liveness oracle */
int share_on; /* memory-backend share=on verified */
int ok; /* 1 => all fail-closed gates passed (attach) */
int retry; /* 1 => transient (QMP not up yet) — back off */
} vmsig_host_facts;
/* Two-stage probe. Stage 1 reads host config (cheap, local). Stage 2 corroborates liveness
* and derives `low` (QMP round-trip, bounded). Splitting them lets the state machine treat
* "config error" (permanent, drop) apart from "QMP not up yet" (transient, retry). */
typedef struct vmsig_host_probe {
/* Populate paths + name + cfg_ram_bytes + share_on from host config; stat the RAM file.
* Sets out->ok=0 on any permanent gate failure (no share=on, missing/oversized file).
* Returns 0 when `out` was populated, -1 on a usage error. */
int (*config)(const struct vmsig_host_probe* p, uint32_t vmid, vmsig_host_facts* out);
/* Corroborate liveness + derive `low` via QMP. Mutates `io`: sets vm_state, low, ok; or
* retry=1 (QMP not reachable yet) / ok=0 (stale: file present but VM dead / unparsable). */
int (*live)(const struct vmsig_host_probe* p, vmsig_host_facts* io);
void* ud; /* implementation-private */
} vmsig_host_probe;
/* The default Proxmox probe over (watch_dir, pve_conf). `qmp_dir` is the QMP socket dir
* (Proxmox: /var/run/qemu-server, socket "<qmp_dir>/<vmid>.qmp"). The returned struct
* references the path strings by pointer — the caller keeps them alive. */
vmsig_host_probe host_probe_proxmox(const char* watch_dir, const char* pve_conf,
const char* qmp_dir);
#endif /* VMSIG_HOST_PROBE_H */
+49
View File
@@ -0,0 +1,49 @@
#ifndef VMSIG_SLOT_H
#define VMSIG_SLOT_H
#include <stdint.h>
/* slot.h — vmid <-> endpoint allocator (private to the discovery module).
*
* The signaling core addresses VMs by an ENDPOINT bit in a 64-bit mask (endpoint < 64). A
* Proxmox vmid (100..1e9) does NOT fit 6 bits, so the binding is a PINNED table, not a pure
* function: a vmid keeps the SAME endpoint across VM restarts (so a control's endpoint_mask
* stays coherent), and the table is persisted so a daemon restart re-derives the same map.
*
* Bit reuse is a coherence event, not a silent alias: a freed bit is handed to a DIFFERENT
* vmid only AFTER the roster DETACH for the old occupant has been published. The discovery
* loop is single-threaded and publishes DETACH synchronously before any later attach, so the
* ordering itself enforces this — the allocator only needs to never double-assign a live bit. */
#define VMSIG_SLOT_COUNT 64
typedef struct {
uint32_t vmid; /* 0 => slot free */
} slot_ent;
typedef struct {
slot_ent ent[VMSIG_SLOT_COUNT];
uint64_t used_mask; /* mirror: bit e set <=> ent[e].vmid != 0 */
} slot_table;
/* Reset to all-free. */
void slot_init(slot_table* t);
/* Endpoint pinned to `vmid`, or -1 if `vmid` is not bound (or 0). */
int slot_lookup(const slot_table* t, uint32_t vmid);
/* Pin `vmid` to a stable endpoint. Idempotent: if `vmid` is already bound, returns its
* existing endpoint. Otherwise assigns the lowest free bit. Returns the endpoint [0,64),
* or -1 if `vmid`==0 or the table is full (the 64-VM ceiling). */
int slot_alloc(slot_table* t, uint32_t vmid);
/* Release the slot bound to `vmid` (no-op if not bound). */
void slot_free(slot_table* t, uint32_t vmid);
/* Persist the table to `path` atomically (tmp + rename), mode 0600. 0 / -1. */
int slot_save(const slot_table* t, const char* path);
/* Load the table from `path`. On a missing/corrupt file, initializes empty and returns 0
* (a fresh start is valid). -1 only on a hard error. */
int slot_load(slot_table* t, const char* path);
#endif /* VMSIG_SLOT_H */