added9d226
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
62 lines
3.5 KiB
Go
62 lines
3.5 KiB
Go
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"
|
|
}
|