Files
pulse/internal/vmware/provider.go
T
rcourtman 7bbeddd5b0 Say why vSphere datastores and networks need attention
An amber vSphere datastore or network gave no reason on its row: the
vCenter alarm behind it sat one click away in the drawer or one tab
away on the Overview. The alarm text itself led with boilerplate
("Network network-302 has VMware alarm ... (yellow)"), so the Overview
truncated the part that mattered.

A vCenter incident summary is now what vCenter flagged: the alarm's
own name, or "vCenter health is <colour>". Every surface that shows
it already names the resource and severity. Alert cards, notification
templates, and Patrol titles carry the resource name. Patrol's seed
context now names the resource on each alert line, and Patrol titles
key off the incident code instead of parsing the wording, while still
reading records persisted in the old form.

The alert card used to find vSphere by looking for "vmware" in the
message. It now routes on the incident provider, which also sends ESXi
host alarms to vSphere instead of to Machines, where hosts are not
listed. The card's own test becomes a registered alerts proof.

The Datastores and Networks tables gain the exception-first Health
column that TrueNAS storage uses. The cell moves into a shared
PlatformIssueReason, so the two platforms cannot drift; TrueNAS
renders the same. A row's dot and its reason now come from one
classification. Hosts shows only from a 72rem container, where it
reads instead of truncating, and the tablet band splits columns by
weight.
2026-10-06 10:45:16 +01:00

1776 lines
69 KiB
Go

