v0.81.0: NAS verify-before-commit — retry=0, journal classifier, detached verify job + auto-rollback
Agent half of the verify-before-commit task (SPIKE-nas-verify-2026-07-11, b57f6c1): retry=0 in the production NFS options (Q4-vi); ClassifyNetVerifyFailure on the live Q4 strings (nfs_export merges not-found/not-permitted); add = sync fast-fail (2s TCP pre-probe, nothing installed) + detached in-memory verify job judging /proc/mounts only, auto-rollback on failure; GET /netstorage/verify-status (phase none = the controller's Scenario-F rollback signal); unprivileged journalctl (systemd-journal group, NO new sudoers grants). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -216,6 +216,19 @@ type Server struct {
|
||||
swapMu sync.Mutex
|
||||
swapInFlight map[int]bool
|
||||
|
||||
// Network-storage verify job (SPIKE-nas-verify): the IN-MEMORY single slot + the seams the
|
||||
// detached pipeline runs through (tests inject; production defaults set in NewServer).
|
||||
netVerifyMu sync.Mutex
|
||||
netVerifyCur *netVerifyJob
|
||||
// netTrigger performs the mount-waking directory read through the automount path (Q1).
|
||||
netTrigger func(where string) error
|
||||
// netMounted judges mount success from /proc/mounts — the §8 truth source (never readability).
|
||||
netMounted func(where string) bool
|
||||
// netJournal reads a mount unit's journal tail UNPRIVILEGED (systemd-journal group, no sudo).
|
||||
netJournal func(ctx context.Context, unit string) (string, error)
|
||||
// netReachable is the 2 s TCP endpoint pre-probe (sync fast-fail + classification tiebreak).
|
||||
netReachable func(proto storage.NetworkProtocol, server string) bool
|
||||
|
||||
baseCtx context.Context // for fire-and-forget backups; set in Run
|
||||
}
|
||||
|
||||
@@ -270,6 +283,10 @@ func NewServer(o Options) (*Server, error) {
|
||||
s.reresolveWipe = s.reresolveDurableForWipe
|
||||
s.reresolveBlank = s.reresolveDurableForBlankFormat
|
||||
s.deviceDurableID = storage.DeviceDurableID
|
||||
s.netTrigger = triggerNetMount
|
||||
s.netMounted = storage.NetworkMountedAt
|
||||
s.netJournal = readUnitJournal
|
||||
s.netReachable = storage.NetworkEndpointReachable
|
||||
if o.ControllerSwap != nil {
|
||||
s.swap = NewControllerSwapper(o.ControllerSwap, o.ControllerSwapStateDir, o.Logger)
|
||||
}
|
||||
@@ -304,8 +321,11 @@ func (s *Server) Handler() http.Handler {
|
||||
|
||||
// Network storage (NAS) — Part A1: mount/list/remove a bulk-media NAS share host-side (automount
|
||||
// idle-unmount; +100000 uid recipe). A distinct class from a drive — no enroll/eject/wipe.
|
||||
// Add is verify-before-commit (SPIKE-nas-verify): it starts a detached verify job the caller
|
||||
// polls on /netstorage/verify-status; a failed verify auto-rolls-back agent-side.
|
||||
mux.HandleFunc("POST /netstorage/add", s.withGuest(s.handleNetStorageAdd))
|
||||
mux.HandleFunc("GET /netstorage", s.withGuest(s.handleNetStorageList))
|
||||
mux.HandleFunc("GET /netstorage/verify-status", s.withGuest(s.handleNetVerifyStatus))
|
||||
mux.HandleFunc("POST /netstorage/remove", s.withGuest(s.handleNetStorageRemove))
|
||||
|
||||
// agentic controller update (Phase 1): in-guest image swap + rollback, owned by the agent.
|
||||
|
||||
Reference in New Issue
Block a user