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" }