Pulse/internal/monitoring/monitor_client_init.go
rcourtman 9c3d96cab2 Add unified connections API (list + probe) with Disabled flag
Introduces GET /api/connections and POST /api/connections/probe as the
backend half of the one-ledger / one-editor connection redesign.

- GET /api/connections aggregates PVE/PBS/PMG/VMware/TrueNAS/agent rows
  into a unified Connection shape with derived state (active, paused,
  unauthorized, unreachable, stale, pending) computed from in-memory
  scheduler health plus agent Host.LastSeen. No new persisted state.
- POST /api/connections/probe fingerprints a host across the five
  supported products in parallel (2s dial + 1s read, 3s total, max 5
  concurrent). Admin-gated (RequireAdmin + ScopeSettingsWrite) to block
  unauthenticated SSRF against internal hosts.
- Disabled bool on PVEInstance/PBSInstance/PMGInstance (zero-value =
  enabled, preserves existing nodes.json); pollers skip disabled
  instances at client init, reconnect, and per-node iteration.
- NodeConfigRequest/Response gain Enabled; write path translates
  *bool -> Disabled so omitted field leaves state untouched.
- ConnectionsAPI frontend client (list/probe) typed off the Go shape.

Contracts updated: api-contracts, monitoring, agent-lifecycle,
performance-and-scalability, storage-recovery. Proofs added:
contract_test.go JSON snapshot for Connection and ProbeResponse,
monitoring guardrails for the Disabled-skip behavior, and a vitest
mock-client test for ConnectionsAPI.

Frontend editor / drawer / table rewrite lands in a separate block.
2026-04-19 11:42:53 +01:00

158 lines
5.3 KiB
Go

package monitoring
import (
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring/errors"
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
"github.com/rs/zerolog/log"
)
func (m *Monitor) initPVEClients(cfg *config.Config) {
log.Info().Int("count", len(cfg.PVEInstances)).Msg("initializing PVE clients")
for _, pve := range cfg.PVEInstances {
if pve.Disabled {
log.Info().Str("instance", pve.Name).Msg("Skipping PVE client init: instance is paused")
continue
}
log.Info().
Str("name", pve.Name).
Str("host", pve.Host).
Str("user", pve.User).
Bool("hasToken", pve.TokenName != "").
Msg("Configuring PVE instance")
// Check if this is a cluster
if pve.IsCluster && len(pve.ClusterEndpoints) > 0 {
endpoints, endpointFingerprints := m.buildClusterEndpointsForInit(pve)
log.Info().
Str("cluster", pve.ClusterName).
Strs("endpoints", endpoints).
Int("fingerprints", len(endpointFingerprints)).
Msg("Creating cluster-aware client")
clientConfig := config.CreateProxmoxConfig(&pve)
clientConfig.Timeout = cfg.ConnectionTimeout
clusterClient := proxmox.NewClusterClient(
pve.Name,
clientConfig,
endpoints,
endpointFingerprints,
)
m.pveClients[pve.Name] = clusterClient
log.Info().
Str("instance", pve.Name).
Str("cluster", pve.ClusterName).
Int("endpoints", len(endpoints)).
Msg("Cluster client created successfully")
// Set initial connection health to true for cluster
m.setProviderConnectionHealth(InstanceTypePVE, pve.Name, true)
continue
}
// Create regular client
clientConfig := config.CreateProxmoxConfig(&pve)
clientConfig.Timeout = cfg.ConnectionTimeout
client, err := newProxmoxClientFunc(clientConfig)
if err != nil {
monErr := errors.WrapConnectionError("create_pve_client", pve.Name, err)
log.Error().
Err(monErr).
Str("instance", pve.Name).
Str("host", pve.Host).
Str("user", pve.User).
Bool("hasPassword", pve.Password != "").
Bool("hasToken", pve.TokenValue != "").
Msg("Failed to create PVE client - node will show as disconnected")
// Set initial connection health to false for this node
m.setProviderConnectionHealth(InstanceTypePVE, pve.Name, false)
continue
}
m.pveClients[pve.Name] = client
log.Info().Str("instance", pve.Name).Msg("PVE client created successfully")
// Set initial connection health to true
m.setProviderConnectionHealth(InstanceTypePVE, pve.Name, true)
}
}
func (m *Monitor) initPBSClients(cfg *config.Config) {
log.Info().Int("count", len(cfg.PBSInstances)).Msg("initializing PBS clients")
for _, pbsInst := range cfg.PBSInstances {
if pbsInst.Disabled {
log.Info().Str("instance", pbsInst.Name).Msg("Skipping PBS client init: instance is paused")
continue
}
log.Info().
Str("name", pbsInst.Name).
Str("host", pbsInst.Host).
Str("user", pbsInst.User).
Bool("hasToken", pbsInst.TokenName != "").
Msg("Configuring PBS instance")
clientConfig := config.CreatePBSConfig(&pbsInst)
clientConfig.Timeout = 60 * time.Second // Very generous timeout for slow PBS servers
client, err := pbs.NewClient(clientConfig)
if err != nil {
monErr := errors.WrapConnectionError("create_pbs_client", pbsInst.Name, err)
log.Error().
Err(monErr).
Str("instance", pbsInst.Name).
Str("host", pbsInst.Host).
Str("user", pbsInst.User).
Bool("hasPassword", pbsInst.Password != "").
Bool("hasToken", pbsInst.TokenValue != "").
Msg("Failed to create PBS client - node will show as disconnected")
// Set initial connection health to false for this node
m.setProviderConnectionHealth(InstanceTypePBS, pbsInst.Name, false)
continue
}
m.pbsClients[pbsInst.Name] = client
log.Info().Str("instance", pbsInst.Name).Msg("PBS client created successfully")
// Set initial connection health to true
m.setProviderConnectionHealth(InstanceTypePBS, pbsInst.Name, true)
}
}
func (m *Monitor) initPMGClients(cfg *config.Config) {
log.Info().Int("count", len(cfg.PMGInstances)).Msg("initializing PMG clients")
for _, pmgInst := range cfg.PMGInstances {
if pmgInst.Disabled {
log.Info().Str("instance", pmgInst.Name).Msg("Skipping PMG client init: instance is paused")
continue
}
log.Info().
Str("name", pmgInst.Name).
Str("host", pmgInst.Host).
Str("user", pmgInst.User).
Bool("hasToken", pmgInst.TokenName != "").
Msg("Configuring PMG instance")
clientConfig := config.CreatePMGConfig(&pmgInst)
if clientConfig.Timeout <= 0 {
clientConfig.Timeout = 45 * time.Second
}
client, err := pmg.NewClient(clientConfig)
if err != nil {
monErr := errors.WrapConnectionError("create_pmg_client", pmgInst.Name, err)
log.Error().
Err(monErr).
Str("instance", pmgInst.Name).
Str("host", pmgInst.Host).
Str("user", pmgInst.User).
Bool("hasPassword", pmgInst.Password != "").
Bool("hasToken", pmgInst.TokenValue != "").
Msg("Failed to create PMG client - gateway will show as disconnected")
m.setProviderConnectionHealth(InstanceTypePMG, pmgInst.Name, false)
continue
}
m.pmgClients[pmgInst.Name] = client
log.Info().Str("instance", pmgInst.Name).Msg("PMG client created successfully")
m.setProviderConnectionHealth(InstanceTypePMG, pmgInst.Name, true)
}
}