mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 16:27:37 +00:00
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.
158 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|