Pulse/internal/api/vmware_handlers.go

990 lines
29 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
)
const vmwareConnectionsPathPrefix = "/api/vmware/connections/"
// VMwareHandlers manages VMware vCenter connection CRUD and connectivity checks.
type VMwareHandlers struct {
getPersistence func(ctx context.Context) *config.ConfigPersistence
getMonitor func(ctx context.Context) *monitoring.Monitor
getPoller func(ctx context.Context) *monitoring.VMwarePoller
newClient func(vmware.ClientConfig) (vmwareClient, error)
previewRecords func(ctx context.Context, instance config.VMwareVCenterInstance) ([]unifiedresources.IngestRecord, error)
statusMu sync.RWMutex
statuses map[string]vmwareConnectionRuntimeStatus
}
type vmwareClient interface {
TestConnection(ctx context.Context) (*vmware.InventorySummary, error)
Close()
}
type vmwareConnectionResponse struct {
config.VMwareVCenterInstance
Poll *monitoring.VMwareConnectionPollStatus `json:"poll,omitempty"`
Observed *monitoring.VMwareConnectionObservedSummary `json:"observed,omitempty"`
}
type vmwareConnectionRuntimeStatus struct {
Poll *monitoring.VMwareConnectionPollStatus
Observed *monitoring.VMwareConnectionObservedSummary
}
// HandleAdd stores a new VMware vCenter connection.
func (h *VMwareHandlers) HandleAdd(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
if mock.IsMockEnabled() {
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
defer r.Body.Close()
instance, ok := decodeVMwareInstanceRequest(w, r, config.NewVMwareVCenterInstance())
if !ok {
return
}
instance.ID = strings.TrimSpace(instance.ID)
if instance.ID == "" {
instance.ID = config.NewVMwareVCenterInstance().ID
}
normalizeVMwareInstance(&instance)
if err := instance.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
if h.enforceMonitoredSystemLimit(w, r, instance) {
return
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
instances = append(instances, instance)
if err := persistence.SaveVMwareConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_save_failed", "Failed to save VMware configuration", map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusCreated, instance.Redacted())
}
// HandleList returns all configured VMware connections with sensitive fields redacted.
func (h *VMwareHandlers) HandleList(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
if mock.IsMockEnabled() {
writeJSON(w, http.StatusOK, mockVMwareConnectionResponses())
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
orgID := resolveTenantOrgID(r)
var summaries map[string]monitoring.VMwareConnectionSummary
if h != nil && h.getPoller != nil {
if poller := h.getPoller(r.Context()); poller != nil {
summaries = poller.ConnectionSummaries(orgID, instances)
}
}
responses := make([]vmwareConnectionResponse, 0, len(instances))
for i := range instances {
item := instances[i]
item.ApplyDefaults()
response := vmwareConnectionResponse{
VMwareVCenterInstance: item.Redacted(),
}
connID := strings.TrimSpace(item.ID)
if summary, ok := summaries[connID]; ok {
response.Poll = summary.Poll
response.Observed = summary.Observed
} else {
status := h.runtimeStatus(connID)
response.Poll = status.Poll
response.Observed = status.Observed
}
responses = append(responses, response)
}
writeJSON(w, http.StatusOK, responses)
}
// HandleDelete removes a configured VMware vCenter connection by ID.
func (h *VMwareHandlers) HandleDelete(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
if mock.IsMockEnabled() {
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
return
}
connectionID, ok := vmwareConnectionIDFromPath(r.URL.Path)
if !ok {
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
index := -1
for i := range instances {
if strings.TrimSpace(instances[i].ID) == connectionID {
index = i
break
}
}
if index < 0 {
writeErrorResponse(w, http.StatusNotFound, "vmware_not_found", "Connection not found", nil)
return
}
instances = append(instances[:index], instances[index+1:]...)
if err := persistence.SaveVMwareConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_save_failed", "Failed to save VMware configuration", map[string]string{"error": err.Error()})
return
}
h.clearRuntimeStatus(connectionID)
writeJSON(w, http.StatusOK, map[string]any{
"success": true,
"id": connectionID,
})
}
// HandleUpdate replaces a configured VMware vCenter connection by ID while
// preserving unchanged masked secrets from the stored record.
func (h *VMwareHandlers) HandleUpdate(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
if mock.IsMockEnabled() {
writeErrorResponse(w, http.StatusForbidden, "mock_mode_enabled", "Cannot modify connections in mock mode", nil)
return
}
connectionID, ok := vmwareConnectionIDFromPath(r.URL.Path)
if !ok {
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
index := -1
for i := range instances {
if strings.TrimSpace(instances[i].ID) == connectionID {
index = i
break
}
}
if index < 0 {
writeErrorResponse(w, http.StatusNotFound, "vmware_not_found", "Connection not found", nil)
return
}
instance, ok := decodeVMwareInstanceRequest(w, r, instances[index])
if !ok {
return
}
instance.ID = connectionID
normalizeVMwareInstance(&instance)
instance.PreserveMaskedSecrets(instances[index])
if err := instance.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
if h.enforceMonitoredSystemLimitReplacement(w, r, instances[index], instance) {
return
}
instances[index] = instance
if err := persistence.SaveVMwareConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_save_failed", "Failed to save VMware configuration", map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, instance.Redacted())
}
// HandlePreviewConnection projects the monitored-system impact of a proposed
// VMware vCenter connection without persisting it.
func (h *VMwareHandlers) HandlePreviewConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
instance, ok := decodeVMwareInstanceRequest(w, r, config.NewVMwareVCenterInstance())
if !ok {
return
}
normalizeVMwareInstance(&instance)
if err := instance.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
monitor := h.monitorForRequest(r.Context())
usage := monitoredSystemUsage(monitor)
if !usage.available {
writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
return
}
candidate := vmwareMonitoredSystemCandidate(instance)
if !candidate.CountsTowardMonitoredSystems() {
preview := unifiedresources.PreviewMonitoredSystemCandidate(usage.readState, candidate)
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), false, preview).NormalizeCollections())
return
}
records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), instance)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return
}
preview := unifiedresources.PreviewMonitoredSystemRecords(usage.readState, map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: records,
})
if len(preview.ProjectedSystems) == 0 {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", "VMware connection did not resolve to a canonical monitored system preview", nil)
return
}
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), false, preview).NormalizeCollections())
}
// HandlePreviewSavedConnection projects the monitored-system impact of editing
// one saved VMware connection using the same secret-preservation path as
// update and saved test flows.
func (h *VMwareHandlers) HandlePreviewSavedConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
connectionID, ok := vmwareConnectionIDFromPreviewPath(r.URL.Path)
if !ok {
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
for i := range instances {
current := instances[i]
if strings.TrimSpace(current.ID) != connectionID {
continue
}
payload, hasPayload, ok := decodeOptionalVMwareInstanceRequest(w, r, current)
if !ok {
return
}
next := current
if hasPayload {
payload.ID = connectionID
normalizeVMwareInstance(&payload)
payload.PreserveMaskedSecrets(current)
if err := payload.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
next = payload
}
monitor := h.monitorForRequest(r.Context())
usage := monitoredSystemUsage(monitor)
if !usage.available {
writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
return
}
candidate := vmwareMonitoredSystemCandidate(next)
replacement := unifiedresources.MonitoredSystemReplacement{
Source: unifiedresources.SourceVMware,
Selector: unifiedresources.MonitoredSystemReplacementSelector{
ResourceID: connectionID,
},
}
if !candidate.CountsTowardMonitoredSystems() {
preview := unifiedresources.PreviewMonitoredSystemCandidateReplacement(
usage.readState,
replacement,
candidate,
)
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), true, preview).NormalizeCollections())
return
}
records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), next)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return
}
preview := unifiedresources.PreviewMonitoredSystemRecordsReplacement(
usage.readState,
replacement,
map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: records,
},
)
if len(preview.ProjectedSystems) == 0 {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", "VMware connection did not resolve to a canonical monitored system preview", nil)
return
}
writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), true, preview).NormalizeCollections())
return
}
writeErrorResponse(w, http.StatusNotFound, "vmware_not_found", "Connection not found", nil)
}
// HandleTestConnection validates connectivity for a proposed VMware vCenter connection.
func (h *VMwareHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
instance, ok := decodeVMwareInstanceRequest(w, r, config.NewVMwareVCenterInstance())
if !ok {
return
}
normalizeVMwareInstance(&instance)
if err := instance.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
h.writeConnectionTestResult(w, r, instance)
}
// HandleTestSavedConnection validates connectivity for one saved VMware
// connection using the server-owned stored secret material instead of relying
// on frontend redaction placeholders.
func (h *VMwareHandlers) HandleTestSavedConnection(w http.ResponseWriter, r *http.Request) {
if !h.featureEnabled(w) {
return
}
connectionID, ok := vmwareConnectionIDFromTestPath(r.URL.Path)
if !ok {
writeErrorResponse(w, http.StatusBadRequest, "missing_connection_id", "Connection ID is required", nil)
return
}
persistence := h.persistenceForRequest(w, r.Context())
if persistence == nil {
return
}
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
return
}
for i := range instances {
instance := instances[i]
if strings.TrimSpace(instance.ID) != connectionID {
continue
}
normalizeVMwareInstance(&instance)
payload, hasPayload, ok := decodeOptionalVMwareInstanceRequest(w, r, instance)
if !ok {
return
}
if hasPayload {
payload.ID = connectionID
normalizeVMwareInstance(&payload)
payload.PreserveMaskedSecrets(instance)
if err := payload.Validate(); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
instance = payload
}
summary, invalidConfig, err := h.testConnectionInstance(r, instance)
if err != nil {
if !hasPayload {
at := time.Now().UTC()
if poller := h.pollerForRequest(r.Context()); poller != nil {
poller.RecordConnectionTestFailure(resolveTenantOrgID(r), connectionID, err, at)
} else {
h.recordTestFailure(connectionID, err, at)
}
}
h.writeConnectionFailure(w, invalidConfig, err)
return
}
if !hasPayload {
at := time.Now().UTC()
if poller := h.pollerForRequest(r.Context()); poller != nil {
poller.RecordConnectionTestSuccess(resolveTenantOrgID(r), connectionID, summary, at)
} else {
h.recordTestSuccess(connectionID, summary, at)
}
}
writeJSON(w, http.StatusOK, map[string]any{"success": true})
return
}
writeErrorResponse(w, http.StatusNotFound, "vmware_not_found", "Connection not found", nil)
}
func (h *VMwareHandlers) writeConnectionTestResult(
w http.ResponseWriter,
r *http.Request,
instance config.VMwareVCenterInstance,
) {
_, invalidConfig, err := h.testConnectionInstance(r, instance)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"success": true})
}
func (h *VMwareHandlers) writeConnectionFailure(w http.ResponseWriter, invalidConfig bool, err error) {
if invalidConfig {
writeErrorResponse(w, http.StatusBadRequest, "vmware_invalid_config", "Invalid VMware vCenter connection configuration", connectionFailureDetails(err))
return
}
writeErrorResponse(w, http.StatusBadRequest, "vmware_connection_failed", "Failed to connect to VMware vCenter", connectionFailureDetails(err))
}
func connectionFailureDetails(err error) map[string]string {
if err == nil {
return nil
}
details := map[string]string{"error": err.Error()}
if category := connectionErrorCategory(err); category != "" {
details["category"] = category
}
return details
}
func connectionErrorCategory(err error) string {
if err == nil {
return ""
}
if connectionErr, ok := err.(*vmware.ConnectionError); ok {
return strings.TrimSpace(connectionErr.Category)
}
return ""
}
func (h *VMwareHandlers) testConnectionInstance(
r *http.Request,
instance config.VMwareVCenterInstance,
) (*vmware.InventorySummary, bool, error) {
newClient := h.newClient
if newClient == nil {
newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) { return vmware.NewClient(cfg) }
}
client, err := newClient(vmware.ClientConfig{
Host: instance.Host,
Port: instance.Port,
Username: instance.Username,
Password: instance.Password,
InsecureSkipVerify: instance.InsecureSkipVerify,
Timeout: 10 * time.Second,
})
if err != nil {
return nil, true, err
}
defer client.Close()
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
summary, err := client.TestConnection(ctx)
if err != nil {
return nil, false, err
}
return summary, false, nil
}
func normalizeVMwareInstance(instance *config.VMwareVCenterInstance) {
if instance == nil {
return
}
instance.Name = strings.TrimSpace(instance.Name)
instance.Host = strings.TrimSpace(instance.Host)
instance.Username = strings.TrimSpace(instance.Username)
instance.Password = strings.TrimSpace(instance.Password)
instance.ApplyDefaults()
}
func decodeVMwareInstanceRequest(
w http.ResponseWriter,
r *http.Request,
base config.VMwareVCenterInstance,
) (config.VMwareVCenterInstance, bool) {
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
defer r.Body.Close()
instance := base
if err := json.NewDecoder(r.Body).Decode(&instance); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid request body", map[string]string{"error": err.Error()})
return config.VMwareVCenterInstance{}, false
}
return instance, true
}
func decodeOptionalVMwareInstanceRequest(
w http.ResponseWriter,
r *http.Request,
base config.VMwareVCenterInstance,
) (config.VMwareVCenterInstance, bool, bool) {
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid request body", map[string]string{"error": err.Error()})
return config.VMwareVCenterInstance{}, false, false
}
if len(bytes.TrimSpace(body)) == 0 {
return base, false, true
}
instance := base
if err := json.Unmarshal(body, &instance); err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_request", "Invalid request body", map[string]string{"error": err.Error()})
return config.VMwareVCenterInstance{}, false, false
}
return instance, true, true
}
func (h *VMwareHandlers) featureEnabled(w http.ResponseWriter) bool {
if vmware.IsFeatureEnabled() {
return true
}
writeErrorResponse(w, http.StatusNotFound, "vmware_disabled", "VMware integration has been explicitly disabled", nil)
return false
}
func (h *VMwareHandlers) persistenceForRequest(w http.ResponseWriter, ctx context.Context) *config.ConfigPersistence {
if h == nil || h.getPersistence == nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_unavailable", "VMware service unavailable", nil)
return nil
}
persistence := h.getPersistence(ctx)
if persistence == nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_unavailable", "VMware service unavailable", nil)
return nil
}
return persistence
}
func (h *VMwareHandlers) pollerForRequest(ctx context.Context) *monitoring.VMwarePoller {
if h == nil || h.getPoller == nil {
return nil
}
return h.getPoller(ctx)
}
func (h *VMwareHandlers) monitorForRequest(ctx context.Context) *monitoring.Monitor {
if h == nil || h.getMonitor == nil {
return nil
}
return h.getMonitor(ctx)
}
func (h *VMwareHandlers) enforceMonitoredSystemLimit(
w http.ResponseWriter,
r *http.Request,
instance config.VMwareVCenterInstance,
) bool {
candidate := vmwareMonitoredSystemCandidate(instance)
if !candidate.CountsTowardMonitoredSystems() {
return false
}
limit := maxMonitoredSystemsLimitForContext(r.Context())
if limit <= 0 {
return false
}
monitor := h.monitorForRequest(r.Context())
usage := monitoredSystemUsage(monitor)
if !usage.available {
writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
return true
}
records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), instance)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return true
}
decision := monitoredSystemLimitDecisionForRecordsFromUsage(r.Context(), limit, usage, map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: records,
})
if !decision.usageAvailable {
writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
return true
}
if !decision.exceeded {
return false
}
emitLimitBlockedEvent(r.Context(), decision.current, decision.limit)
writeMaxMonitoredSystemsLimitExceeded(w, decision)
return true
}
func (h *VMwareHandlers) enforceMonitoredSystemLimitReplacement(
w http.ResponseWriter,
r *http.Request,
current config.VMwareVCenterInstance,
next config.VMwareVCenterInstance,
) bool {
candidate := vmwareMonitoredSystemCandidate(next)
if !candidate.CountsTowardMonitoredSystems() {
return false
}
limit := maxMonitoredSystemsLimitForContext(r.Context())
if limit <= 0 {
return false
}
monitor := h.monitorForRequest(r.Context())
usage := monitoredSystemUsage(monitor)
if !usage.available {
writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
return true
}
records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), next)
if err != nil {
h.writeConnectionFailure(w, invalidConfig, err)
return true
}
replacementID := strings.TrimSpace(current.ID)
decision := monitoredSystemLimitDecisionForRecordsReplacementFromUsage(r.Context(), limit, usage, unifiedresources.MonitoredSystemReplacement{
Source: unifiedresources.SourceVMware,
Selector: unifiedresources.MonitoredSystemReplacementSelector{
ResourceID: replacementID,
},
}, map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
unifiedresources.SourceVMware: records,
})
if !decision.usageAvailable {
writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
return true
}
if !decision.exceeded {
return false
}
emitLimitBlockedEvent(r.Context(), decision.current, decision.limit)
writeMaxMonitoredSystemsLimitExceeded(w, decision)
return true
}
func (h *VMwareHandlers) previewMonitoredSystemRecords(
ctx context.Context,
instance config.VMwareVCenterInstance,
) ([]unifiedresources.IngestRecord, bool, error) {
if h != nil && h.previewRecords != nil {
records, err := h.previewRecords(ctx, instance)
return records, false, err
}
client, err := vmware.NewClient(vmware.ClientConfig{
Host: instance.Host,
Port: instance.Port,
Username: instance.Username,
Password: instance.Password,
InsecureSkipVerify: instance.InsecureSkipVerify,
Timeout: 20 * time.Second,
})
if err != nil {
return nil, true, err
}
provider := vmware.NewAPIProvider(vmware.ProviderMetadata{
ConnectionID: strings.TrimSpace(instance.ID),
ConnectionName: strings.TrimSpace(instance.Name),
VCenterHost: strings.TrimSpace(instance.Host),
}, client)
defer provider.Close()
refreshCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
if err := provider.Refresh(refreshCtx); err != nil {
return nil, false, err
}
return provider.Records(), false, nil
}
func vmwareMonitoredSystemCandidate(
instance config.VMwareVCenterInstance,
) unifiedresources.MonitoredSystemCandidate {
return unifiedresources.MonitoredSystemCandidate{
Source: unifiedresources.SourceVMware,
Type: unifiedresources.ResourceTypeAgent,
Name: strings.TrimSpace(instance.Name),
Hostname: pulseTokenHostCandidate(instance.Host),
HostURL: strings.TrimSpace(instance.Host),
ResourceID: strings.TrimSpace(instance.ID),
State: monitoredSystemCandidateStateFromEnabled(instance.Enabled),
}
}
func (h *VMwareHandlers) runtimeStatus(connectionID string) vmwareConnectionRuntimeStatus {
if h == nil {
return vmwareConnectionRuntimeStatus{}
}
h.statusMu.RLock()
defer h.statusMu.RUnlock()
if h.statuses == nil {
return vmwareConnectionRuntimeStatus{}
}
status, ok := h.statuses[connectionID]
if !ok {
return vmwareConnectionRuntimeStatus{}
}
return cloneVMwareRuntimeStatus(status)
}
func (h *VMwareHandlers) recordTestSuccess(connectionID string, summary *vmware.InventorySummary, at time.Time) {
if h == nil {
return
}
h.statusMu.Lock()
defer h.statusMu.Unlock()
if h.statuses == nil {
h.statuses = make(map[string]vmwareConnectionRuntimeStatus)
}
current := h.statuses[connectionID]
current.Poll = &monitoring.VMwareConnectionPollStatus{
IntervalSeconds: defaultVMwarePollIntervalSeconds(),
LastAttemptAt: timePointer(at),
LastSuccessAt: timePointer(at),
}
if summary != nil {
current.Observed = &monitoring.VMwareConnectionObservedSummary{
CollectedAt: timePointer(at),
Hosts: summary.Hosts,
VMs: summary.VMs,
Datastores: summary.Datastores,
VIRelease: strings.TrimSpace(summary.VIRelease),
}
}
h.statuses[connectionID] = current
}
func (h *VMwareHandlers) recordTestFailure(connectionID string, err error, at time.Time) {
if h == nil {
return
}
h.statusMu.Lock()
defer h.statusMu.Unlock()
if h.statuses == nil {
h.statuses = make(map[string]vmwareConnectionRuntimeStatus)
}
current := h.statuses[connectionID]
poll := current.Poll
if poll == nil {
poll = &monitoring.VMwareConnectionPollStatus{
IntervalSeconds: defaultVMwarePollIntervalSeconds(),
}
}
poll.LastAttemptAt = timePointer(at)
poll.LastError = &monitoring.VMwareConnectionPollError{
At: timePointer(at),
Message: err.Error(),
Category: connectionErrorCategory(err),
}
current.Poll = poll
h.statuses[connectionID] = current
}
func (h *VMwareHandlers) clearRuntimeStatus(connectionID string) {
if h == nil {
return
}
h.statusMu.Lock()
defer h.statusMu.Unlock()
if h.statuses == nil {
return
}
delete(h.statuses, connectionID)
}
func cloneVMwareRuntimeStatus(status vmwareConnectionRuntimeStatus) vmwareConnectionRuntimeStatus {
cloned := vmwareConnectionRuntimeStatus{}
if status.Poll != nil {
poll := *status.Poll
if poll.LastAttemptAt != nil {
poll.LastAttemptAt = timePointer(*poll.LastAttemptAt)
}
if poll.LastSuccessAt != nil {
poll.LastSuccessAt = timePointer(*poll.LastSuccessAt)
}
if poll.LastError != nil {
errCopy := *poll.LastError
if errCopy.At != nil {
errCopy.At = timePointer(*errCopy.At)
}
poll.LastError = &errCopy
}
cloned.Poll = &poll
}
if status.Observed != nil {
observed := *status.Observed
if observed.CollectedAt != nil {
observed.CollectedAt = timePointer(*observed.CollectedAt)
}
cloned.Observed = &observed
}
return cloned
}
func defaultVMwarePollIntervalSeconds() int {
return int((60 * time.Second) / time.Second)
}
func timePointer(value time.Time) *time.Time {
v := value
return &v
}
func vmwareConnectionIDFromPath(path string) (string, bool) {
if !strings.HasPrefix(path, vmwareConnectionsPathPrefix) {
return "", false
}
raw := strings.Trim(strings.TrimPrefix(path, vmwareConnectionsPathPrefix), "/")
if raw == "" || strings.Contains(raw, "/") {
return "", false
}
connectionID, err := url.PathUnescape(raw)
if err != nil {
return "", false
}
connectionID = strings.TrimSpace(connectionID)
if connectionID == "" || strings.Contains(connectionID, "/") {
return "", false
}
return connectionID, true
}
func vmwareConnectionIDFromTestPath(path string) (string, bool) {
return vmwareConnectionIDFromActionPath(path, "/test")
}
func vmwareConnectionIDFromPreviewPath(path string) (string, bool) {
return vmwareConnectionIDFromActionPath(path, "/preview")
}
func vmwareConnectionIDFromActionPath(path string, suffix string) (string, bool) {
if !strings.HasPrefix(path, vmwareConnectionsPathPrefix) {
return "", false
}
raw := strings.Trim(strings.TrimPrefix(path, vmwareConnectionsPathPrefix), "/")
if !strings.HasSuffix(raw, suffix) {
return "", false
}
raw = strings.TrimSuffix(raw, suffix)
if raw == "" || strings.Contains(raw, "/") {
return "", false
}
connectionID, err := url.PathUnescape(raw)
if err != nil {
return "", false
}
connectionID = strings.TrimSpace(connectionID)
if connectionID == "" || strings.Contains(connectionID, "/") {
return "", false
}
return connectionID, true
}