The internal data place, partitions as places, add_internal_place (felhom.eu architecture/16 §5-§7, §9; build B)
gates / gates (push) Successful in 27s

- internal place: the record /var/lib/felhom-agent/internal-place.json -> mounted at /mnt/belso by fs-UUID like an
  enrolled drive (startup + 20 s tick), kind "internal" in GET /disks; never takes a household drive's /mnt/belso;
  eject/decommission/format refused (dm on the system disk).
- configs/felhom-internal-place: the ruled size rule and the only lvcreate/mkfs of a system-disk volume (installer +
  the new signed op add_internal_place, operator key, no parameters).
- partitions listed per free disk (empty / filesystem / in_use); a data-bearing part's wipe id binds disk id + part
  number + fs-UUID.
- felhom-mkfs-guarded: Felhom's own sibling drive no longer blocks formatting the free part; an unlistable disk refuses.
Tests red-proved (script mutations, Go mutations, the old wrapper measured against the new tests).

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
admin committed 2026-10-16 03:55:47 +02:00
1 parent e569ec6c5f
commit 912f7c127e
24 files changed
+1591 -69

No files matched your search

+26 -9
View File
@@ -959,10 +959,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
// state. Gates the watchdog's self-heal re-mount to ENROLLED drives, and the local API records
// enroll/eject into it. Open failure → ungated legacy remount (logged) so the daemon still runs.
var intentReader storage.IntentReader
intentStateDir := "/var/lib/felhom-agent"
if cfg.Authz.NonceStorePath != "" {
intentStateDir = filepath.Dir(cfg.Authz.NonceStorePath) // share the agent's durable state dir
}
intentStateDir := agentStateDir(cfg)
intentStore, ierr := storage.OpenIntentStore(filepath.Join(intentStateDir, "drive-intents.json"))
if ierr != nil {
logger.Warn("storage: intent store unavailable — self-heal runs UNGATED (legacy remount-any)", "err", ierr)
@@ -1168,7 +1165,11 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
}
logger.Warn("osupdate: capability probe after the config bundle", "ok", ok, "total", total, "degraded", strings.Join(names, ","))
}}
jobsRunner := signedjobs.NewRunner(client, gate, signedjobs.ExecutorChain{wipeExec, decommExec, updateExec, dockerExec, pveExec, kernelExec, bundleExec}, cfg.Hub.HostID, logger)
// v0.158.0 (felhom.eu `16` §9): the internal data place on an old box — the root helper, then the mount (late-bound
// to the local API server built below).
internalPlaceExec := signedjobs.NewInternalPlaceExecutor(suRunner,
localapi.InternalPlaceFileIn(intentStateDir), logger)
jobsRunner := signedjobs.NewRunner(client, gate, signedjobs.ExecutorChain{wipeExec, decommExec, updateExec, dockerExec, pveExec, kernelExec, bundleExec, internalPlaceExec}, cfg.Hub.HostID, logger)
loop.SetEnvelopeObserver(hub.MultiObserver(desiredSyncer, jobsRunner))
// Controller-driven escrow ceremony (v0.88.0): static config facts + the LATE-BOUND DR gate —
@@ -1201,6 +1202,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
}
localSrv := buildLocalAPIServer(cfg, px, backupStore, heavyOps, observer, driveKnown, hostOps, gate, collector, client, intentRec, guestBindStore, formatJobStore, logRing, escrowCeremonyCfg, logger, &localTokens)
if localSrv != nil {
internalPlaceExec.SetEnsure(localSrv.EnsureInternalPlace)
// R-998 (doc 14): POST /host/poweroff — guest shutdown, then host power-off; guards in hostpoweroff.go.
pwMode := proxmox.RunnerMode(cfg.Privileged.Mode)
if pwMode == "" {
@@ -1419,6 +1421,9 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
// 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 // concrete *storage.SudoHostOps — exposes ReassertEnrolledMounts
// v0.158.0 (felhom.eu `16` §5): the internal data place is mounted like an enrolled drive — first, so the
// re-assert below sees its unit.
localSrv.EnsureInternalPlace(ctx)
if mountReasserter != nil {
mountReasserter.ReassertEnrolledMounts(ctx)
}
@@ -1456,6 +1461,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
case <-t.C:
// Re-assert mounts first (handles a USB that enumerated late after a host reboot, or a
// unit re-disabled at runtime), then re-assert the guest binds against the now-live mounts.
localSrv.EnsureInternalPlace(ctx) // `16` §5: a record written since the last tick (the signed job)
if mountReasserter != nil {
mountReasserter.ReassertEnrolledMounts(ctx)
}
@@ -2023,10 +2029,12 @@ func buildLocalAPIServer(cfg config.Config, px *proxmox.Client, store *backup.St
// 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).
// A1 (v0.62.0): the scan is restricted to felhom-pool members (ownership proven, not assumed).
StaleLock: localapi.NewStaleLockController(px, &proxmox.ExecRunner{Mode: gaMode, SudoPath: cfg.Privileged.SudoPath}, reconcile.DefaultPool, logger),
Intent: intent, // slice 10 P3: record enroll/eject intent for self-heal
GuestBinds: guestBinds, // F9: per-guest bind record for the startup re-assert
FormatJobs: formatJobs, // F20-BUG3: detached-format job record + restart recovery
StaleLock: localapi.NewStaleLockController(px, &proxmox.ExecRunner{Mode: gaMode, SudoPath: cfg.Privileged.SudoPath}, reconcile.DefaultPool, logger),
Intent: intent, // slice 10 P3: record enroll/eject intent for self-heal
// v0.158.0 (felhom.eu `16` §5): the internal data place record, beside the intent store.
InternalPlaceFile: localapi.InternalPlaceFileIn(agentStateDir(cfg)),
GuestBinds: guestBinds, // F9: per-guest bind record for the startup re-assert
FormatJobs: formatJobs, // F20-BUG3: detached-format job record + restart recovery
// Host metrics (slice 9): the shared collector serves GET /host/metrics — a fresh host +
// per-storage view to the customer's monitoring page (reuses the slice-4 collector).
@@ -3905,3 +3913,12 @@ func (f *factsReporter) SystemFacts(ctx context.Context) (int, json.RawMessage,
f.facts, f.err = f.leg.Facts(ctx, f.vmid)
return f.vmid, f.facts, f.err
}
// agentStateDir is the agent's durable state dir: beside the nonce store when one is configured, else the default.
// The intent store, the guest-bind record and the internal place record (v0.158.0) all live here.
func agentStateDir(cfg config.Config) string {
if cfg.Authz.NonceStorePath != "" {
return filepath.Dir(cfg.Authz.NonceStorePath)
}
return "/var/lib/felhom-agent"
}
+1 -1
View File
@@ -43,7 +43,7 @@ func main() {
func run() error {
var (
op = flag.String("op", "", "op class to sign, e.g. storage_wipe | guest_destroy | decommission | agent_update | os_docker_step | os_pve_step | os_kernel_step | agent_config_update")
op = flag.String("op", "", "op class to sign, e.g. storage_wipe | guest_destroy | decommission | agent_update | os_docker_step | os_pve_step | os_kernel_step | agent_config_update | add_internal_place")
host = flag.String("host", "", "target host_id (anti-retarget — the op runs ONLY on this host)")
guest = flag.String("guest", "", "target guest_id (\"\" = host-scoped op)")
keyID = flag.String("key-id", "", "key id of the signing key (must match a pinned agent signer)")