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:
@@ -0,0 +1,61 @@
|
||||
package storage
|
||||
|
||||
import "strings"
|
||||
|
||||
// Network-mount verify failure classification — the categorized refusal behind verify-before-commit.
|
||||
// The categories and their journal substrings are the LIVE-MEASURED error taxonomy of
|
||||
// SPIKE-nas-verify-2026-07-11 §Q4 (felhom.eu/documentation/audits/): every mount failure exits
|
||||
// rc=32, so classification MUST run on the journal's message strings, never on exit codes — an
|
||||
// exit-code classifier cannot even split wrong-password from wrong-share-name.
|
||||
|
||||
// Verify failure category codes. The controller maps these to the customer-facing Hungarian
|
||||
// messages; the codes themselves are a stable wire vocabulary — do not rename casually.
|
||||
const (
|
||||
NetVerifyUnreachable = "unreachable" // no host behind the endpoint (pre-probe or No route/refused)
|
||||
NetVerifyNFSExport = "nfs_export" // NFS export missing OR not permitted — MERGED (Q4 ii≡iii)
|
||||
NetVerifySMBAuth = "smb_auth" // SMB wrong username/password (mount error(13))
|
||||
NetVerifySMBShare = "smb_share" // SMB share name not found (mount error(2))
|
||||
NetVerifyTimeout = "timeout" // blocked until systemd's 90 s start cap (black-holed-but-routed)
|
||||
NetVerifyMountFailed = "mount_failed" // no diagnostic matched / journal unavailable
|
||||
)
|
||||
|
||||
// netVerifyRule is one first-match-wins row of the classification table. The substrings are
|
||||
// VERBATIM from the spike's Q4 transcripts (mount.nfs4 / mount.cifs / systemd on PVE 8) — matching
|
||||
// is on the stable fragment, tolerant of surrounding version drift.
|
||||
type netVerifyRule struct {
|
||||
substr string
|
||||
code string
|
||||
hint string
|
||||
}
|
||||
|
||||
// netVerifyRules — ordered: protocol-specific diagnostics before generic ones. Note the NFS rule
|
||||
// keys on the full "reason given by server:" fragment, so SMB's "mount error(2): No such file or
|
||||
// directory" can never shadow it (and vice versa).
|
||||
var netVerifyRules = []netVerifyRule{
|
||||
{"No route to host", NetVerifyUnreachable, "no route to the server (retry=0 fast-fail)"},
|
||||
{"Connection refused", NetVerifyUnreachable, "the server refused the connection"},
|
||||
{"Connection timed out", NetVerifyUnreachable, "the connection timed out"},
|
||||
{"reason given by server: No such file or directory", NetVerifyNFSExport,
|
||||
"NFS export not found OR not permitted for this client — NFSv4 cannot distinguish the two (SPIKE Q4 ii≡iii)"},
|
||||
{"mount error(13)", NetVerifySMBAuth, "SMB logon failure (STATUS_LOGON_FAILURE) — wrong username or password"},
|
||||
{"mount error(2)", NetVerifySMBShare, "SMB share not found (BAD_NETWORK_NAME)"},
|
||||
{"Mounting timed out. Terminating", NetVerifyTimeout,
|
||||
"mount blocked until systemd's start timeout — server routed but not answering"},
|
||||
}
|
||||
|
||||
// ClassifyNetVerifyFailure maps a mount unit's journal tail to a verify failure category + an
|
||||
// operator-facing English hint (the Hungarian customer message is the controller's job). Pure,
|
||||
// table-driven, first match wins. tcpReachable (the endpoint pre-probe result at classification
|
||||
// time) only breaks the tie when NO substring matched: an empty/unmatched journal against a
|
||||
// dead endpoint is still `unreachable`, not the generic `mount_failed`.
|
||||
func ClassifyNetVerifyFailure(journalTail string, tcpReachable bool) (code, hint string) {
|
||||
for _, r := range netVerifyRules {
|
||||
if strings.Contains(journalTail, r.substr) {
|
||||
return r.code, r.hint
|
||||
}
|
||||
}
|
||||
if !tcpReachable {
|
||||
return NetVerifyUnreachable, "no mount diagnostic in the journal and the endpoint is not reachable"
|
||||
}
|
||||
return NetVerifyMountFailed, "mount failed with no recognized diagnostic — see the raw journal detail"
|
||||
}
|
||||
Reference in New Issue
Block a user