Pulse/internal/config/vmware.go
rcourtman 406b966b86 v6(settings): phase 8 — per-surface Monitor* scope for TrueNAS and VMware
Adds positive MonitorDatasets/Pools/Replication booleans to TrueNASInstance
and MonitorVMs/Hosts/Datastores to VMwareVCenterInstance, matching the
PVE/PBS/PMG scope pattern. NewInstance defaults all surfaces to true;
ApplyDefaults migrates legacy all-false records to all-true so existing
truenas.json / vmware.json on disk continue monitoring after upgrade.

The unified connections aggregator now reads those booleans into the
Scope map and flips SupportsScope to true for both types, so the Scope
UI in the ConnectionEditor is a straight wire-through to the native
config fields — no new storage or adapter layer. Per-type API clients,
form state, and the TrueNAS and VMware credential slots render the same
three-checkbox "Collection scope" panel used by PVE/PBS/PMG, replacing
the old per-type Stop-this-surface dialog end-to-end from the editor's
side.

Contracts updated: agent-lifecycle, api-contracts, storage-recovery.
Tests: truenas.test.ts and vmware.test.ts round-trip the new monitor*
flags through list + update payloads; config tests cover the legacy
all-false ApplyDefaults migration; aggregator test asserts the scope map
and SupportsScope: true for both types.

Provider Refresh paths still fetch everything in one trip; honoring
Monitor* inside VMware and TrueNAS pollers is deferred to a follow-up.
2026-04-19 15:02:18 +01:00

106 lines
3.3 KiB
Go

package config
import (
"fmt"
"strings"
"github.com/google/uuid"
)
const vmwareSensitiveMask = "********"
const defaultVMwarePort = 443
// IsVMwareSensitiveMask reports whether the value is the redacted placeholder
// used by the VMware settings API.
func IsVMwareSensitiveMask(value string) bool {
return strings.TrimSpace(value) == vmwareSensitiveMask
}
// VMwareVCenterInstance represents a configured VMware vCenter endpoint.
type VMwareVCenterInstance struct {
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
Enabled bool `json:"enabled"`
// Per-surface collection scope. Positive booleans. Existing records
// that predate these fields have all-zero Monitor* values —
// ApplyDefaults treats that as "legacy" and enables every surface so
// behavior doesn't silently change on upgrade.
MonitorVMs bool `json:"monitorVms"`
MonitorHosts bool `json:"monitorHosts"`
MonitorDatastores bool `json:"monitorDatastores"`
}
// NewVMwareVCenterInstance returns a new instance with generated ID and sane
// defaults.
func NewVMwareVCenterInstance() VMwareVCenterInstance {
return VMwareVCenterInstance{
ID: uuid.NewString(),
Port: defaultVMwarePort,
Enabled: true,
MonitorVMs: true,
MonitorHosts: true,
MonitorDatastores: true,
}
}
// ApplyDefaults normalizes legacy zero-value config onto the canonical stored
// defaults without changing explicitly configured values.
func (v *VMwareVCenterInstance) ApplyDefaults() {
if v == nil {
return
}
if v.Port <= 0 {
v.Port = defaultVMwarePort
}
// Legacy records predate per-surface scope booleans. Zero-value false
// across all three means "never configured" — enable everything so
// existing users keep full visibility after upgrade.
if !v.MonitorVMs && !v.MonitorHosts && !v.MonitorDatastores {
v.MonitorVMs = true
v.MonitorHosts = true
v.MonitorDatastores = true
}
}
// Validate performs required VMware configuration checks.
func (v *VMwareVCenterInstance) Validate() error {
if v == nil {
return fmt.Errorf("vmware vcenter instance is required")
}
if strings.TrimSpace(v.Host) == "" {
return fmt.Errorf("vmware vcenter host is required")
}
if strings.TrimSpace(v.Username) == "" || strings.TrimSpace(v.Password) == "" {
return fmt.Errorf("vmware credentials are required: provide username and password")
}
return nil
}
// Redacted returns a copy with sensitive credentials masked.
func (v *VMwareVCenterInstance) Redacted() VMwareVCenterInstance {
if v == nil {
return VMwareVCenterInstance{}
}
redacted := *v
if strings.TrimSpace(redacted.Password) != "" {
redacted.Password = vmwareSensitiveMask
}
return redacted
}
// PreserveMaskedSecrets restores stored credentials when an update payload uses
// the API redaction placeholder for unchanged secret fields.
func (v *VMwareVCenterInstance) PreserveMaskedSecrets(existing VMwareVCenterInstance) {
if v == nil {
return
}
if IsVMwareSensitiveMask(v.Password) {
v.Password = existing.Password
}
}