mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-26 04:36:37 +03:00
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:
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user