Pulse/internal/vmware/provider.go
rcourtman 9f722a442a Dedupe alerts PMG queue checks, vmware clones, licensing and proxmox client clones
Clears the dupl pairs outside internal/api:

- internal/alerts/pmg.go: the total/deferred/hold per-node queue checks
  share evaluatePMGNodeQueueAlert; the historical short-circuit (a
  below-threshold clear or invalid spec skips the node's remaining
  checks) is preserved via the helper's skip-node return.
- internal/vmware/provider.go: the cloneInventory* family rides generic
  cloneSliceWith / cloneShallowSlice helpers instead of fourteen copies
  of the same nil/make/loop scaffold.
- internal/vmware/client.go + client_signals.go: the byte-identical
  Automation and VI/JSON fetchers delegate to one getSessionScopedJSON.
- internal/vmware/fixtures.go: added to the dupl exclude list in
  .golangci.yml — literal mock fixture catalogs are the same category as
  the existing internal/mock/ exclusion.
- pkg/licensing/license_server_client.go: Activate and
  ExchangeLegacyLicense share postActivationRequest (idempotent POST +
  shared activation response decode).
- pkg/proxmox/client.go: LXC/VM RRD fetches share getGuestRRDData.
- pkg/pulsecli/actions.go is intentionally left for the api-contracts
  slice.

Full test suites pass for internal/alerts, internal/vmware,
pkg/licensing, pkg/proxmox.
2026-06-10 09:49:03 +01:00

1728 lines
66 KiB
Go

