agent v0.50.0: NAS network storage Part A1 (NFS/SMB automount foundation)

Host-side NFS/SMB automount of a bulk-media NAS share under /mnt/felhom-drives/<name>
(propagates into the guest via the existing shared bind), the +100000 uid recipe,
per-share liveness, and add/list/remove local-API endpoints. A NAS is a distinct class
that bypasses the drive enroll/eject/decommission/SMART/watchdog machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
2026-06-30 11:12:27 +02:00
parent 7aeb7caefe
commit 63aa63d0d6
10 changed files with 1351 additions and 5 deletions
+10 -4
View File
@@ -45,7 +45,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.49.0"
var version = "0.50.0"
// runGuestHook is the PVE pre-start hook body (`felhom-agent guest-hook <vmid> <phase>`). On the
// pre-start phase it creates placeholder dirs for any absent bind-mount source so the guest always boots
@@ -216,7 +216,10 @@ func newProxmoxClient(cfg config.Config) (*proxmox.Client, error) {
// every argument is validated in internal/storage before any command is built. A
// missing/declined sudoers entry degrades per-op (SMART→UNKNOWN, mount→logged error), not a
// crash.
func newHostOps(cfg config.Config, logger *slog.Logger) storage.HostOps {
// Returns the concrete *storage.SudoHostOps (not the HostOps interface) so callers that need the
// methods outside that lean interface — the host-reboot mount re-assert and the Part-A1 network-mount
// surface — can reach them without a type assertion. It still satisfies storage.HostOps everywhere.
func newHostOps(cfg config.Config, logger *slog.Logger) *storage.SudoHostOps {
mode := proxmox.RunnerMode(cfg.Privileged.Mode)
if mode == "" {
mode = proxmox.RunnerSudo
@@ -549,7 +552,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
// re-enumeration can move the device (/dev/sdb→sdc); ReassertEnrolledMounts re-resolves each by
// filesystem UUID and re-mounts (idempotent `enable --now`) so a letter reshuffle is a no-op.
// Type-asserted (the concrete op exposes it; the interface stays lean).
mountReasserter, _ := hostOps.(*storage.SudoHostOps)
mountReasserter := hostOps // concrete *storage.SudoHostOps — exposes ReassertEnrolledMounts
if mountReasserter != nil {
mountReasserter.ReassertEnrolledMounts(ctx)
}
@@ -699,7 +702,7 @@ func buildRestoreTestScheduler(cfg config.Config, px *proxmox.Client, engine *re
// leaf (stable fingerprint). Any failure DISABLES the server (returns nil) WITHOUT crashing the
// daemon — the host still reports/reconciles; only the controller channel is unavailable until
// fixed. The opened token store is returned via outTokens so the caller can Close it.
func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.Store, observer *storage.Observer, hostOps storage.HostOps, gate *reconcile.Gate, collector *hub.Collector, intent localapi.IntentRecorder, guestBinds *localapi.GuestBindStore, formatJobs *localapi.FormatJobStore, logger *slog.Logger, outTokens **localapi.TokenStore) *localapi.Server {
func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.Store, observer *storage.Observer, hostOps *storage.SudoHostOps, gate *reconcile.Gate, collector *hub.Collector, intent localapi.IntentRecorder, guestBinds *localapi.GuestBindStore, formatJobs *localapi.FormatJobStore, logger *slog.Logger, outTokens **localapi.TokenStore) *localapi.Server {
if !cfg.LocalAPI.Enabled() {
return nil
}
@@ -751,6 +754,9 @@ 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
// Network storage (NAS) — Part A1: the privileged host network-mount surface (NFS/SMB automount).
NetStorage: hostOps,
SmbCredsDir: cfg.Privileged.SmbCredsDir,
ControllerSwap: guestBinder, // Phase 1: agentic controller update — in-guest image swap
// F2-b: recover a guest left with a stale vzdump lock by a reboot-during-backup. Reads + start
// go through the API client; the `pct unlock` is the one fenced root-CLI op (no API equivalent).