mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 04:06:21 +00:00
5016cbc2ba
Project vCenter network inventory through canonical resources and add the vSphere Networks table backed by vCenter network topology. Align resource presentation coalescing so state and resource APIs share the same host contract.
889 lines
26 KiB
Go
889 lines
26 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
|
|
}
|
|
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
|
|
}
|
|
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(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(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(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(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) 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,
|
|
Networks: summary.Networks,
|
|
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
|
|
}
|