package vmware
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
type InventoryAlarm struct {
Alarm string `json:"alarm,omitempty"`
Name string `json:"name,omitempty"`
OverallStatus string `json:"overall_status,omitempty"`
Acknowledged bool `json:"acknowledged,omitempty"`
TriggeredAt time.Time `json:"triggered_at,omitempty"`
}
type InventoryTask struct {
Task string `json:"task,omitempty"`
Name string `json:"name,omitempty"`
State string `json:"state,omitempty"`
DescriptionID string `json:"description_id,omitempty"`
StartedAt time.Time `json:"started_at,omitempty"`
CompletedAt time.Time `json:"completed_at,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type InventoryEvent struct {
Event string `json:"event,omitempty"`
Type string `json:"type,omitempty"`
Message string `json:"message,omitempty"`
User string `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
}
// InventoryVMSnapshot preserves the VI JSON snapshot tree as read-only VM
// context. It must not be promoted into Pulse recovery artifacts.
type InventoryVMSnapshot struct {
Snapshot string `json:"snapshot,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
ID int `json:"id,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
State string `json:"state,omitempty"`
Quiesced bool `json:"quiesced"`
ReplaySupported bool `json:"replay_supported,omitempty"`
Current bool `json:"current,omitempty"`
Children []InventoryVMSnapshot `json:"children,omitempty"`
}
// InventoryVMNetworkAdapter preserves vCenter VM hardware Ethernet adapter
// facts as read-only workload context.
type InventoryVMNetworkAdapter struct {
NIC string `json:"nic,omitempty"`
Label string `json:"label,omitempty"`
Type string `json:"type,omitempty"`
MACType string `json:"mac_type,omitempty"`
MACAddress string `json:"mac_address,omitempty"`
PCISlotNumber *int64 `json:"pci_slot_number,omitempty"`
BackingType string `json:"backing_type,omitempty"`
NetworkID string `json:"network_id,omitempty"`
NetworkName string `json:"network_name,omitempty"`
DistributedSwitchUUID string `json:"distributed_switch_uuid,omitempty"`
DistributedPort string `json:"distributed_port,omitempty"`
OpaqueNetworkType string `json:"opaque_network_type,omitempty"`
OpaqueNetworkID string `json:"opaque_network_id,omitempty"`
HostDevice string `json:"host_device,omitempty"`
State string `json:"state,omitempty"`
StartConnected bool `json:"start_connected"`
AllowGuestControl bool `json:"allow_guest_control"`
WakeOnLANEnabled bool `json:"wake_on_lan_enabled"`
UPTCompatibility bool `json:"upt_compatibility_enabled,omitempty"`
UPTV2Compatibility bool `json:"upt_v2_compatibility_enabled,omitempty"`
}
// InventoryVMVirtualDisk preserves vCenter VM hardware disk facts as read-only
// workload context.
type InventoryVMVirtualDisk struct {
Disk string `json:"disk,omitempty"`
Label string `json:"label,omitempty"`
Type string `json:"type,omitempty"`
IDEPrimary *bool `json:"ide_primary,omitempty"`
IDEMaster *bool `json:"ide_master,omitempty"`
SCSIBus *int64 `json:"scsi_bus,omitempty"`
SCSIUnit *int64 `json:"scsi_unit,omitempty"`
SATABus *int64 `json:"sata_bus,omitempty"`
SATAUnit *int64 `json:"sata_unit,omitempty"`
NVMEBus *int64 `json:"nvme_bus,omitempty"`
NVMEUnit *int64 `json:"nvme_unit,omitempty"`
BackingType string `json:"backing_type,omitempty"`
VMDKFile string `json:"vmdk_file,omitempty"`
DatastoreName string `json:"datastore_name,omitempty"`
CapacityBytes *int64 `json:"capacity_bytes,omitempty"`
}
// InventoryVMTools preserves VMware Tools runtime facts as read-only VM
// context.
type InventoryVMTools struct {
AutoUpdateSupported *bool `json:"auto_update_supported,omitempty"`
InstallAttemptCount *int64 `json:"install_attempt_count,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
VersionNumber *int64 `json:"version_number,omitempty"`
Version string `json:"version,omitempty"`
UpgradePolicy string `json:"upgrade_policy,omitempty"`
VersionStatus string `json:"version_status,omitempty"`
InstallType string `json:"install_type,omitempty"`
RunState string `json:"run_state,omitempty"`
GuestRebootRequested *bool `json:"guest_reboot_requested,omitempty"`
GuestRebootComponents []string `json:"guest_reboot_components,omitempty"`
GuestRebootRequestTime string `json:"guest_reboot_request_time,omitempty"`
}
// InventoryVMBootDevice preserves one vCenter VM boot-device entry as
// read-only virtual hardware context.
type InventoryVMBootDevice struct {
Type string `json:"type,omitempty"`
NIC string `json:"nic,omitempty"`
Disks []string `json:"disks,omitempty"`
}
// InventoryVMHardware preserves vCenter VM hardware, CPU, memory, and boot
// configuration as read-only VM context.
type InventoryVMHardware struct {
GuestOS string `json:"guest_os,omitempty"`
InstantCloneFrozen *bool `json:"instant_clone_frozen,omitempty"`
Version string `json:"version,omitempty"`
UpgradePolicy string `json:"upgrade_policy,omitempty"`
UpgradeVersion string `json:"upgrade_version,omitempty"`
UpgradeStatus string `json:"upgrade_status,omitempty"`
UpgradeErrorMessage string `json:"upgrade_error_message,omitempty"`
BootType string `json:"boot_type,omitempty"`
EFILegacyBoot *bool `json:"efi_legacy_boot,omitempty"`
BootNetworkProtocol string `json:"boot_network_protocol,omitempty"`
BootDelayMilliseconds *int64 `json:"boot_delay_milliseconds,omitempty"`
BootRetry *bool `json:"boot_retry,omitempty"`
BootRetryDelayMilliseconds *int64 `json:"boot_retry_delay_milliseconds,omitempty"`
EnterSetupMode *bool `json:"enter_setup_mode,omitempty"`
BootDevices []InventoryVMBootDevice `json:"boot_devices,omitempty"`
CPUCoresPerSocket *int64 `json:"cpu_cores_per_socket,omitempty"`
CPUHotAddEnabled *bool `json:"cpu_hot_add_enabled,omitempty"`
CPUHotRemoveEnabled *bool `json:"cpu_hot_remove_enabled,omitempty"`
MemoryHotAddEnabled *bool `json:"memory_hot_add_enabled,omitempty"`
MemoryHotAddIncrementMiB *int64 `json:"memory_hot_add_increment_mib,omitempty"`
MemoryHotAddLimitMiB *int64 `json:"memory_hot_add_limit_mib,omitempty"`
}
// InventoryMetrics captures the current runtime metric floor projected onto
// canonical Pulse metrics for VMware-backed hosts and VMs. Sources:
// - Throughput / utilisation (cpu, mem, net*, disk*BytesPerSecond) come from
// VI/JSON PerformanceManager rollups (see client_metrics.go).
// - UptimeSeconds comes from PerformanceManager counters: prefer
// sys.osUptime.latest for VMs (guest OS uptime, requires VMware Tools)
// with fall back to sys.uptime.latest (VMX-process uptime; also the only
// uptime source for ESXi hosts).
// - DiskUsedBytes / DiskTotalBytes / DiskPercent are aggregated from the
// vSphere Automation REST endpoint
// `GET /api/vcenter/vm/{vm}/guest/local-filesystem`, which returns a map
// of mount-point -> {capacity, free_space} when VMware Tools is running.
// Host-level guest disk usage does not exist in vSphere; these fields
// stay nil for hosts.
type InventoryMetrics struct {
CPUPercent *float64 `json:"cpu_percent,omitempty"`
MemoryPercent *float64 `json:"memory_percent,omitempty"`
MemoryUsedBytes *int64 `json:"memory_used_bytes,omitempty"`
MemoryTotalBytes *int64 `json:"memory_total_bytes,omitempty"`
NetInBytesPerSecond *float64 `json:"net_in_bytes_per_second,omitempty"`
NetOutBytesPerSecond *float64 `json:"net_out_bytes_per_second,omitempty"`
DiskReadBytesPerSecond *float64 `json:"disk_read_bytes_per_second,omitempty"`
DiskWriteBytesPerSecond *float64 `json:"disk_write_bytes_per_second,omitempty"`
UptimeSeconds *int64 `json:"uptime_seconds,omitempty"`
DiskUsedBytes *int64 `json:"disk_used_bytes,omitempty"`
DiskTotalBytes *int64 `json:"disk_total_bytes,omitempty"`
DiskPercent *float64 `json:"disk_percent,omitempty"`
}
// InventoryEnrichmentIssue captures one optional VMware read that degraded a
// successful base inventory refresh without invalidating the core phase-1
// resource floor.
type InventoryEnrichmentIssue struct {
Stage string `json:"stage,omitempty"`
EntityType string `json:"entity_type,omitempty"`
EntityID string `json:"entity_id,omitempty"`
Category string `json:"category,omitempty"`
Message string `json:"message,omitempty"`
}
// InventoryCluster is the vCenter Automation API cluster summary. Pulse keeps
// clusters as read-only topology metadata on hosts and VMs, not top-level
// resources.
type InventoryCluster struct {
Cluster string `json:"cluster"`
Name string `json:"name"`
HAEnabled *bool `json:"ha_enabled,omitempty"`
DRSEnabled *bool `json:"drs_enabled,omitempty"`
}
// InventoryNetwork is the vCenter Automation API network summary enriched with
// VI JSON topology. Pulse keeps networks as first-class read-side resources
// because vCenter exposes them as inventory objects used by hosts and VMs.
type InventoryNetwork struct {
Network string `json:"network"`
Name string `json:"name"`
Type string `json:"type"`
DatacenterID string `json:"datacenter_id,omitempty"`
DatacenterName string `json:"datacenter_name,omitempty"`
FolderID string `json:"folder_id,omitempty"`
FolderName string `json:"folder_name,omitempty"`
HostIDs []string `json:"host_ids,omitempty"`
HostNames []string `json:"host_names,omitempty"`
VMIDs []string `json:"vm_ids,omitempty"`
VMNames []string `json:"vm_names,omitempty"`
OverallStatus string `json:"overall_status,omitempty"`
TriggeredAlarms []InventoryAlarm `json:"triggered_alarms,omitempty"`
RecentTasks []InventoryTask `json:"recent_tasks,omitempty"`
RecentEvents []InventoryEvent `json:"recent_events,omitempty"`
}
// InventoryHost is the canonical phase-1 host summary returned by the vCenter
// Automation API list endpoint.
type InventoryHost struct {
Host string `json:"host"`
Name string `json:"name"`
ConnectionState string `json:"connection_state"`
PowerState string `json:"power_state,omitempty"`
HostUUID string `json:"host_uuid,omitempty"`
DatacenterID string `json:"datacenter_id,omitempty"`
DatacenterName string `json:"datacenter_name,omitempty"`
ComputeResourceID string `json:"compute_resource_id,omitempty"`
ComputeResourceName string `json:"compute_resource_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterHAEnabled *bool `json:"cluster_ha_enabled,omitempty"`
ClusterDRSEnabled *bool `json:"cluster_drs_enabled,omitempty"`
FolderID string `json:"folder_id,omitempty"`
FolderName string `json:"folder_name,omitempty"`
DatastoreIDs []string `json:"datastore_ids,omitempty"`
DatastoreNames []string `json:"datastore_names,omitempty"`
OverallStatus string `json:"overall_status,omitempty"`
TriggeredAlarms []InventoryAlarm `json:"triggered_alarms,omitempty"`
RecentTasks []InventoryTask `json:"recent_tasks,omitempty"`
RecentEvents []InventoryEvent `json:"recent_events,omitempty"`
Metrics *InventoryMetrics `json:"metrics,omitempty"`
}
// InventoryVM is the canonical phase-1 VM summary returned by the vCenter
// Automation API list endpoint.
type InventoryVM struct {
VM string `json:"vm"`
Name string `json:"name"`
PowerState string `json:"power_state"`
CPUCount int `json:"cpu_count,omitempty"`
MemorySizeMiB int64 `json:"memory_size_mib,omitempty"`
DatacenterID string `json:"datacenter_id,omitempty"`
DatacenterName string `json:"datacenter_name,omitempty"`
ComputeResourceID string `json:"compute_resource_id,omitempty"`
ComputeResourceName string `json:"compute_resource_name,omitempty"`
ClusterID string `json:"cluster_id,omitempty"`
ClusterName string `json:"cluster_name,omitempty"`
ClusterHAEnabled *bool `json:"cluster_ha_enabled,omitempty"`
ClusterDRSEnabled *bool `json:"cluster_drs_enabled,omitempty"`
FolderID string `json:"folder_id,omitempty"`
FolderName string `json:"folder_name,omitempty"`
ResourcePoolID string `json:"resource_pool_id,omitempty"`
ResourcePoolName string `json:"resource_pool_name,omitempty"`
RuntimeHostID string `json:"runtime_host_id,omitempty"`
RuntimeHostName string `json:"runtime_host_name,omitempty"`
DatastoreIDs []string `json:"datastore_ids,omitempty"`
DatastoreNames []string `json:"datastore_names,omitempty"`
InstanceUUID string `json:"instance_uuid,omitempty"`
BIOSUUID string `json:"bios_uuid,omitempty"`
GuestOSFamily string `json:"guest_os_family,omitempty"`
GuestHostname string `json:"guest_hostname,omitempty"`
GuestIPAddresses []string `json:"guest_ip_addresses,omitempty"`
OverallStatus string `json:"overall_status,omitempty"`
TriggeredAlarms []InventoryAlarm `json:"triggered_alarms,omitempty"`
RecentTasks []InventoryTask `json:"recent_tasks,omitempty"`
RecentEvents []InventoryEvent `json:"recent_events,omitempty"`
SnapshotCount int `json:"snapshot_count,omitempty"`
CurrentSnapshotID string `json:"current_snapshot_id,omitempty"`
SnapshotTree []InventoryVMSnapshot `json:"snapshot_tree,omitempty"`
NetworkAdapters []InventoryVMNetworkAdapter `json:"network_adapters,omitempty"`
VirtualDisks []InventoryVMVirtualDisk `json:"virtual_disks,omitempty"`
Tools *InventoryVMTools `json:"tools,omitempty"`
Hardware *InventoryVMHardware `json:"hardware,omitempty"`
Metrics *InventoryMetrics `json:"metrics,omitempty"`
}
// InventoryDatastore is the canonical phase-1 datastore summary returned by
// the vCenter Automation API list endpoint.
type InventoryDatastore struct {
Datastore string `json:"datastore"`
Name string `json:"name"`
Type string `json:"type"`
FreeSpace int64 `json:"free_space,omitempty"`
Capacity int64 `json:"capacity,omitempty"`
DatacenterID string `json:"datacenter_id,omitempty"`
DatacenterName string `json:"datacenter_name,omitempty"`
FolderID string `json:"folder_id,omitempty"`
FolderName string `json:"folder_name,omitempty"`
HostIDs []string `json:"host_ids,omitempty"`
HostNames []string `json:"host_names,omitempty"`
VMIDs []string `json:"vm_ids,omitempty"`
VMNames []string `json:"vm_names,omitempty"`
Accessible *bool `json:"accessible,omitempty"`
MultipleHostAccess *bool `json:"multiple_host_access,omitempty"`
MaintenanceMode string `json:"maintenance_mode,omitempty"`
URL string `json:"url,omitempty"`
OverallStatus string `json:"overall_status,omitempty"`
TriggeredAlarms []InventoryAlarm `json:"triggered_alarms,omitempty"`
RecentTasks []InventoryTask `json:"recent_tasks,omitempty"`
RecentEvents []InventoryEvent `json:"recent_events,omitempty"`
}
// InventorySnapshot captures the projected inventory floor for one vCenter
// connection at one point in time.
type InventorySnapshot struct {
ConnectionID string
ConnectionName string
VCenterHost string
VIRelease string
CollectedAt time.Time
Hosts []InventoryHost
VMs []InventoryVM
Datastores []InventoryDatastore
Clusters []InventoryCluster
Networks []InventoryNetwork
EnrichmentIssues []InventoryEnrichmentIssue
}
// ProviderMetadata carries operator-owned vCenter connection labels onto the
// projected resource graph.
type ProviderMetadata struct {
ConnectionID string
ConnectionName string
VCenterHost string
}
// Fetcher loads a VMware inventory snapshot from a concrete source.
type Fetcher interface {
Fetch(ctx context.Context) (*InventorySnapshot, error)
}
type fetcherCloser interface {
Close()
}
// APIFetcher loads inventory from the live VMware client.
type APIFetcher struct {
Client *Client
Metadata ProviderMetadata
}
// Fetch implements Fetcher.
func (f *APIFetcher) Fetch(ctx context.Context) (*InventorySnapshot, error) {
if f == nil || f.Client == nil {
return nil, fmt.Errorf("vmware api fetcher client is nil")
}
snapshot, err := f.Client.CollectInventory(ctx)
if err != nil {
return nil, err
}
if snapshot == nil {
return nil, fmt.Errorf("vmware api fetcher returned nil inventory")
}
snapshot.ConnectionID = strings.TrimSpace(f.Metadata.ConnectionID)
snapshot.ConnectionName = strings.TrimSpace(f.Metadata.ConnectionName)
snapshot.VCenterHost = strings.TrimSpace(f.Metadata.VCenterHost)
return snapshot, nil
}
// Close releases idle resources held by the underlying VMware client.
func (f *APIFetcher) Close() {
if f == nil || f.Client == nil {
return
}
f.Client.Close()
}
// FixtureFetcher loads inventory from static fixtures for tests.
type FixtureFetcher struct {
Snapshot InventorySnapshot
}
// Fetch implements Fetcher.
func (f *FixtureFetcher) Fetch(context.Context) (*InventorySnapshot, error) {
if f == nil {
return nil, nil
}
return cloneInventorySnapshot(&f.Snapshot), nil
}
// Provider converts VMware inventory snapshots into unified resources.
type Provider struct {
fetcher Fetcher
lastSnapshot *InventorySnapshot
mu sync.Mutex
now func() time.Time
}
// NewLiveProvider returns a provider backed by a concrete fetcher.
func NewLiveProvider(fetcher Fetcher) *Provider {
return &Provider{
fetcher: fetcher,
now: func() time.Time {
return time.Now().UTC()
},
}
}
// NewAPIProvider returns a provider backed by the live VMware API client.
func NewAPIProvider(metadata ProviderMetadata, client *Client) *Provider {
return NewLiveProvider(&APIFetcher{
Client: client,
Metadata: metadata,
})
}
// NewProvider returns a fixture-backed provider.
func NewProvider(snapshot InventorySnapshot) *Provider {
if snapshot.CollectedAt.IsZero() {
snapshot.CollectedAt = time.Now().UTC()
}
provider := NewLiveProvider(&FixtureFetcher{Snapshot: snapshot})
provider.lastSnapshot = cloneInventorySnapshot(&snapshot)
return provider
}
// Refresh fetches and caches the latest snapshot.
func (p *Provider) Refresh(ctx context.Context) error {
if p == nil {
return fmt.Errorf("vmware provider is nil")
}
if p.fetcher == nil {
return fmt.Errorf("vmware provider fetcher is nil")
}
snapshot, err := p.fetcher.Fetch(ctx)
if err != nil {
return fmt.Errorf("refresh vmware inventory: %w", err)
}
if snapshot == nil {
return fmt.Errorf("vmware provider fetcher returned nil inventory")
}
sortInventorySnapshot(snapshot)
p.mu.Lock()
p.lastSnapshot = cloneInventorySnapshot(snapshot)
p.mu.Unlock()
return nil
}
// Close releases resources held by the active fetcher, if supported.
func (p *Provider) Close() {
if p == nil || p.fetcher == nil {
return
}
if closer, ok := p.fetcher.(fetcherCloser); ok {
closer.Close()
}
}
// Snapshot returns a defensive copy of the cached inventory snapshot.
func (p *Provider) Snapshot() *InventorySnapshot {
if p == nil {
return nil
}
p.mu.Lock()
snapshot := cloneInventorySnapshot(p.lastSnapshot)
p.mu.Unlock()
return snapshot
}
// FixtureRecords projects a VMware fixture snapshot into canonical unified
// resource ingest records without consulting the runtime feature flag.
func FixtureRecords(snapshot InventorySnapshot) []unifiedresources.IngestRecord {
return vmwareRecordsFromSnapshot(&snapshot, nil)
}
// Records returns canonical VMware unified resources if the integration is enabled.
func (p *Provider) Records() []unifiedresources.IngestRecord {
if p == nil || !IsFeatureEnabled() {
return nil
}
return vmwareRecordsFromSnapshot(p.Snapshot(), p.now)
}
func vmwareRecordsFromSnapshot(snapshot *InventorySnapshot, now func() time.Time) []unifiedresources.IngestRecord {
if snapshot == nil {
return nil
}
collectedAt := snapshot.CollectedAt
if collectedAt.IsZero() {
if now != nil {
collectedAt = now().UTC()
} else {
collectedAt = time.Now().UTC()
}
}
connectionName := firstNonEmptyTrimmed(snapshot.ConnectionName, snapshot.VCenterHost, snapshot.ConnectionID)
vcenterHost := strings.TrimSpace(snapshot.VCenterHost)
records := make([]unifiedresources.IngestRecord, 0, len(snapshot.Hosts)+len(snapshot.VMs)+len(snapshot.Datastores)+len(snapshot.Networks))
hostSourceIDsByManagedObject := make(map[string]string, len(snapshot.Hosts))
for _, host := range snapshot.Hosts {
hostID := strings.TrimSpace(host.Host)
if hostID == "" {
continue
}
hostSourceIDsByManagedObject[hostID] = vmwareSourceID(snapshot.ConnectionID, "host", hostID)
}
for _, host := range snapshot.Hosts {
name := firstNonEmptyTrimmed(host.Name, host.Host)
if name == "" {
continue
}
incidents := hostIncidents(host)
resource := unifiedresources.Resource{
Type: unifiedresources.ResourceTypeAgent,
Technology: "vmware",
Name: name,
Status: unifiedresources.IncidentsStatus(hostStatus(host), incidents),
LastSeen: collectedAt,
UpdatedAt: collectedAt,
Uptime: inventoryUptimeSeconds(host.Metrics),
Incidents: incidents,
Metrics: inventoryMetricsResourceMetrics(host.Metrics),
Agent: vmwareHostAgentData(snapshot, host),
VMware: &unifiedresources.VMwareData{
ConnectionID: strings.TrimSpace(snapshot.ConnectionID),
ConnectionName: connectionName,
VCenterHost: vcenterHost,
ManagedObjectID: strings.TrimSpace(host.Host),
EntityType: "host",
HostUUID: strings.TrimSpace(host.HostUUID),
DatacenterID: strings.TrimSpace(host.DatacenterID),
DatacenterName: strings.TrimSpace(host.DatacenterName),
ComputeResourceID: strings.TrimSpace(host.ComputeResourceID),
ComputeResourceName: strings.TrimSpace(host.ComputeResourceName),
ClusterID: strings.TrimSpace(host.ClusterID),
ClusterName: strings.TrimSpace(host.ClusterName),
ClusterHAEnabled: cloneBoolPointer(host.ClusterHAEnabled),
ClusterDRSEnabled: cloneBoolPointer(host.ClusterDRSEnabled),
FolderID: strings.TrimSpace(host.FolderID),
FolderName: strings.TrimSpace(host.FolderName),
ConnectionState: strings.TrimSpace(host.ConnectionState),
PowerState: strings.TrimSpace(host.PowerState),
OverallStatus: strings.TrimSpace(host.OverallStatus),
DatastoreIDs: cloneStringSlice(host.DatastoreIDs),
DatastoreNames: cloneStringSlice(host.DatastoreNames),
ActiveAlarmCount: len(host.TriggeredAlarms),
ActiveAlarmSummary: vmwareAlarmSummary(host.TriggeredAlarms),
RecentTaskCount: len(host.RecentTasks),
RecentTaskSummary: vmwareRecentTaskSummary(host.RecentTasks),
},
Tags: filterNonEmptyStrings(
"vmware",
"vsphere",
"host",
"source:vcenter",
tagWithValue("connection", strings.ToLower(connectionName)),
tagWithValue("power", strings.ToLower(strings.TrimSpace(host.PowerState))),
tagWithValue("state", strings.ToLower(strings.TrimSpace(host.ConnectionState))),
),
}
identity := unifiedresources.ResourceIdentity{
DMIUUID: strings.TrimSpace(host.HostUUID),
Hostnames: filterNonEmptyStrings(name),
ClusterName: vmwareClusterHint(host.ClusterName, host.ComputeResourceName),
}
records = append(records, unifiedresources.IngestRecord{
SourceID: vmwareSourceID(snapshot.ConnectionID, "host", host.Host),
Resource: resource,
Identity: identity,
})
}
for _, vm := range snapshot.VMs {
name := firstNonEmptyTrimmed(vm.Name, vm.VM)
if name == "" {
continue
}
incidents := vmIncidents(vm)
resource := unifiedresources.Resource{
Type: unifiedresources.ResourceTypeVM,
Technology: "vmware",
Name: name,
Status: unifiedresources.IncidentsStatus(vmStatus(vm), incidents),
LastSeen: collectedAt,
UpdatedAt: collectedAt,
Uptime: inventoryUptimeSeconds(vm.Metrics),
Incidents: incidents,
Metrics: inventoryMetricsResourceMetrics(vm.Metrics),
ParentName: strings.TrimSpace(vm.RuntimeHostName),
VMware: &unifiedresources.VMwareData{
ConnectionID: strings.TrimSpace(snapshot.ConnectionID),
ConnectionName: connectionName,
VCenterHost: vcenterHost,
ManagedObjectID: strings.TrimSpace(vm.VM),
EntityType: "vm",
DatacenterID: strings.TrimSpace(vm.DatacenterID),
DatacenterName: strings.TrimSpace(vm.DatacenterName),
ComputeResourceID: strings.TrimSpace(vm.ComputeResourceID),
ComputeResourceName: strings.TrimSpace(vm.ComputeResourceName),
ClusterID: strings.TrimSpace(vm.ClusterID),
ClusterName: strings.TrimSpace(vm.ClusterName),
ClusterHAEnabled: cloneBoolPointer(vm.ClusterHAEnabled),
ClusterDRSEnabled: cloneBoolPointer(vm.ClusterDRSEnabled),
FolderID: strings.TrimSpace(vm.FolderID),
FolderName: strings.TrimSpace(vm.FolderName),
ResourcePoolID: strings.TrimSpace(vm.ResourcePoolID),
ResourcePoolName: strings.TrimSpace(vm.ResourcePoolName),
RuntimeHostID: strings.TrimSpace(vm.RuntimeHostID),
RuntimeHostName: strings.TrimSpace(vm.RuntimeHostName),
PowerState: strings.TrimSpace(vm.PowerState),
CPUCount: vm.CPUCount,
MemorySizeMiB: vm.MemorySizeMiB,
DatastoreIDs: cloneStringSlice(vm.DatastoreIDs),
DatastoreNames: cloneStringSlice(vm.DatastoreNames),
InstanceUUID: strings.TrimSpace(vm.InstanceUUID),
BIOSUUID: strings.TrimSpace(vm.BIOSUUID),
GuestOSFamily: strings.TrimSpace(vm.GuestOSFamily),
GuestHostname: strings.TrimSpace(vm.GuestHostname),
GuestIPAddresses: cloneStringSlice(vm.GuestIPAddresses),
OverallStatus: strings.TrimSpace(vm.OverallStatus),
ActiveAlarmCount: len(vm.TriggeredAlarms),
ActiveAlarmSummary: vmwareAlarmSummary(vm.TriggeredAlarms),
RecentTaskCount: len(vm.RecentTasks),
RecentTaskSummary: vmwareRecentTaskSummary(vm.RecentTasks),
SnapshotCount: vm.SnapshotCount,
CurrentSnapshotID: strings.TrimSpace(vm.CurrentSnapshotID),
SnapshotTree: vmwareSnapshotTreeData(vm.SnapshotTree),
NetworkAdapters: vmwareNetworkAdaptersData(vm.NetworkAdapters),
VirtualDisks: vmwareVirtualDisksData(vm.VirtualDisks),
Tools: vmwareToolsData(vm.Tools),
Hardware: vmwareVMHardwareData(vm.Hardware),
},
Tags: filterNonEmptyStrings(
"vmware",
"vsphere",
"vm",
"source:vcenter",
tagWithValue("connection", strings.ToLower(connectionName)),
tagWithValue("power", strings.ToLower(strings.TrimSpace(vm.PowerState))),
),
}
identity := unifiedresources.ResourceIdentity{
MachineID: firstNonEmptyTrimmed(vm.InstanceUUID, vm.BIOSUUID),
Hostnames: uniqueSortedTrimmedStrings([]string{name, vm.GuestHostname}),
IPAddresses: uniqueSortedTrimmedStrings(vm.GuestIPAddresses),
MACAddresses: uniqueSortedTrimmedStrings(vmwareNetworkAdapterMACAddresses(vm.NetworkAdapters)),
ClusterName: vmwareClusterHint(vm.ClusterName, vm.ComputeResourceName),
}
records = append(records, unifiedresources.IngestRecord{
SourceID: vmwareSourceID(snapshot.ConnectionID, "vm", vm.VM),
ParentSourceID: hostSourceIDsByManagedObject[strings.TrimSpace(vm.RuntimeHostID)],
Resource: resource,
Identity: identity,
})
}
for _, datastore := range snapshot.Datastores {
name := firstNonEmptyTrimmed(datastore.Name, datastore.Datastore)
if name == "" {
continue
}
used := datastore.Capacity - datastore.FreeSpace
if used < 0 {
used = 0
}
incidents := datastoreIncidents(datastore)
resource := unifiedresources.Resource{
Type: unifiedresources.ResourceTypeStorage,
Technology: "vmware",
Name: name,
Status: unifiedresources.IncidentsStatus(datastoreStatus(datastore), incidents),
LastSeen: collectedAt,
UpdatedAt: collectedAt,
Incidents: incidents,
Metrics: &unifiedresources.ResourceMetrics{
Disk: diskMetric(datastore.Capacity, used),
},
Storage: &unifiedresources.StorageMeta{
Type: normalizeDatastoreType(datastore.Type),
Platform: "vmware-vsphere",
Topology: "datastore",
Enabled: vmwareDatastoreEnabled(datastore),
Active: vmwareDatastoreActive(datastore),
Shared: vmwareDatastoreShared(datastore),
Nodes: cloneStringSlice(datastore.HostNames),
ConsumerCount: len(datastore.VMNames),
ConsumerTypes: vmwareDatastoreConsumerTypes(datastore),
TopConsumers: vmwareDatastoreTopConsumers(datastore),
},
VMware: &unifiedresources.VMwareData{
ConnectionID: strings.TrimSpace(snapshot.ConnectionID),
ConnectionName: connectionName,
VCenterHost: vcenterHost,
ManagedObjectID: strings.TrimSpace(datastore.Datastore),
EntityType: "datastore",
DatacenterID: strings.TrimSpace(datastore.DatacenterID),
DatacenterName: strings.TrimSpace(datastore.DatacenterName),
FolderID: strings.TrimSpace(datastore.FolderID),
FolderName: strings.TrimSpace(datastore.FolderName),
DatastoreType: strings.TrimSpace(datastore.Type),
DatastoreURL: strings.TrimSpace(datastore.URL),
DatastoreAccessible: cloneBoolPointer(datastore.Accessible),
MultipleHostAccess: cloneBoolPointer(datastore.MultipleHostAccess),
MaintenanceMode: strings.TrimSpace(datastore.MaintenanceMode),
OverallStatus: strings.TrimSpace(datastore.OverallStatus),
ActiveAlarmCount: len(datastore.TriggeredAlarms),
ActiveAlarmSummary: vmwareAlarmSummary(datastore.TriggeredAlarms),
RecentTaskCount: len(datastore.RecentTasks),
RecentTaskSummary: vmwareRecentTaskSummary(datastore.RecentTasks),
},
Tags: filterNonEmptyStrings(
"vmware",
"vsphere",
"datastore",
"source:vcenter",
tagWithValue("connection", strings.ToLower(connectionName)),
tagWithValue("type", strings.ToLower(strings.TrimSpace(datastore.Type))),
),
}
records = append(records, unifiedresources.IngestRecord{
SourceID: vmwareSourceID(snapshot.ConnectionID, "datastore", datastore.Datastore),
Resource: resource,
})
}
for _, network := range snapshot.Networks {
name := firstNonEmptyTrimmed(network.Name, network.Network)
if name == "" {
continue
}
incidents := networkIncidents(network)
resource := unifiedresources.Resource{
Type: unifiedresources.ResourceTypeNetwork,
Technology: "vmware",
Name: name,
Status: unifiedresources.IncidentsStatus(networkStatus(network), incidents),
LastSeen: collectedAt,
UpdatedAt: collectedAt,
Incidents: incidents,
VMware: &unifiedresources.VMwareData{
ConnectionID: strings.TrimSpace(snapshot.ConnectionID),
ConnectionName: connectionName,
VCenterHost: vcenterHost,
ManagedObjectID: strings.TrimSpace(network.Network),
EntityType: "network",
DatacenterID: strings.TrimSpace(network.DatacenterID),
DatacenterName: strings.TrimSpace(network.DatacenterName),
FolderID: strings.TrimSpace(network.FolderID),
FolderName: strings.TrimSpace(network.FolderName),
NetworkType: strings.TrimSpace(network.Type),
NetworkHostIDs: cloneStringSlice(network.HostIDs),
NetworkHostNames: cloneStringSlice(network.HostNames),
NetworkVMIDs: cloneStringSlice(network.VMIDs),
NetworkVMNames: cloneStringSlice(network.VMNames),
OverallStatus: strings.TrimSpace(network.OverallStatus),
ActiveAlarmCount: len(network.TriggeredAlarms),
ActiveAlarmSummary: vmwareAlarmSummary(network.TriggeredAlarms),
RecentTaskCount: len(network.RecentTasks),
RecentTaskSummary: vmwareRecentTaskSummary(network.RecentTasks),
},
Tags: filterNonEmptyStrings(
"vmware",
"vsphere",
"network",
"source:vcenter",
tagWithValue("connection", strings.ToLower(connectionName)),
tagWithValue("type", strings.ToLower(strings.TrimSpace(network.Type))),
),
}
records = append(records, unifiedresources.IngestRecord{
SourceID: vmwareSourceID(snapshot.ConnectionID, "network", network.Network),
Resource: resource,
})
}
return records
}
func sortInventorySnapshot(snapshot *InventorySnapshot) {
if snapshot == nil {
return
}
sort.Slice(snapshot.Hosts, func(i, j int) bool {
return vmwareSortKey(snapshot.Hosts[i].Host, snapshot.Hosts[i].Name) < vmwareSortKey(snapshot.Hosts[j].Host, snapshot.Hosts[j].Name)
})
sort.Slice(snapshot.VMs, func(i, j int) bool {
return vmwareSortKey(snapshot.VMs[i].VM, snapshot.VMs[i].Name) < vmwareSortKey(snapshot.VMs[j].VM, snapshot.VMs[j].Name)
})
sort.Slice(snapshot.Datastores, func(i, j int) bool {
return vmwareSortKey(snapshot.Datastores[i].Datastore, snapshot.Datastores[i].Name) < vmwareSortKey(snapshot.Datastores[j].Datastore, snapshot.Datastores[j].Name)
})
sort.Slice(snapshot.Clusters, func(i, j int) bool {
return vmwareSortKey(snapshot.Clusters[i].Cluster, snapshot.Clusters[i].Name) < vmwareSortKey(snapshot.Clusters[j].Cluster, snapshot.Clusters[j].Name)
})
sort.Slice(snapshot.Networks, func(i, j int) bool {
return vmwareSortKey(snapshot.Networks[i].Network, snapshot.Networks[i].Name) < vmwareSortKey(snapshot.Networks[j].Network, snapshot.Networks[j].Name)
})
sort.Slice(snapshot.EnrichmentIssues, func(i, j int) bool {
return inventoryEnrichmentIssueSortKey(snapshot.EnrichmentIssues[i]) <
inventoryEnrichmentIssueSortKey(snapshot.EnrichmentIssues[j])
})
}
func cloneInventorySnapshot(in *InventorySnapshot) *InventorySnapshot {
if in == nil {
return nil
}
out := *in
out.Hosts = cloneInventoryHosts(in.Hosts)
out.VMs = cloneInventoryVMs(in.VMs)
out.Datastores = cloneInventoryDatastores(in.Datastores)
out.Clusters = cloneInventoryClusters(in.Clusters)
out.Networks = cloneInventoryNetworks(in.Networks)
out.EnrichmentIssues = cloneInventoryEnrichmentIssues(in.EnrichmentIssues)
return &out
}
// cloneSliceWith deep-copies a slice, applying fix to each copied element so
// it can re-clone its pointer- and slice-typed fields.
func cloneSliceWith[T any](in []T, fix func(*T)) []T {
if in == nil {
return nil
}
out := make([]T, len(in))
for i := range in {
out[i] = in[i]
fix(&out[i])
}
return out
}
// cloneShallowSlice copies a slice whose elements carry no shared references.
func cloneShallowSlice[T any](in []T) []T {
if in == nil {
return nil
}
out := make([]T, len(in))
copy(out, in)
return out
}
func cloneInventoryHosts(in []InventoryHost) []InventoryHost {
return cloneSliceWith(in, func(item *InventoryHost) {
item.ClusterHAEnabled = cloneBoolPointer(item.ClusterHAEnabled)
item.ClusterDRSEnabled = cloneBoolPointer(item.ClusterDRSEnabled)
item.DatastoreIDs = cloneStringSlice(item.DatastoreIDs)
item.DatastoreNames = cloneStringSlice(item.DatastoreNames)
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
item.Metrics = cloneInventoryMetrics(item.Metrics)
})
}
func cloneInventoryVMs(in []InventoryVM) []InventoryVM {
return cloneSliceWith(in, func(item *InventoryVM) {
item.ClusterHAEnabled = cloneBoolPointer(item.ClusterHAEnabled)
item.ClusterDRSEnabled = cloneBoolPointer(item.ClusterDRSEnabled)
item.DatastoreIDs = cloneStringSlice(item.DatastoreIDs)
item.DatastoreNames = cloneStringSlice(item.DatastoreNames)
item.GuestIPAddresses = cloneStringSlice(item.GuestIPAddresses)
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
item.SnapshotTree = cloneInventoryVMSnapshots(item.SnapshotTree)
item.NetworkAdapters = cloneInventoryVMNetworkAdapters(item.NetworkAdapters)
item.VirtualDisks = cloneInventoryVMVirtualDisks(item.VirtualDisks)
item.Tools = cloneInventoryVMTools(item.Tools)
item.Hardware = cloneInventoryVMHardware(item.Hardware)
item.Metrics = cloneInventoryMetrics(item.Metrics)
})
}
func cloneInventoryDatastores(in []InventoryDatastore) []InventoryDatastore {
return cloneSliceWith(in, func(item *InventoryDatastore) {
item.HostIDs = cloneStringSlice(item.HostIDs)
item.HostNames = cloneStringSlice(item.HostNames)
item.VMIDs = cloneStringSlice(item.VMIDs)
item.VMNames = cloneStringSlice(item.VMNames)
item.Accessible = cloneBoolPointer(item.Accessible)
item.MultipleHostAccess = cloneBoolPointer(item.MultipleHostAccess)
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
})
}
func cloneInventoryClusters(in []InventoryCluster) []InventoryCluster {
return cloneSliceWith(in, func(item *InventoryCluster) {
item.HAEnabled = cloneBoolPointer(item.HAEnabled)
item.DRSEnabled = cloneBoolPointer(item.DRSEnabled)
})
}
func cloneInventoryNetworks(in []InventoryNetwork) []InventoryNetwork {
return cloneSliceWith(in, func(item *InventoryNetwork) {
item.HostIDs = cloneStringSlice(item.HostIDs)
item.HostNames = cloneStringSlice(item.HostNames)
item.VMIDs = cloneStringSlice(item.VMIDs)
item.VMNames = cloneStringSlice(item.VMNames)
item.TriggeredAlarms = cloneInventoryAlarms(item.TriggeredAlarms)
item.RecentTasks = cloneInventoryTasks(item.RecentTasks)
item.RecentEvents = cloneInventoryEvents(item.RecentEvents)
})
}
func cloneInventoryAlarms(in []InventoryAlarm) []InventoryAlarm {
return cloneShallowSlice(in)
}
func cloneInventoryTasks(in []InventoryTask) []InventoryTask {
return cloneShallowSlice(in)
}
func cloneInventoryEvents(in []InventoryEvent) []InventoryEvent {
return cloneShallowSlice(in)
}
func cloneInventoryVMSnapshots(in []InventoryVMSnapshot) []InventoryVMSnapshot {
return cloneSliceWith(in, func(item *InventoryVMSnapshot) {
item.CreatedAt = cloneTimePointer(item.CreatedAt)
item.Children = cloneInventoryVMSnapshots(item.Children)
})
}
func cloneInventoryVMNetworkAdapters(in []InventoryVMNetworkAdapter) []InventoryVMNetworkAdapter {
return cloneSliceWith(in, func(item *InventoryVMNetworkAdapter) {
item.PCISlotNumber = cloneInt64Pointer(item.PCISlotNumber)
})
}
func cloneInventoryVMVirtualDisks(in []InventoryVMVirtualDisk) []InventoryVMVirtualDisk {
return cloneSliceWith(in, func(item *InventoryVMVirtualDisk) {
item.IDEPrimary = cloneBoolPointer(item.IDEPrimary)
item.IDEMaster = cloneBoolPointer(item.IDEMaster)
item.SCSIBus = cloneInt64Pointer(item.SCSIBus)
item.SCSIUnit = cloneInt64Pointer(item.SCSIUnit)
item.SATABus = cloneInt64Pointer(item.SATABus)
item.SATAUnit = cloneInt64Pointer(item.SATAUnit)
item.NVMEBus = cloneInt64Pointer(item.NVMEBus)
item.NVMEUnit = cloneInt64Pointer(item.NVMEUnit)
item.CapacityBytes = cloneInt64Pointer(item.CapacityBytes)
})
}
func cloneInventoryVMTools(in *InventoryVMTools) *InventoryVMTools {
if in == nil {
return nil
}
out := *in
out.AutoUpdateSupported = cloneBoolPointer(in.AutoUpdateSupported)
out.InstallAttemptCount = cloneInt64Pointer(in.InstallAttemptCount)
out.VersionNumber = cloneInt64Pointer(in.VersionNumber)
out.GuestRebootRequested = cloneBoolPointer(in.GuestRebootRequested)
out.GuestRebootComponents = cloneStringSlice(in.GuestRebootComponents)
return &out
}
func cloneInventoryVMHardware(in *InventoryVMHardware) *InventoryVMHardware {
if in == nil {
return nil
}
out := *in
out.InstantCloneFrozen = cloneBoolPointer(in.InstantCloneFrozen)
out.EFILegacyBoot = cloneBoolPointer(in.EFILegacyBoot)
out.BootDelayMilliseconds = cloneInt64Pointer(in.BootDelayMilliseconds)
out.BootRetry = cloneBoolPointer(in.BootRetry)
out.BootRetryDelayMilliseconds = cloneInt64Pointer(in.BootRetryDelayMilliseconds)
out.EnterSetupMode = cloneBoolPointer(in.EnterSetupMode)
out.BootDevices = cloneInventoryVMBootDevices(in.BootDevices)
out.CPUCoresPerSocket = cloneInt64Pointer(in.CPUCoresPerSocket)
out.CPUHotAddEnabled = cloneBoolPointer(in.CPUHotAddEnabled)
out.CPUHotRemoveEnabled = cloneBoolPointer(in.CPUHotRemoveEnabled)
out.MemoryHotAddEnabled = cloneBoolPointer(in.MemoryHotAddEnabled)
out.MemoryHotAddIncrementMiB = cloneInt64Pointer(in.MemoryHotAddIncrementMiB)
out.MemoryHotAddLimitMiB = cloneInt64Pointer(in.MemoryHotAddLimitMiB)
return &out
}
func cloneInventoryVMBootDevices(in []InventoryVMBootDevice) []InventoryVMBootDevice {
return cloneSliceWith(in, func(item *InventoryVMBootDevice) {
item.Disks = cloneStringSlice(item.Disks)
})
}
func cloneInventoryEnrichmentIssues(in []InventoryEnrichmentIssue) []InventoryEnrichmentIssue {
return cloneShallowSlice(in)
}
func inventoryEnrichmentIssueSortKey(issue InventoryEnrichmentIssue) string {
return strings.ToLower(strings.TrimSpace(issue.Stage)) + "\x00" +
strings.ToLower(strings.TrimSpace(issue.EntityType)) + "\x00" +
strings.ToLower(strings.TrimSpace(issue.EntityID)) + "\x00" +
strings.ToLower(strings.TrimSpace(issue.Category)) + "\x00" +
strings.ToLower(strings.TrimSpace(issue.Message))
}
func cloneInventoryMetrics(in *InventoryMetrics) *InventoryMetrics {
if in == nil {
return nil
}
out := *in
out.CPUPercent = cloneFloat64Pointer(in.CPUPercent)
out.MemoryPercent = cloneFloat64Pointer(in.MemoryPercent)
out.MemoryUsedBytes = cloneInt64Pointer(in.MemoryUsedBytes)
out.MemoryTotalBytes = cloneInt64Pointer(in.MemoryTotalBytes)
out.NetInBytesPerSecond = cloneFloat64Pointer(in.NetInBytesPerSecond)
out.NetOutBytesPerSecond = cloneFloat64Pointer(in.NetOutBytesPerSecond)
out.DiskReadBytesPerSecond = cloneFloat64Pointer(in.DiskReadBytesPerSecond)
out.DiskWriteBytesPerSecond = cloneFloat64Pointer(in.DiskWriteBytesPerSecond)
out.UptimeSeconds = cloneInt64Pointer(in.UptimeSeconds)
out.DiskUsedBytes = cloneInt64Pointer(in.DiskUsedBytes)
out.DiskTotalBytes = cloneInt64Pointer(in.DiskTotalBytes)
out.DiskPercent = cloneFloat64Pointer(in.DiskPercent)
return &out
}
func cloneTimePointer(in *time.Time) *time.Time {
if in == nil {
return nil
}
out := in.UTC()
return &out
}
func vmwareSourceID(connectionID, entityType, managedObjectID string) string {
parts := filterNonEmptyStrings(strings.TrimSpace(connectionID), strings.TrimSpace(entityType), strings.TrimSpace(managedObjectID))
return strings.Join(parts, ":")
}
// SourceID returns the canonical VMware source identifier used for projected
// records, metrics targets, and mock/runtime history.
func SourceID(connectionID, entityType, managedObjectID string) string {
return vmwareSourceID(connectionID, entityType, managedObjectID)
}
func hostStatus(host InventoryHost) unifiedresources.ResourceStatus {
switch strings.ToUpper(strings.TrimSpace(host.ConnectionState)) {
case "CONNECTED":
switch strings.ToUpper(strings.TrimSpace(host.PowerState)) {
case "", "POWERED_ON":
return unifiedresources.StatusOnline
case "POWERED_OFF":
return unifiedresources.StatusOffline
default:
return unifiedresources.StatusWarning
}
case "DISCONNECTED", "NOT_RESPONDING":
return unifiedresources.StatusOffline
default:
return unifiedresources.StatusUnknown
}
}
func vmStatus(vm InventoryVM) unifiedresources.ResourceStatus {
switch strings.ToUpper(strings.TrimSpace(vm.PowerState)) {
case "POWERED_ON":
return unifiedresources.StatusOnline
case "POWERED_OFF", "SUSPENDED":
return unifiedresources.StatusOffline
default:
return unifiedresources.StatusUnknown
}
}
func datastoreStatus(datastore InventoryDatastore) unifiedresources.ResourceStatus {
if strings.TrimSpace(datastore.Datastore) == "" && strings.TrimSpace(datastore.Name) == "" {
return unifiedresources.StatusUnknown
}
if datastore.Accessible != nil && !*datastore.Accessible {
return unifiedresources.StatusOffline
}
if mode := strings.ToLower(strings.TrimSpace(datastore.MaintenanceMode)); mode != "" && mode != "normal" {
return unifiedresources.StatusWarning
}
return unifiedresources.StatusOnline
}
func networkStatus(network InventoryNetwork) unifiedresources.ResourceStatus {
if strings.TrimSpace(network.Network) == "" && strings.TrimSpace(network.Name) == "" {
return unifiedresources.StatusUnknown
}
return unifiedresources.StatusOnline
}
func hostIncidents(host InventoryHost) []unifiedresources.ResourceIncident {
return appendVMwareAlarmsAndHealthIncidents("host", host.Host, strings.TrimSpace(host.OverallStatus), host.TriggeredAlarms)
}
func vmIncidents(vm InventoryVM) []unifiedresources.ResourceIncident {
return appendVMwareAlarmsAndHealthIncidents("vm", vm.VM, strings.TrimSpace(vm.OverallStatus), vm.TriggeredAlarms)
}
func datastoreIncidents(datastore InventoryDatastore) []unifiedresources.ResourceIncident {
return appendVMwareAlarmsAndHealthIncidents("datastore", datastore.Datastore, strings.TrimSpace(datastore.OverallStatus), datastore.TriggeredAlarms)
}
func networkIncidents(network InventoryNetwork) []unifiedresources.ResourceIncident {
return appendVMwareAlarmsAndHealthIncidents("network", network.Network, strings.TrimSpace(network.OverallStatus), network.TriggeredAlarms)
}
func appendVMwareAlarmsAndHealthIncidents(entityType, managedObjectID, overallStatus string, alarms []InventoryAlarm) []unifiedresources.ResourceIncident {
incidents := make([]unifiedresources.ResourceIncident, 0, len(alarms)+1)
for _, alarm := range alarms {
severity, ok := vmwareRiskLevel(alarm.OverallStatus)
if !ok {
continue
}
nativeID := firstNonEmptyTrimmed(alarm.Alarm, alarm.Name, managedObjectID)
summary := vmwareAlarmIncidentSummary(entityType, managedObjectID, alarm)
startedAt := alarm.TriggeredAt
incidents = append(incidents, unifiedresources.ResourceIncident{
Provider: "vmware",
NativeID: nativeID,
Code: "vmware_alarm_state",
Severity: severity,
Source: string(unifiedresources.SourceVMware),
Summary: summary,
StartedAt: startedAt,
})
}
if len(incidents) == 0 {
if severity, ok := vmwareRiskLevel(overallStatus); ok {
incidents = append(incidents, unifiedresources.ResourceIncident{
Provider: "vmware",
NativeID: firstNonEmptyTrimmed(managedObjectID, entityType),
Code: "vmware_health_state",
Severity: severity,
Source: string(unifiedresources.SourceVMware),
Summary: vmwareOverallStatusSummary(entityType, overallStatus),
})
}
}
return incidents
}
func vmwareRiskLevel(status string) (storagehealth.RiskLevel, bool) {
switch strings.ToLower(strings.TrimSpace(status)) {
case "red":
return storagehealth.RiskCritical, true
case "yellow":
return storagehealth.RiskWarning, true
default:
return "", false
}
}
func vmwareAlarmIncidentSummary(entityType, managedObjectID string, alarm InventoryAlarm) string {
entityLabel := vmwareEntityLabel(entityType)
alarmName := firstNonEmptyTrimmed(alarm.Name, alarm.Alarm)
status := strings.ToLower(strings.TrimSpace(alarm.OverallStatus))
if alarmName == "" {
alarmName = "VMware alarm"
}
if status == "" {
status = "active"
}
if ref := strings.TrimSpace(managedObjectID); ref != "" {
return fmt.Sprintf("%s %s has VMware alarm %s (%s)", entityLabel, ref, alarmName, status)
}
return fmt.Sprintf("%s has VMware alarm %s (%s)", entityLabel, alarmName, status)
}
func vmwareOverallStatusSummary(entityType, overallStatus string) string {
entityLabel := vmwareEntityLabel(entityType)
status := strings.ToLower(strings.TrimSpace(overallStatus))
if status == "" {
status = "degraded"
}
return fmt.Sprintf("%s has VMware overall status %s", entityLabel, status)
}
func vmwareEntityLabel(entityType string) string {
switch strings.ToLower(strings.TrimSpace(entityType)) {
case "host":
return "Host"
case "vm":
return "VM"
case "datastore":
return "Datastore"
case "network":
return "Network"
default:
return "Resource"
}
}
func vmwareAlarmSummary(alarms []InventoryAlarm) string {
if len(alarms) == 0 {
return ""
}
parts := make([]string, 0, len(alarms))
for _, alarm := range alarms {
name := firstNonEmptyTrimmed(alarm.Name, alarm.Alarm)
if name == "" {
continue
}
status := strings.ToLower(strings.TrimSpace(alarm.OverallStatus))
if status == "" {
parts = append(parts, name)
continue
}
parts = append(parts, name+" ("+status+")")
if len(parts) == 3 {
break
}
}
if len(parts) == 0 {
return ""
}
if len(alarms) > len(parts) {
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(alarms)-len(parts))
}
return strings.Join(parts, ", ")
}
func vmwareRecentTaskSummary(tasks []InventoryTask) string {
if len(tasks) == 0 {
return ""
}
parts := make([]string, 0, len(tasks))
for _, task := range tasks {
name := firstNonEmptyTrimmed(task.Name, task.DescriptionID, task.Task)
if name == "" {
continue
}
state := strings.ToLower(strings.TrimSpace(task.State))
if state == "" {
parts = append(parts, name)
} else {
parts = append(parts, name+" ("+state+")")
}
if len(parts) == 3 {
break
}
}
if len(parts) == 0 {
return ""
}
if len(tasks) > len(parts) {
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(tasks)-len(parts))
}
return strings.Join(parts, ", ")
}
func normalizeDatastoreType(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func vmwareClusterHint(clusterName, computeResourceName string) string {
return firstNonEmptyTrimmed(clusterName, computeResourceName)
}
func vmwareDatastoreEnabled(datastore InventoryDatastore) bool {
if datastore.Accessible == nil {
return true
}
return *datastore.Accessible
}
func vmwareDatastoreActive(datastore InventoryDatastore) bool {
if datastore.Accessible != nil && !*datastore.Accessible {
return false
}
mode := strings.ToLower(strings.TrimSpace(datastore.MaintenanceMode))
return mode == "" || mode == "normal"
}
func vmwareDatastoreShared(datastore InventoryDatastore) bool {
if datastore.MultipleHostAccess != nil {
return *datastore.MultipleHostAccess
}
return len(datastore.HostNames) > 1
}
func vmwareDatastoreConsumerTypes(datastore InventoryDatastore) []string {
if len(datastore.VMNames) == 0 {
return nil
}
return []string{string(unifiedresources.ResourceTypeVM)}
}
func vmwareDatastoreTopConsumers(datastore InventoryDatastore) []unifiedresources.StorageConsumerMeta {
if len(datastore.VMNames) == 0 {
return nil
}
consumers := make([]unifiedresources.StorageConsumerMeta, 0, len(datastore.VMNames))
for _, name := range datastore.VMNames {
if strings.TrimSpace(name) == "" {
continue
}
consumer := unifiedresources.StorageConsumerMeta{
ResourceType: unifiedresources.ResourceTypeVM,
Name: strings.TrimSpace(name),
}
consumers = append(consumers, consumer)
if len(consumers) == 5 {
break
}
}
if len(consumers) == 0 {
return nil
}
return consumers
}
func vmwareSnapshotTreeData(snapshots []InventoryVMSnapshot) []unifiedresources.VMwareSnapshotData {
if len(snapshots) == 0 {
return nil
}
out := make([]unifiedresources.VMwareSnapshotData, 0, len(snapshots))
for _, snapshot := range snapshots {
item := unifiedresources.VMwareSnapshotData{
Snapshot: strings.TrimSpace(snapshot.Snapshot),
Name: strings.TrimSpace(snapshot.Name),
Description: strings.TrimSpace(snapshot.Description),
ID: snapshot.ID,
CreatedAt: cloneTimePointer(snapshot.CreatedAt),
State: strings.TrimSpace(snapshot.State),
Quiesced: snapshot.Quiesced,
ReplaySupported: snapshot.ReplaySupported,
Current: snapshot.Current,
Children: vmwareSnapshotTreeData(snapshot.Children),
}
out = append(out, item)
}
return out
}
func vmwareNetworkAdaptersData(adapters []InventoryVMNetworkAdapter) []unifiedresources.VMwareNetworkAdapterData {
if len(adapters) == 0 {
return nil
}
out := make([]unifiedresources.VMwareNetworkAdapterData, 0, len(adapters))
for _, adapter := range adapters {
item := unifiedresources.VMwareNetworkAdapterData{
NIC: strings.TrimSpace(adapter.NIC),
Label: strings.TrimSpace(adapter.Label),
Type: strings.TrimSpace(adapter.Type),
MACType: strings.TrimSpace(adapter.MACType),
MACAddress: strings.TrimSpace(adapter.MACAddress),
PCISlotNumber: cloneInt64Pointer(adapter.PCISlotNumber),
BackingType: strings.TrimSpace(adapter.BackingType),
NetworkID: strings.TrimSpace(adapter.NetworkID),
NetworkName: strings.TrimSpace(adapter.NetworkName),
DistributedSwitchUUID: strings.TrimSpace(adapter.DistributedSwitchUUID),
DistributedPort: strings.TrimSpace(adapter.DistributedPort),
OpaqueNetworkType: strings.TrimSpace(adapter.OpaqueNetworkType),
OpaqueNetworkID: strings.TrimSpace(adapter.OpaqueNetworkID),
HostDevice: strings.TrimSpace(adapter.HostDevice),
State: strings.TrimSpace(adapter.State),
StartConnected: adapter.StartConnected,
AllowGuestControl: adapter.AllowGuestControl,
WakeOnLANEnabled: adapter.WakeOnLANEnabled,
UPTCompatibility: adapter.UPTCompatibility,
UPTV2Compatibility: adapter.UPTV2Compatibility,
}
out = append(out, item)
}
return out
}
func vmwareVirtualDisksData(disks []InventoryVMVirtualDisk) []unifiedresources.VMwareVirtualDiskData {
if len(disks) == 0 {
return nil
}
out := make([]unifiedresources.VMwareVirtualDiskData, 0, len(disks))
for _, disk := range disks {
item := unifiedresources.VMwareVirtualDiskData{
Disk: strings.TrimSpace(disk.Disk),
Label: strings.TrimSpace(disk.Label),
Type: strings.TrimSpace(disk.Type),
IDEPrimary: cloneBoolPointer(disk.IDEPrimary),
IDEMaster: cloneBoolPointer(disk.IDEMaster),
SCSIBus: cloneInt64Pointer(disk.SCSIBus),
SCSIUnit: cloneInt64Pointer(disk.SCSIUnit),
SATABus: cloneInt64Pointer(disk.SATABus),
SATAUnit: cloneInt64Pointer(disk.SATAUnit),
NVMEBus: cloneInt64Pointer(disk.NVMEBus),
NVMEUnit: cloneInt64Pointer(disk.NVMEUnit),
BackingType: strings.TrimSpace(disk.BackingType),
VMDKFile: strings.TrimSpace(disk.VMDKFile),
DatastoreName: strings.TrimSpace(disk.DatastoreName),
CapacityBytes: cloneInt64Pointer(disk.CapacityBytes),
}
out = append(out, item)
}
return out
}
func vmwareToolsData(tools *InventoryVMTools) *unifiedresources.VMwareToolsData {
if tools == nil {
return nil
}
return &unifiedresources.VMwareToolsData{
AutoUpdateSupported: cloneBoolPointer(tools.AutoUpdateSupported),
InstallAttemptCount: cloneInt64Pointer(tools.InstallAttemptCount),
ErrorMessage: strings.TrimSpace(tools.ErrorMessage),
VersionNumber: cloneInt64Pointer(tools.VersionNumber),
Version: strings.TrimSpace(tools.Version),
UpgradePolicy: strings.TrimSpace(tools.UpgradePolicy),
VersionStatus: strings.TrimSpace(tools.VersionStatus),
InstallType: strings.TrimSpace(tools.InstallType),
RunState: strings.TrimSpace(tools.RunState),
GuestRebootRequested: cloneBoolPointer(tools.GuestRebootRequested),
GuestRebootComponents: cloneStringSlice(tools.GuestRebootComponents),
GuestRebootRequestTime: strings.TrimSpace(tools.GuestRebootRequestTime),
}
}
func vmwareVMHardwareData(hardware *InventoryVMHardware) *unifiedresources.VMwareVMHardwareData {
if hardware == nil {
return nil
}
return &unifiedresources.VMwareVMHardwareData{
GuestOS: strings.TrimSpace(hardware.GuestOS),
InstantCloneFrozen: cloneBoolPointer(hardware.InstantCloneFrozen),
Version: strings.TrimSpace(hardware.Version),
UpgradePolicy: strings.TrimSpace(hardware.UpgradePolicy),
UpgradeVersion: strings.TrimSpace(hardware.UpgradeVersion),
UpgradeStatus: strings.TrimSpace(hardware.UpgradeStatus),
UpgradeErrorMessage: strings.TrimSpace(hardware.UpgradeErrorMessage),
BootType: strings.TrimSpace(hardware.BootType),
EFILegacyBoot: cloneBoolPointer(hardware.EFILegacyBoot),
BootNetworkProtocol: strings.TrimSpace(hardware.BootNetworkProtocol),
BootDelayMilliseconds: cloneInt64Pointer(hardware.BootDelayMilliseconds),
BootRetry: cloneBoolPointer(hardware.BootRetry),
BootRetryDelayMilliseconds: cloneInt64Pointer(hardware.BootRetryDelayMilliseconds),
EnterSetupMode: cloneBoolPointer(hardware.EnterSetupMode),
BootDevices: vmwareBootDevicesData(hardware.BootDevices),
CPUCoresPerSocket: cloneInt64Pointer(hardware.CPUCoresPerSocket),
CPUHotAddEnabled: cloneBoolPointer(hardware.CPUHotAddEnabled),
CPUHotRemoveEnabled: cloneBoolPointer(hardware.CPUHotRemoveEnabled),
MemoryHotAddEnabled: cloneBoolPointer(hardware.MemoryHotAddEnabled),
MemoryHotAddIncrementMiB: cloneInt64Pointer(hardware.MemoryHotAddIncrementMiB),
MemoryHotAddLimitMiB: cloneInt64Pointer(hardware.MemoryHotAddLimitMiB),
}
}
func vmwareBootDevicesData(devices []InventoryVMBootDevice) []unifiedresources.VMwareBootDeviceData {
if len(devices) == 0 {
return nil
}
out := make([]unifiedresources.VMwareBootDeviceData, 0, len(devices))
for _, device := range devices {
out = append(out, unifiedresources.VMwareBootDeviceData{
Type: strings.TrimSpace(device.Type),
NIC: strings.TrimSpace(device.NIC),
Disks: cloneStringSlice(device.Disks),
})
}
return out
}
func vmwareNetworkAdapterMACAddresses(adapters []InventoryVMNetworkAdapter) []string {
if len(adapters) == 0 {
return nil
}
addresses := make([]string, 0, len(adapters))
for _, adapter := range adapters {
if mac := strings.TrimSpace(adapter.MACAddress); mac != "" {
addresses = append(addresses, mac)
}
}
return addresses
}
func diskMetric(total, used int64) *unifiedresources.MetricValue {
if total <= 0 {
return nil
}
totalCopy := total
usedCopy := used
percent := (float64(used) / float64(total)) * 100
return &unifiedresources.MetricValue{
Total: &totalCopy,
Used: &usedCopy,
Value: percent,
Percent: percent,
Unit: "bytes",
}
}
func inventoryMetricsResourceMetrics(in *InventoryMetrics) *unifiedresources.ResourceMetrics {
if in == nil {
return nil
}
metrics := &unifiedresources.ResourceMetrics{}
if in.CPUPercent != nil {
metrics.CPU = &unifiedresources.MetricValue{
Value: *in.CPUPercent,
Percent: *in.CPUPercent,
Unit: "percent",
Source: unifiedresources.SourceVMware,
}
}
if in.MemoryPercent != nil {
metrics.Memory = &unifiedresources.MetricValue{
Percent: *in.MemoryPercent,
Unit: "bytes",
Source: unifiedresources.SourceVMware,
}
if in.MemoryUsedBytes != nil {
used := *in.MemoryUsedBytes
metrics.Memory.Used = &used
}
if in.MemoryTotalBytes != nil {
total := *in.MemoryTotalBytes
metrics.Memory.Total = &total
}
}
if in.NetInBytesPerSecond != nil {
metrics.NetIn = &unifiedresources.MetricValue{
Value: *in.NetInBytesPerSecond,
Unit: "bytes/s",
Source: unifiedresources.SourceVMware,
}
}
if in.NetOutBytesPerSecond != nil {
metrics.NetOut = &unifiedresources.MetricValue{
Value: *in.NetOutBytesPerSecond,
Unit: "bytes/s",
Source: unifiedresources.SourceVMware,
}
}
if in.DiskReadBytesPerSecond != nil {
metrics.DiskRead = &unifiedresources.MetricValue{
Value: *in.DiskReadBytesPerSecond,
Unit: "bytes/s",
Source: unifiedresources.SourceVMware,
}
}
if in.DiskWriteBytesPerSecond != nil {
metrics.DiskWrite = &unifiedresources.MetricValue{
Value: *in.DiskWriteBytesPerSecond,
Unit: "bytes/s",
Source: unifiedresources.SourceVMware,
}
}
// Guest filesystem capacity / usage from /api/vcenter/vm/{vm}/guest/local-filesystem.
// Total/Used populate only when both are known; Percent populates from the
// adapter's computed value (or derived from used/total if absent).
if in.DiskTotalBytes != nil || in.DiskPercent != nil {
disk := &unifiedresources.MetricValue{
Unit: "bytes",
Source: unifiedresources.SourceVMware,
}
if in.DiskUsedBytes != nil {
used := *in.DiskUsedBytes
disk.Used = &used
}
if in.DiskTotalBytes != nil {
total := *in.DiskTotalBytes
disk.Total = &total
}
switch {
case in.DiskPercent != nil:
disk.Percent = *in.DiskPercent
disk.Value = *in.DiskPercent
case in.DiskUsedBytes != nil && in.DiskTotalBytes != nil && *in.DiskTotalBytes > 0:
percent := float64(*in.DiskUsedBytes) / float64(*in.DiskTotalBytes) * 100
disk.Percent = percent
disk.Value = percent
}
metrics.Disk = disk
}
if metrics.CPU == nil &&
metrics.Memory == nil &&
metrics.NetIn == nil &&
metrics.NetOut == nil &&
metrics.DiskRead == nil &&
metrics.DiskWrite == nil &&
metrics.Disk == nil {
return nil
}
return metrics
}
func vmwareHostAgentData(snapshot *InventorySnapshot, host InventoryHost) *unifiedresources.AgentData {
if snapshot == nil {
return nil
}
agent := &unifiedresources.AgentData{
AgentID: vmwareSourceID(snapshot.ConnectionID, "host", host.Host),
Hostname: firstNonEmptyTrimmed(host.Name, host.Host),
MachineID: strings.TrimSpace(host.HostUUID),
Platform: "vmware-vsphere",
OSName: "VMware ESXi",
OSVersion: strings.TrimSpace(snapshot.VIRelease),
NetInRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.NetInBytesPerSecond }),
NetOutRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.NetOutBytesPerSecond }),
DiskReadRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.DiskReadBytesPerSecond }),
DiskWriteRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.DiskWriteBytesPerSecond }),
}
if host.Metrics != nil && host.Metrics.MemoryTotalBytes != nil {
total := *host.Metrics.MemoryTotalBytes
used := int64(0)
if host.Metrics.MemoryUsedBytes != nil {
used = *host.Metrics.MemoryUsedBytes
}
free := total - used
if free < 0 {
free = 0
}
agent.Memory = &unifiedresources.AgentMemoryMeta{
Total: total,
Used: used,
Free: free,
}
}
return agent
}
func inventoryMetricFloat64(metrics *InventoryMetrics, pick func(*InventoryMetrics) *float64) float64 {
if metrics == nil || pick == nil {
return 0
}
value := pick(metrics)
if value == nil {
return 0
}
return *value
}
// inventoryUptimeSeconds returns the uptime stamp on the InventoryMetrics
// payload, or 0 when unknown. Pulse's canonical Resource.Uptime is int64
// seconds; the vSphere adapter prefers guest OS uptime (sys.osUptime.latest
// from VMware Tools) and falls back to VMX-process uptime
// (sys.uptime.latest) for VMs without Tools and for ESXi hosts.
func inventoryUptimeSeconds(metrics *InventoryMetrics) int64 {
if metrics == nil || metrics.UptimeSeconds == nil {
return 0
}
value := *metrics.UptimeSeconds
if value < 0 {
return 0
}
return value
}
func cloneFloat64Pointer(in *float64) *float64 {
if in == nil {
return nil
}
value := *in
return &value
}
func cloneInt64Pointer(in *int64) *int64 {
if in == nil {
return nil
}
value := *in
return &value
}
func cloneBoolPointer(in *bool) *bool {
if in == nil {
return nil
}
value := *in
return &value
}
func cloneStringSlice(in []string) []string {
return cloneShallowSlice(in)
}
func firstNonEmptyTrimmed(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
func filterNonEmptyStrings(values ...string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
continue
}
key := strings.ToLower(trimmed)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, trimmed)
}
return out
}
func tagWithValue(prefix, value string) string {
prefix = strings.TrimSpace(prefix)
value = strings.TrimSpace(value)
if prefix == "" || value == "" {
return ""
}
return prefix + ":" + value
}
func vmwareSortKey(id, name string) string {
return firstNonEmptyTrimmed(strings.ToLower(strings.TrimSpace(id)), strings.ToLower(strings.TrimSpace(name)))
}