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))) }