package vmware
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
type InventoryAlarm struct {
Alarm string `json:"alarm,omitempty"`
Name string `json:"name,omitempty"`
OverallStatus string `json:"overall_status,omitempty"`
Acknowledged bool `json:"acknowledged,omitempty"`
TriggeredAt time.Time `json:"triggered_at,omitempty"`
}
type InventoryTask struct {
Task string `json:"task,omitempty"`
Name string `json:"name,omitempty"`
State string `json:"state,omitempty"`
DescriptionID string `json:"description_id,omitempty"`
StartedAt time.Time `json:"started_at,omitempty"`
CompletedAt time.Time `json:"completed_at,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type InventoryEvent struct {
Event string `json:"event,omitempty"`
Type string `json:"type,omitempty"`
Message string `json:"message,omitempty"`
User string `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
}
// InventoryVMSnapshot preserves the VI JSON snapshot tree as read-only VM
// context. It must not be promoted into Pulse recovery artifacts.
type InventoryVMSnapshot struct {
Snapshot string `json:"snapshot,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
ID int `json:"id,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
State string `json:"state,omitempty"`
Quiesced bool `json:"quiesced"`
ReplaySupported bool `json:"replay_supported,omitempty"`
Current bool `json:"current,omitempty"`
Children []InventoryVMSnapshot `json:"children,omitempty"`
}
// InventoryVMNetworkAdapter preserves vCenter VM hardware Ethernet adapter
// facts as read-only workload context.
type InventoryVMNetworkAdapter struct {
NIC string `json:"nic,omitempty"`
Label string `json:"label,omitempty"`
Type string `json:"type,omitempty"`
MACType string `json:"mac_type,omitempty"`
MACAddress string `json:"mac_address,omitempty"`
PCISlotNumber *int64 `json:"pci_slot_number,omitempty"`
BackingType string `json:"backing_type,omitempty"`
NetworkID string `json:"network_id,omitempty"`
NetworkName string `json:"network_name,omitempty"`
DistributedSwitchUUID string `json:"distributed_switch_uuid,omitempty"`
DistributedPort string `json:"distributed_port,omitempty"`
OpaqueNetworkType string `json:"opaque_network_type,omitempty"`
OpaqueNetworkID string `json:"opaque_network_id,omitempty"`
HostDevice string `json:"host_device,omitempty"`
State string `json:"state,omitempty"`
StartConnected bool `json:"start_connected"`
AllowGuestControl bool `json:"allow_guest_control"`
WakeOnLANEnabled bool `json:"wake_on_lan_enabled"`
UPTCompatibility bool `json:"upt_compatibility_enabled,omitempty"`
UPTV2Compatibility bool `json:"upt_v2_compatibility_enabled,omitempty"`
}
// InventoryVMVirtualDisk preserves vCenter VM hardware disk facts as read-only
// workload context.
type InventoryVMVirtualDisk struct {
Disk string `json:"disk,omitempty"`
Label string `json:"label,omitempty"`
Type string `json:"type,omitempty"`
IDEPrimary *bool `json:"ide_primary,omitempty"`
IDEMaster *bool `json:"ide_master,omitempty"`
SCSIBus *int64 `json:"scsi_bus,omitempty"`
SCSIUnit *int64 `json:"scsi_unit,omitempty"`
SATABus *int64 `json:"sata_bus,omitempty"`
SATAUnit *int64 `json:"sata_unit,omitempty"`
NVMEBus *int64 `json:"nvme_bus,omitempty"`
NVMEUnit *int64 `json:"nvme_unit,omitempty"`
BackingType string `json:"backing_type,omitempty"`
VMDKFile string `json:"vmdk_file,omitempty"`
DatastoreName string `json:"datastore_name,omitempty"`
CapacityBytes *int64 `json:"capacity_bytes,omitempty"`
}
// InventoryVMTools preserves VMware Tools runtime facts as read-only VM
// context.
type InventoryVMTools struct {
AutoUpdateSupported *bool `json:"auto_update_supported,omitempty"`
InstallAttemptCount *int64 `json:"install_attempt_count,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
VersionNumber *int64 `json:"version_number,omitempty"`
Version string `json:"version,omitempty"`
UpgradePolicy string `json:"upgrade_policy,omitempty"`
VersionStatus string `json:"version_status,omitempty"`
InstallType string `json:"install_type,omitempty"`
RunState string `json:"run_state,omitempty"`
GuestRebootRequested *bool `json:"guest_reboot_requested,omitempty"`
GuestRebootComponents []string `json:"guest_reboot_components,omitempty"`
GuestRebootRequestTime string `json:"guest_reboot_request_time,omitempty"`
}
// InventoryVMBootDevice preserves one vCenter VM boot-device entry as
// read-only virtual hardware context.
type InventoryVMBootDevice struct {
Type string `json:"type,omitempty"`
NIC string `json:"nic,omitempty"`
Disks []string `json:"disks,omitempty"`
}
// InventoryVMHardware preserves vCenter VM hardware, CPU, memory, and boot
// configuration as read-only VM context.
type InventoryVMHardware struct {
GuestOS string `json:"guest_os,omitempty"`
InstantCloneFrozen *bool `json:"instant_clone_frozen,omitempty"`
Version string `json:"version,omitempty"`
UpgradePolicy string `json:"upgrade_policy,omitempty"`
UpgradeVersion string `json:"upgrade_version,omitempty"`
UpgradeStatus string `json:"upgrade_status,omitempty"`
UpgradeErrorMessage string `json:"upgrade_error_message,omitempty"`
BootType string `json:"boot_type,omitempty"`
EFILegacyBoot *bool `json:"efi_legacy_boot,omitempty"`
BootNetworkProtocol string `json:"boot_network_protocol,omitempty"`
BootDelayMilliseconds *int64 `json:"boot_delay_milliseconds,omitempty"`
BootRetry *bool `json:"boot_retry,omitempty"`
BootRetryDelayMilliseconds *int64 `json:"boot_retry_delay_milliseconds,omitempty"`
EnterSetupMode *bool `json:"enter_setup_mode,omitempty"`
BootDevices []InventoryVMBootDevice `json:"boot_devices,omitempty"`
CPUCoresPerSocket *int64 `json:"cpu_cores_per_socket,omitempty"`
CPUHotAddEnabled *bool `json:"cpu_hot_add_enabled,omitempty"`
CPUHotRemoveEnabled *bool `json:"cpu_hot_remove_enabled,omitempty"`
MemoryHotAddEnabled *bool `json:"memory_hot_add_enabled,omitempty"`
MemoryHotAddIncrementMiB *int64 `json:"memory_hot_add_increment_mib,omitempty"`
MemoryHotAddLimitMiB *int64 `json:"memory_hot_add_limit_mib,omitempty"`
}
// InventoryMetrics captures the current runtime metric floor projected onto
// canonical Pulse metrics for VMware-backed hosts and VMs. Sources:
// - Throughput / utilisation (cpu, mem, net*, disk*BytesPerSecond) come from
// VI/JSON PerformanceManager rollups (see client_metrics.go).
// - UptimeSeconds comes from PerformanceManager counters: prefer
// sys.osUptime.latest for VMs (guest OS uptime, requires VMware Tools)
// with fall back to sys.uptime.latest (VMX-process uptime; also the only
// uptime source for ESXi hosts).
// - DiskUsedBytes / DiskTotalBytes / DiskPercent are aggregated from the
// vSphere Automation REST endpoint
// `GET /api/vcenter/vm/{vm}/guest/local-filesystem`, which returns a map
// of mount-point -> {capacity, free_space} when VMware Tools is running.
// Host-level guest disk usage does not exist in vSphere; these fields
// stay nil for hosts.
type InventoryMetrics struct {
CPUPercent *float64 `json:"cpu_percent,omitempty"`
MemoryPercent *float64 `json:"memory_percent,omitempty"`
MemoryUsedBytes *int64 `json:"memory_used_bytes,omitempty"`
MemoryTotalBytes *int64 `json:"memory_total_bytes,omitempty"`
NetInBytesPerSecond *float64 `json:"net_in_bytes_per_second,omitempty"`
NetOutBytesPerSecond *float64 `json:"net_out_bytes_per_second,omitempty"`
DiskReadBytesPerSecond *float64 `json:"disk_read_bytes_per_second,omitempty"`
DiskWriteBytesPerSecond *float64 `json:"disk_write_bytes_per_second,omitempty"`
UptimeSeconds *int64 `json:"uptime_seconds,omitempty"`
DiskUsedBytes *int64 `json:"disk_used_bytes,omitempty"`
DiskTotalBytes *int64 `json:"disk_total_bytes,omitempty"`
DiskPercent *float64 `json:"disk_percent,omitempty"`
}
// InventoryEnrichmentIssue captures one optional VMware read that degraded a
// successful base inventory refresh without invalidating the core phase-1
// resource floor.
type InventoryEnrichmentIssue struct {
Stage string `json:"stage,omitempty"`
EntityType string `json:"entity_type,omitempty"`
EntityID string `json:"entity_id,omitempty"`
Category string `json:"category,omitempty"`
Message string `json:"message,omitempty"`
}
// InventoryTag is one operator-authored vCenter tag attached to an inventory
// object, read from the CIS tagging service. vCenter tags always belong to a
// category ("Environment", "Owner", ...), so the category travels with the tag
// name: two categories may each hold a tag called "Production", and dropping
// the category would merge them into one meaningless label.
type InventoryTag struct {
TagID string `json:"tag_id,omitempty"`
Name string `json:"name"`
CategoryID string `json:"category_id,omitempty"`
Category string `json:"category,omitempty"`
Description string `json:"description,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"`
Tags []InventoryTag `json:"tags,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"`
Tags []InventoryTag `json:"tags,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: vmwareTagsData(host.Tags),
},
Tags: vmwareResourceTags([]string{
"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))),
}, host.Tags),
}
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: vmwareTagsData(vm.Tags),
},
Tags: vmwareResourceTags([]string{
"vmware",
"vsphere",
"vm",
"source:vcenter",
tagWithValue("connection", strings.ToLower(connectionName)),
tagWithValue("power", strings.ToLower(strings.TrimSpace(vm.PowerState))),
}, vm.Tags),
}
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(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(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
}
}
// An incident summary is the alert message and the reason a row shows in
// vSphere's Health column, beside the resource's name and a severity badge.
// So it says only what vCenter flagged: the alarm's own name ("Network packet
// loss above threshold"), not the managed object ID (network-302) or the
// alarm colour, which the name and severity already carry.
func vmwareAlarmIncidentSummary(alarm InventoryAlarm) string {
if alarmName := firstNonEmptyTrimmed(alarm.Name, alarm.Alarm); alarmName != "" {
return alarmName
}
return "vCenter alarm"
}
// Without a triggered alarm, vCenter's overall status colour is all there is:
// vCenter admins know its yellow and red.
func vmwareOverallStatusSummary(overallStatus string) string {
status := strings.ToLower(strings.TrimSpace(overallStatus))
if status == "" {
status = "degraded"
}
return "vCenter health is " + status
}
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
}
// vmwareResourceTags merges the adapter's provenance keywords with the
// operator's real vCenter tag labels into the flat searchable keyword set.
//
// The provenance strings stay. `Resource.Tags` is the only keyword set
// `resourceSearchMatch.ts`, the `?tags=` resources filter, and saved report
// schedules read, so dropping "vmware"/"vsphere"/"source:vcenter" would
// silently stop matching searches and saved filters that rely on them. Real
// vCenter tags are appended to that set, never substituted for it.
//
// Because the set is deliberately mixed, it is not what a per-row Tags cell
// should render — surfaces that show the operator's own labels read the
// `VMware.Tags` facet instead.
func vmwareResourceTags(provenance []string, tags []InventoryTag) []string {
values := make([]string, 0, len(provenance)+len(tags))
values = append(values, provenance...)
for _, tag := range tags {
values = append(values, InventoryTagLabel(tag))
}
return filterNonEmptyStrings(values...)
}
// vmwareTagsData projects vCenter tags onto the canonical VMware facet. Tags
// without a resolvable name are dropped rather than rendered as empty chips.
func vmwareTagsData(tags []InventoryTag) []unifiedresources.VMwareTagData {
if len(tags) == 0 {
return nil
}
out := make([]unifiedresources.VMwareTagData, 0, len(tags))
for _, tag := range tags {
label := InventoryTagLabel(tag)
if label == "" {
continue
}
out = append(out, unifiedresources.VMwareTagData{
TagID: strings.TrimSpace(tag.TagID),
Name: strings.TrimSpace(tag.Name),
CategoryID: strings.TrimSpace(tag.CategoryID),
Category: strings.TrimSpace(tag.Category),
Description: strings.TrimSpace(tag.Description),
Label: label,
})
}
if len(out) == 0 {
return nil
}
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)))
}