mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Clears the sixteen dupl pair groups in internal/api plus the pkg/pulsecli pair: - router.go: privileged settings endpoints share the serveSetupTokenOrSettingsWrite gate; patrol findings convert via one unifiedFindingFromAI; the five infrastructure-summary chart loops share collectGuestChartData / fillChartSeriesFromBatch; the VM/LXC workloads summary loops share appendGuestWorkloadSummaries. - deploy_handlers.go: preflight and job status/SSE handlers share handleDeployJobStatus / handleDeployJobEvents. - recovery_handlers.go: points series/facets share parseRecoveryListPointsOptions. - truenas_handlers.go / vmware_handlers.go / router_routes_registration.go: the connection update flow (locate, decode-with-fallback, normalize, preserve masked secrets, validate, save, redact) moves to the new platform_connection_shared.go (updatePlatformConnection + decodeOptionalInstanceRequest + the admin-gated item-route builder); per-platform wrappers carry nolint'd declarative wiring only. - docker_metadata.go / guest_metadata.go: GET/PUT payload semantics move to metadata_handlers_shared.go; the zero-record-instead-of-404 contract is pinned by TestContract_MetadataGetPayloadsUseZeroRecordsInsteadOf404. - kubernetes_agents.go / docker_agents.go: lifecycle PUTs share per-handler action helpers. - config_node_handlers.go: PBS/PMG probes share testProxmoxPlatformConnection. - cloud_handoff_handlers.go / purchase_return_redemptions.go: secrets sqlite stores open through openHardenedSecretsDB so permission hardening stays single-sourced. - ai_handlers.go / chat_service_adapter.go: the GetMessages adapters are deliberate contract mirrors — suppressed with nolint and enforced by TestOrchestratorAndChatAdaptersMapTheSameMessageFields. - pkg/pulsecli/actions.go: action subcommands seed env defaults via actionAPIDefaults (tested); the audit/events cobra registration pair is nolint'd parallel wiring. - .golangci.yml: exclude gitignored tmp/ from ./... typechecking. - subsystem_lookup_test.py: refresh the pinned api-contracts.md line numbers shifted by the contract additions. golangci-lint run ./... is now fully green. Full internal/api and pkg/pulsecli test suites pass.
855 lines
25 KiB
Go
855 lines
25 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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.
|
|
//
|
|
//nolint:dupl // parallel platform wiring of the shared updatePlatformConnection flow; only platform symbols differ
|
|
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
|
|
}
|
|
|
|
updatePlatformConnection(w, r, connectionID, instances, platformConnectionUpdateSpec[config.VMwareVCenterInstance]{
|
|
notFoundCode: "vmware_not_found",
|
|
saveFailCode: "vmware_save_failed",
|
|
saveFailMsg: "Failed to save VMware configuration",
|
|
id: func(inst config.VMwareVCenterInstance) string { return inst.ID },
|
|
decode: decodeVMwareInstanceRequest,
|
|
setID: func(inst *config.VMwareVCenterInstance, id string) { inst.ID = id },
|
|
normalize: normalizeVMwareInstance,
|
|
preserve: func(updated *config.VMwareVCenterInstance, stored config.VMwareVCenterInstance) {
|
|
updated.PreserveMaskedSecrets(stored)
|
|
},
|
|
validate: func(inst config.VMwareVCenterInstance) error { return inst.Validate() },
|
|
redacted: func(inst config.VMwareVCenterInstance) any { return inst.Redacted() },
|
|
save: persistence.SaveVMwareConfig,
|
|
})
|
|
}
|
|
|
|
// 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) {
|
|
return decodeOptionalInstanceRequest(w, r, base)
|
|
}
|
|
|
|
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
|
|
}
|