v0.90.0 — guest RAM resize (R-24) + fast-tick-until-convergence (R-28)
MinAgent coupling: felhom-controller v0.143.0 gates its guest-memory-resize UI on this agent (FeatureGuestMemoryResize, MinAgent 0.90.0). R-24 guest RAM resize (internal/localapi/guestmemory.go): self-scoped GET/POST /guest/memory. Agent enforces every bound FRESH per request (min 2048, max host_total-2048, shrink floor max(2048, usage+512)); applies via PVE SetConfig — live cgroup apply, no reboot (Phase-0 proven on the nested demo box). Verify-after-apply re-reads maxmem before claiming success. New narrow MemoryOps seam (GuestAPI untouched); Options.Memory nil -> 503. Memory only. R-28 fast-tick (internal/fasttick): while any desired-state item is unapplied - including the pre-tunnel window a hub poke can't reach - pulse the shared out-of-band trigger every 30s, self-disarm on convergence. Four cached sources (desired-gen==0, reconcile Planned-Pending>0, pbsdr waiting_secret only, wgtunnel desired-not-operational); LOUD pbsdr states + pending_signature excluded. Seams: reconcile.Engine.LastResult() + wgtunnel.Manager.TunnelConvergence() (cached, no per-tick exec). Guests-0/0: hypothesis REFUTED live (9201 IS a pool member; 0/0 was the pre-provision window; PoolAddVMID re-assert already covers restore-over-existing). No code change; the fast-tick mitigates the window. Tests + red-proofs (i floor guard, ii max guard, iii always-pulse) all restored green.
This commit is contained in:
@@ -33,6 +33,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/desired"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/dr"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/escrow"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/fasttick"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/felhomsshd"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/guesthook"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
||||
@@ -787,6 +788,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
// registration, no report stanza.
|
||||
wgServers := 0
|
||||
var wgLoop *wgtunnel.Loop
|
||||
var wgMgr *wgtunnel.Manager // hoisted for the fast-tick convergence source (nil when tunnel disabled)
|
||||
var pokeListener *poke.Listener
|
||||
{
|
||||
wt := cfg.WGTunnel.WithDefaults()
|
||||
@@ -796,7 +798,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
wtMode = proxmox.RunnerSudo
|
||||
}
|
||||
wtRunner := &proxmox.ExecRunner{Mode: wtMode, SudoPath: cfg.Privileged.SudoPath}
|
||||
wgMgr := wgtunnel.NewManager(wtRunner, client, wt.StateDir, logger)
|
||||
wgMgr = wgtunnel.NewManager(wtRunner, client, wt.StateDir, logger)
|
||||
wgMgr.SetStaleAfter(time.Duration(wt.StaleAfterSeconds) * time.Second)
|
||||
wgLoop = wgtunnel.NewLoop(wgMgr, time.Duration(wt.IntervalSeconds)*time.Second, logger)
|
||||
desiredSyncer.AddConsumer(wgLoop) // raw desired-state → the wireguard block
|
||||
@@ -883,6 +885,44 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
if pokeListener != nil {
|
||||
go func() { errc <- pokeListener.Run(ctx) }() // agent-plane immediate-sync listener (v0.89.0)
|
||||
}
|
||||
|
||||
// Fast-tick (v0.90.0, R-28): the agent-plane immediacy SECONDARY. While ANY desired-state item is
|
||||
// unapplied — most importantly the pre-tunnel window a hub poke cannot reach — pulse the SAME
|
||||
// out-of-band trigger the watchdog/poke use, every 30 s, and self-disarm the instant everything
|
||||
// converges. Every source is a cached read (no exec/network per tick). The LOUD pbsdr states
|
||||
// (consumed_failed/verify_failed) and the destructive pending_signature drift are deliberately
|
||||
// EXCLUDED (§8) so a stuck-loud box never hammers.
|
||||
fastTick := fasttick.New(storageTrigger, fasttick.DefaultInterval, logger,
|
||||
fasttick.SourceFunc(func() (bool, string) {
|
||||
if desiredProvider.Generation() == 0 {
|
||||
return true, "desired-state not yet fetched"
|
||||
}
|
||||
return false, ""
|
||||
}),
|
||||
fasttick.SourceFunc(func() (bool, string) {
|
||||
if res, ok := engine.LastResult(); ok && res.Planned-res.Pending > 0 {
|
||||
return true, "reconcile drift not yet applied"
|
||||
}
|
||||
return false, ""
|
||||
}),
|
||||
fasttick.SourceFunc(func() (bool, string) {
|
||||
if pbsdrLoop != nil {
|
||||
if st := pbsdrLoop.PBSDRStatus(ctx); st != nil && st.State == "waiting_secret" {
|
||||
return true, "pbsdr waiting for its consume-once secret"
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}),
|
||||
fasttick.SourceFunc(func() (bool, string) {
|
||||
if wgMgr != nil {
|
||||
if desired, operational := wgMgr.TunnelConvergence(); desired && !operational {
|
||||
return true, "wireguard tunnel not yet operational"
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}),
|
||||
)
|
||||
go func() { errc <- fastTick.Run(ctx) }()
|
||||
if localSrv != nil {
|
||||
localServers = 1
|
||||
// Host-reboot remount fix: BEFORE binding into the guest, re-assert enrolled drive MOUNTS on the
|
||||
@@ -1178,6 +1218,7 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
|
||||
DiskGate: storageGateAdapter{gate: gate, hostID: cfg.Hub.HostID},
|
||||
Guests2: px,
|
||||
GuestAttach: guestBinder, // slice 10 P2: bind enrolled data drives into the guest
|
||||
Memory: px, // v0.90.0 R-24: guest RAM resize (SetConfig live cgroup apply)
|
||||
// Network storage (NAS) — Part A1: the privileged host network-mount surface (NFS/SMB automount).
|
||||
NetStorage: hostOps,
|
||||
SmbCredsDir: cfg.Privileged.SmbCredsDir,
|
||||
|
||||
Reference in New Issue
Block a user