v0.89.0: pbsdr self-grant (R-22) + escrow config live-reload + agent-plane poke listener (Direction-2a)
- pbsdr: on a 403 pre-check (non-default storage id, no ACL yet) self-grant via the root wrapper then re-read, instead of aborting before the grant — closes F4/R-22. Red-proof TestSelfGrant_PreCheck403DoesNotAbortBeforeGrant. - escrow preflight: late-bound CurrentPBSStorageID re-reads agent.json so a pbsdr-seeded pbs_storage_id flips the row green in-process (no restart). Red-proof TestEscrowPreflight_PBSStorageIDLiveReload. - internal/poke: contentless UDP poke listener bound exclusively to the box WG /32 (port 51822), leading-edge debounced, fires the hub-loop out-of-band trigger for an immediate desired-state cycle. First slice of R-13. Red-proofs TestBindConfinement + TestDebounceCoalescesBurst.
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -41,6 +42,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/mgmtplane"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/pbs"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/pbsdr"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/poke"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/provision"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/reconcile"
|
||||
@@ -53,7 +55,7 @@ import (
|
||||
|
||||
// version is the agent version. Overridable at build time with
|
||||
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
|
||||
var version = "0.86.0"
|
||||
var version = "0.89.0"
|
||||
|
||||
// runGuestHook is the PVE hook body (`felhom-agent guest-hook <vmid> <phase>`). On pre-start it
|
||||
// creates placeholder dirs for any absent bind-mount source so the guest always boots (the C1 net);
|
||||
@@ -728,8 +730,22 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
// the pbsdr manager is constructed further down; the closure reads drConfigured at call time
|
||||
// (nil until then → preflight reports the tier not applied, which is the honest pre-wire answer).
|
||||
escrowCeremonyCfg := &localapi.EscrowCeremonyConfig{
|
||||
SudoPath: cfg.Privileged.SudoPath,
|
||||
PBSStorageID: cfg.Escrow.PBSStorageID,
|
||||
SudoPath: cfg.Privileged.SudoPath,
|
||||
PBSStorageID: cfg.Escrow.PBSStorageID,
|
||||
// Live-reload (v0.89.0): the pbsdr bridge seeds escrow.pbs_storage_id into agent.json on DR
|
||||
// convergence; re-read it from disk at preflight time so the row flips green without a
|
||||
// restart. Reading the same file the ceremony subprocess loads keeps the preflight honest.
|
||||
// On a read error, fall back to the daemon-start snapshot. cfg.SourcePath == "" (all-env
|
||||
// config) → nothing to re-read, the snapshot stands.
|
||||
CurrentPBSStorageID: func() string {
|
||||
if cfg.SourcePath == "" {
|
||||
return cfg.Escrow.PBSStorageID
|
||||
}
|
||||
if c, err := config.Load(cfg.SourcePath); err == nil {
|
||||
return c.Escrow.PBSStorageID
|
||||
}
|
||||
return cfg.Escrow.PBSStorageID
|
||||
},
|
||||
HubConfigured: cfg.Hub.URL != "" && cfg.Hub.HostID != "" && cfg.Hub.APIKey != "",
|
||||
DRConfigured: func() bool {
|
||||
if drConfigured != nil {
|
||||
@@ -771,6 +787,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
// registration, no report stanza.
|
||||
wgServers := 0
|
||||
var wgLoop *wgtunnel.Loop
|
||||
var pokeListener *poke.Listener
|
||||
{
|
||||
wt := cfg.WGTunnel.WithDefaults()
|
||||
if wt.Enabled {
|
||||
@@ -785,6 +802,22 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
desiredSyncer.AddConsumer(wgLoop) // raw desired-state → the wireguard block
|
||||
collector.SetWireguardReporter(wgLoop) // heartbeat status stanza
|
||||
logger.Info("wgtunnel: enabled", "interval_s", wt.IntervalSeconds, "state_dir", wt.StateDir)
|
||||
|
||||
// Agent-plane immediate-sync LISTENER (Direction-2a, v0.89.0). Binds a contentless UDP
|
||||
// poke socket EXCLUSIVELY to the box's WG /32 (from registered.json) and nudges the hub
|
||||
// control loop's out-of-band report trigger — the SAME channel the storage watchdog
|
||||
// uses (fan-in; the loop coalesces). Enabled whenever the tunnel is (WG is the only path
|
||||
// a poke can arrive on); a lost poke is harmless — the 15-min cycle still reconciles.
|
||||
pokeListener = poke.NewListener(
|
||||
func() (netip.Addr, bool) { return wgtunnel.LoadAssignedAddr(wt.StateDir) },
|
||||
func() {
|
||||
select {
|
||||
case storageTrigger <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
},
|
||||
poke.Port, logger)
|
||||
logger.Info("poke: agent-plane sync listener enabled", "port", poke.Port)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,13 +873,16 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
// Run reconcile, the hub loop, the storage watchdog, the restore-test scheduler, the PBS
|
||||
// verify loop, (optionally) the local-API server, and (optionally) the LAN resolver loop
|
||||
// concurrently; any one returning ends the daemon (ctx cancel tears down the rest).
|
||||
errc := make(chan error, 7)
|
||||
errc := make(chan error, 8)
|
||||
go func() { errc <- engine.Run(ctx, interval) }()
|
||||
go func() { errc <- loop.Run(ctx) }()
|
||||
go func() { errc <- watchdog.Run(ctx) }()
|
||||
go func() { errc <- scheduler.Run(ctx) }()
|
||||
go func() { errc <- pbsLoop.Run(ctx) }()
|
||||
go func() { errc <- pbsdrLoop.Run(ctx) }()
|
||||
if pokeListener != nil {
|
||||
go func() { errc <- pokeListener.Run(ctx) }() // agent-plane immediate-sync listener (v0.89.0)
|
||||
}
|
||||
if localSrv != nil {
|
||||
localServers = 1
|
||||
// Host-reboot remount fix: BEFORE binding into the guest, re-assert enrolled drive MOUNTS on the
|
||||
|
||||
Reference in New Issue
Block a user