Prefer live monitor connection state in diagnostics

Back-port v5 fix d310c257a to v6, adapted to v6's connection-status key
format. computeDiagnostics now merges a failed PVE/PBS diagnostics probe
with the monitor's live connection state: if the long-running poller still
reports the instance connected, a transient probe failure (network blip,
TLS re-check) no longer flips it to 'disconnected' in the UI. Uses v6's
'pve-<name>'/'pbs-<name>' status keys (v5 used a bare node name, which
would not match in v6). Adds a merge-logic regression test.
This commit is contained in:
rcourtman
2026-06-04 10:07:10 +01:00
parent 6b6684a119
commit 8855b78c0d
2 changed files with 66 additions and 0 deletions
+33
View File
@@ -795,6 +795,33 @@ func writeDiagnosticsResponse(w http.ResponseWriter, diag DiagnosticsInfo, cache
}
}
// diagnosticsMonitorConnectionStatus looks up the live monitor connection state
// for a node by its connection-status key (e.g. "pve-<name>"/"pbs-<name>").
func diagnosticsMonitorConnectionStatus(m *monitoring.Monitor, key string) (bool, bool) {
if m == nil || strings.TrimSpace(key) == "" {
return false, false
}
statuses := m.GetConnectionStatuses()
connected, ok := statuses[key]
return connected, ok
}
// mergeDiagnosticsConnection prevents a transient diagnostics-probe failure from
// reporting an instance as disconnected when the long-running monitor connection
// still reports it connected.
func mergeDiagnosticsConnection(probeConnected bool, probeError string, monitorConnected bool, hasMonitorStatus bool) (bool, string) {
if probeConnected {
return true, probeError
}
if !hasMonitorStatus || !monitorConnected {
return false, probeError
}
if strings.TrimSpace(probeError) == "" {
return true, ""
}
return true, "Live diagnostics probe failed, but the monitor still reports this instance connected: " + probeError
}
func (r *Router) computeDiagnostics(ctx context.Context) DiagnosticsInfo {
diag := EmptyDiagnosticsInfo()
@@ -833,6 +860,7 @@ func (r *Router) computeDiagnostics(ctx context.Context) DiagnosticsInfo {
Host: node.Host,
Type: "pve",
}
monitorConnected, hasMonitorStatus := diagnosticsMonitorConnectionStatus(r.monitor, "pve-"+node.Name)
// Determine auth method (sanitized - don't expose actual values)
if node.TokenName != "" && node.TokenValue != "" {
@@ -860,12 +888,14 @@ func (r *Router) computeDiagnostics(ctx context.Context) DiagnosticsInfo {
nodeDiag.Connected = false
nodeDiag.Error = "Failed to initialize connection"
log.Error().Err(err).Str("node", node.Name).Msg("Diagnostics: Proxmox client init failed")
nodeDiag.Connected, nodeDiag.Error = mergeDiagnosticsConnection(nodeDiag.Connected, nodeDiag.Error, monitorConnected, hasMonitorStatus)
} else {
nodes, err := client.GetNodes(ctx)
if err != nil {
nodeDiag.Connected = false
nodeDiag.Error = "Failed to connect to Proxmox API"
log.Error().Err(err).Str("node", node.Name).Msg("Diagnostics: Proxmox API connection failed")
nodeDiag.Connected, nodeDiag.Error = mergeDiagnosticsConnection(nodeDiag.Connected, nodeDiag.Error, monitorConnected, hasMonitorStatus)
} else {
nodeDiag.Connected = true
@@ -903,6 +933,7 @@ func (r *Router) computeDiagnostics(ctx context.Context) DiagnosticsInfo {
Name: pbsNode.Name,
Host: pbsNode.Host,
}
monitorConnected, hasMonitorStatus := diagnosticsMonitorConnectionStatus(r.monitor, "pbs-"+pbsNode.Name)
testCfg := pbs.ClientConfig{
Host: pbsNode.Host,
@@ -919,11 +950,13 @@ func (r *Router) computeDiagnostics(ctx context.Context) DiagnosticsInfo {
pbsDiag.Connected = false
pbsDiag.Error = "Failed to initialize connection"
log.Error().Err(err).Str("pbs", pbsNode.Name).Msg("Diagnostics: PBS client init failed")
pbsDiag.Connected, pbsDiag.Error = mergeDiagnosticsConnection(pbsDiag.Connected, pbsDiag.Error, monitorConnected, hasMonitorStatus)
} else {
if version, err := client.GetVersion(ctx); err != nil {
pbsDiag.Connected = false
pbsDiag.Error = "Connection established but version check failed"
log.Error().Err(err).Str("pbs", pbsNode.Name).Msg("Diagnostics: PBS version check failed")
pbsDiag.Connected, pbsDiag.Error = mergeDiagnosticsConnection(pbsDiag.Connected, pbsDiag.Error, monitorConnected, hasMonitorStatus)
} else {
pbsDiag.Connected = true
pbsDiag.Details = &PBSDetails{Version: version.Version}
+33
View File
@@ -765,3 +765,36 @@ func TestResolveGroupName(t *testing.T) {
})
}
}
func TestMergeDiagnosticsConnection(t *testing.T) {
cases := []struct {
name string
probeConnected bool
probeError string
monitorConnected bool
hasMonitorStatus bool
wantConnected bool
wantErrContains string
}{
{"probe connected stays connected", true, "", false, false, true, ""},
{"probe failed but monitor connected", false, "timeout", true, true, true, "monitor still reports"},
{"probe failed and no monitor status", false, "timeout", false, false, false, "timeout"},
{"probe failed and monitor disconnected", false, "timeout", false, true, false, "timeout"},
{"probe failed empty error, monitor connected", false, "", true, true, true, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotConnected, gotErr := mergeDiagnosticsConnection(tc.probeConnected, tc.probeError, tc.monitorConnected, tc.hasMonitorStatus)
if gotConnected != tc.wantConnected {
t.Fatalf("connected = %v, want %v", gotConnected, tc.wantConnected)
}
if tc.wantErrContains == "" {
if gotErr != "" {
t.Fatalf("expected empty error, got %q", gotErr)
}
} else if !strings.Contains(gotErr, tc.wantErrContains) {
t.Fatalf("error %q does not contain %q", gotErr, tc.wantErrContains)
}
})
}
}