Pulse/internal/vmware/provider.go

1186 lines
40 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"`
}
// InventoryMetrics captures the current runtime metric floor projected onto
// canonical Pulse metrics for VMware-backed hosts and VMs.
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"`
}
// 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"`
}
// 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"`
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"`
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"`
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
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
}
// NewDefaultProvider returns a provider loaded with the default VMware fixtures.
func NewDefaultProvider() *Provider {
return NewProvider(DefaultFixtures())
}
// 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))
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,
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),
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,
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),
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,
},
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),
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,
})
}
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.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.EnrichmentIssues = cloneInventoryEnrichmentIssues(in.EnrichmentIssues)
return &out
}
func cloneInventoryHosts(in []InventoryHost) []InventoryHost {
if in == nil {
return nil
}
out := make([]InventoryHost, len(in))
for i := range in {
out[i] = in[i]
out[i].DatastoreIDs = cloneStringSlice(in[i].DatastoreIDs)
out[i].DatastoreNames = cloneStringSlice(in[i].DatastoreNames)
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
out[i].Metrics = cloneInventoryMetrics(in[i].Metrics)
}
return out
}
func cloneInventoryVMs(in []InventoryVM) []InventoryVM {
if in == nil {
return nil
}
out := make([]InventoryVM, len(in))
for i := range in {
out[i] = in[i]
out[i].DatastoreIDs = cloneStringSlice(in[i].DatastoreIDs)
out[i].DatastoreNames = cloneStringSlice(in[i].DatastoreNames)
out[i].GuestIPAddresses = cloneStringSlice(in[i].GuestIPAddresses)
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
out[i].Metrics = cloneInventoryMetrics(in[i].Metrics)
}
return out
}
func cloneInventoryDatastores(in []InventoryDatastore) []InventoryDatastore {
if in == nil {
return nil
}
out := make([]InventoryDatastore, len(in))
for i := range in {
out[i] = in[i]
out[i].HostIDs = cloneStringSlice(in[i].HostIDs)
out[i].HostNames = cloneStringSlice(in[i].HostNames)
out[i].VMIDs = cloneStringSlice(in[i].VMIDs)
out[i].VMNames = cloneStringSlice(in[i].VMNames)
out[i].Accessible = cloneBoolPointer(in[i].Accessible)
out[i].MultipleHostAccess = cloneBoolPointer(in[i].MultipleHostAccess)
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
}
return out
}
func cloneInventoryAlarms(in []InventoryAlarm) []InventoryAlarm {
if in == nil {
return nil
}
out := make([]InventoryAlarm, len(in))
copy(out, in)
return out
}
func cloneInventoryTasks(in []InventoryTask) []InventoryTask {
if in == nil {
return nil
}
out := make([]InventoryTask, len(in))
copy(out, in)
return out
}
func cloneInventoryEvents(in []InventoryEvent) []InventoryEvent {
if in == nil {
return nil
}
out := make([]InventoryEvent, len(in))
copy(out, in)
return out
}
func cloneInventoryEnrichmentIssues(in []InventoryEnrichmentIssue) []InventoryEnrichmentIssue {
if in == nil {
return nil
}
out := make([]InventoryEnrichmentIssue, len(in))
copy(out, in)
return out
}
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)
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 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 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"
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 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,
}
}
if metrics.CPU == nil &&
metrics.Memory == nil &&
metrics.NetIn == nil &&
metrics.NetOut == nil &&
metrics.DiskRead == nil &&
metrics.DiskWrite == 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
}
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 {
if in == nil {
return nil
}
out := make([]string, len(in))
copy(out, in)
return out
}
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)))
}