mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
eac9ffcabf
The vSphere adapter filled `Resource.Tags` with six fixed strings on every resource — `vmware`, `vsphere`, `<kind>`, `source:vcenter`, `connection:<name>`, `power:<state>` — and never read vCenter's own tag and category system. Every VM in an estate returned a byte-identical set, so the workload Tags column rendered the same dots on every row and filtering on any of them selected everything. Commit6b78feba8default-hid the column and said in as many words that the hide was a stopgap awaiting this fix. `internal/vmware/client_tags.go` reads the CIS tagging service. That is a different endpoint family from the `/api/vcenter/...` inventory reads, but the same vSphere Automation API, so it reuses the caller's `/api/session` token rather than opening and managing a second session. Associations come from one batched `list-attached-tags-on-objects` POST per bounded object batch, never a per-object request; tag and category names resolve through a client-scoped catalog with a 10-minute TTL, so a steady-state refresh of a tagged estate costs only the association reads while a rename still converges without a restart. A vCenter without the tagging service, or an account without the tag read privilege, degrades into a `tags` stage enrichment issue and leaves the inventory untagged; it never fails the refresh. The provenance strings stay. `Resource.Tags` is the only keyword set `resourceSearchMatch.ts`, the `?tags=` resources filter, and saved report-schedule tag filters read — `collectSearchCandidates` gathers no `technology`, `type`, or `platformScopes` candidate — so dropping "vmware" or "vsphere" would silently stop matching searches and saved filters that depend on them. Real vCenter labels are appended to that set, never substituted for it. Because that flat set is deliberately mixed, it is the wrong source for a per-row Tags cell. Real tags therefore also land on a canonical `VMware.Tags` facet that carries vCenter's category alongside each name, and `useWorkloads.ts` maps `WorkloadGuest.tags` from that facet for any resource carrying VMware metadata — including the empty case, so a vSphere VM nobody tagged renders an empty cell instead of falling back to the provenance dots. vCenter tag names are unique only inside their category, so the flat label is `category:name`: two categories may each hold a "Production". With the column carrying per-row meaning again, `tags` leaves VMWARE_WORKLOAD_DEFAULT_HIDDEN_COLUMN_IDS and the `defaultHiddenMigrationIds` retirement list, and the state-model test that pinned the stopgap now pins its absence. No un-hide migration ships alongside it:6b78feba8is on main but no tag contains it, so the stopgap never shipped and no install carries the auto-hidden preference. That holds only while the two stay together — the migration writes the hide into each user's saved preference on first load, so an rc cut from main carrying the stopgap without this commit would make an explicit un-hide path necessary. Mock fixtures carry uneven tag coverage — several categories on some objects, one on others, none on the rest — because a uniform fixture set would hide exactly the defect this data exists to catch. Verified against a mock estate built from this branch: `/api/resources` returns provenance plus real labels on the flat set and only real labels on `vmware.tags`; the Tags column renders 2-4 dots per tagged VM and none for untagged ones; a dot's tooltip reads `Backup:Nightly`, and clicking it searches `tags:Backup:Nightly` and narrows 18 VMs to the 3 that carry it. Contract deltas: performance-and-scalability.md Extension Point 17 replaces the stopgap paragraph with the two-surface tag contract and the bounded tag-read budget; unified-resources.md states the keyword-union vs facet split and that a present-but-empty facet means "no operator tags" rather than a fallback; storage-recovery.md extends its VMware descriptive-only boundary to `vmware.tags`, because vCenter tag vocabularies read like protection policy (`Backup:Nightly`) and a label the operator wrote must never satisfy a coverage or compliance verdict that recovery-owned evidence should decide.
1794 lines
69 KiB
Go
1794 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(entityType, managedObjectID, alarm)
|
|
startedAt := alarm.TriggeredAt
|
|
incidents = append(incidents, unifiedresources.ResourceIncident{
|
|
Provider: "vmware",
|
|
NativeID: nativeID,
|
|
Code: "vmware_alarm_state",
|
|
Severity: severity,
|
|
Source: string(unifiedresources.SourceVMware),
|
|
Summary: summary,
|
|
StartedAt: startedAt,
|
|
})
|
|
}
|
|
if len(incidents) == 0 {
|
|
if severity, ok := vmwareRiskLevel(overallStatus); ok {
|
|
incidents = append(incidents, unifiedresources.ResourceIncident{
|
|
Provider: "vmware",
|
|
NativeID: firstNonEmptyTrimmed(managedObjectID, entityType),
|
|
Code: "vmware_health_state",
|
|
Severity: severity,
|
|
Source: string(unifiedresources.SourceVMware),
|
|
Summary: vmwareOverallStatusSummary(entityType, overallStatus),
|
|
})
|
|
}
|
|
}
|
|
return incidents
|
|
}
|
|
|
|
func vmwareRiskLevel(status string) (storagehealth.RiskLevel, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
|
case "red":
|
|
return storagehealth.RiskCritical, true
|
|
case "yellow":
|
|
return storagehealth.RiskWarning, true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func vmwareAlarmIncidentSummary(entityType, managedObjectID string, alarm InventoryAlarm) string {
|
|
entityLabel := vmwareEntityLabel(entityType)
|
|
alarmName := firstNonEmptyTrimmed(alarm.Name, alarm.Alarm)
|
|
status := strings.ToLower(strings.TrimSpace(alarm.OverallStatus))
|
|
if alarmName == "" {
|
|
alarmName = "VMware alarm"
|
|
}
|
|
if status == "" {
|
|
status = "active"
|
|
}
|
|
if ref := strings.TrimSpace(managedObjectID); ref != "" {
|
|
return fmt.Sprintf("%s %s has VMware alarm %s (%s)", entityLabel, ref, alarmName, status)
|
|
}
|
|
return fmt.Sprintf("%s has VMware alarm %s (%s)", entityLabel, alarmName, status)
|
|
}
|
|
|
|
func vmwareOverallStatusSummary(entityType, overallStatus string) string {
|
|
entityLabel := vmwareEntityLabel(entityType)
|
|
status := strings.ToLower(strings.TrimSpace(overallStatus))
|
|
if status == "" {
|
|
status = "degraded"
|
|
}
|
|
return fmt.Sprintf("%s has VMware overall status %s", entityLabel, status)
|
|
}
|
|
|
|
func vmwareEntityLabel(entityType string) string {
|
|
switch strings.ToLower(strings.TrimSpace(entityType)) {
|
|
case "host":
|
|
return "Host"
|
|
case "vm":
|
|
return "VM"
|
|
case "datastore":
|
|
return "Datastore"
|
|
case "network":
|
|
return "Network"
|
|
default:
|
|
return "Resource"
|
|
}
|
|
}
|
|
|
|
func vmwareAlarmSummary(alarms []InventoryAlarm) string {
|
|
if len(alarms) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(alarms))
|
|
for _, alarm := range alarms {
|
|
name := firstNonEmptyTrimmed(alarm.Name, alarm.Alarm)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
status := strings.ToLower(strings.TrimSpace(alarm.OverallStatus))
|
|
if status == "" {
|
|
parts = append(parts, name)
|
|
continue
|
|
}
|
|
parts = append(parts, name+" ("+status+")")
|
|
if len(parts) == 3 {
|
|
break
|
|
}
|
|
}
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
if len(alarms) > len(parts) {
|
|
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(alarms)-len(parts))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func vmwareRecentTaskSummary(tasks []InventoryTask) string {
|
|
if len(tasks) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(tasks))
|
|
for _, task := range tasks {
|
|
name := firstNonEmptyTrimmed(task.Name, task.DescriptionID, task.Task)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
state := strings.ToLower(strings.TrimSpace(task.State))
|
|
if state == "" {
|
|
parts = append(parts, name)
|
|
} else {
|
|
parts = append(parts, name+" ("+state+")")
|
|
}
|
|
if len(parts) == 3 {
|
|
break
|
|
}
|
|
}
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
if len(tasks) > len(parts) {
|
|
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(tasks)-len(parts))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func normalizeDatastoreType(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func vmwareClusterHint(clusterName, computeResourceName string) string {
|
|
return firstNonEmptyTrimmed(clusterName, computeResourceName)
|
|
}
|
|
|
|
func vmwareDatastoreEnabled(datastore InventoryDatastore) bool {
|
|
if datastore.Accessible == nil {
|
|
return true
|
|
}
|
|
return *datastore.Accessible
|
|
}
|
|
|
|
func vmwareDatastoreActive(datastore InventoryDatastore) bool {
|
|
if datastore.Accessible != nil && !*datastore.Accessible {
|
|
return false
|
|
}
|
|
mode := strings.ToLower(strings.TrimSpace(datastore.MaintenanceMode))
|
|
return mode == "" || mode == "normal"
|
|
}
|
|
|
|
func vmwareDatastoreShared(datastore InventoryDatastore) bool {
|
|
if datastore.MultipleHostAccess != nil {
|
|
return *datastore.MultipleHostAccess
|
|
}
|
|
return len(datastore.HostNames) > 1
|
|
}
|
|
|
|
func vmwareDatastoreConsumerTypes(datastore InventoryDatastore) []string {
|
|
if len(datastore.VMNames) == 0 {
|
|
return nil
|
|
}
|
|
return []string{string(unifiedresources.ResourceTypeVM)}
|
|
}
|
|
|
|
func vmwareDatastoreTopConsumers(datastore InventoryDatastore) []unifiedresources.StorageConsumerMeta {
|
|
if len(datastore.VMNames) == 0 {
|
|
return nil
|
|
}
|
|
consumers := make([]unifiedresources.StorageConsumerMeta, 0, len(datastore.VMNames))
|
|
for _, name := range datastore.VMNames {
|
|
if strings.TrimSpace(name) == "" {
|
|
continue
|
|
}
|
|
consumer := unifiedresources.StorageConsumerMeta{
|
|
ResourceType: unifiedresources.ResourceTypeVM,
|
|
Name: strings.TrimSpace(name),
|
|
}
|
|
consumers = append(consumers, consumer)
|
|
if len(consumers) == 5 {
|
|
break
|
|
}
|
|
}
|
|
if len(consumers) == 0 {
|
|
return nil
|
|
}
|
|
return consumers
|
|
}
|
|
|
|
func vmwareSnapshotTreeData(snapshots []InventoryVMSnapshot) []unifiedresources.VMwareSnapshotData {
|
|
if len(snapshots) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]unifiedresources.VMwareSnapshotData, 0, len(snapshots))
|
|
for _, snapshot := range snapshots {
|
|
item := unifiedresources.VMwareSnapshotData{
|
|
Snapshot: strings.TrimSpace(snapshot.Snapshot),
|
|
Name: strings.TrimSpace(snapshot.Name),
|
|
Description: strings.TrimSpace(snapshot.Description),
|
|
ID: snapshot.ID,
|
|
CreatedAt: cloneTimePointer(snapshot.CreatedAt),
|
|
State: strings.TrimSpace(snapshot.State),
|
|
Quiesced: snapshot.Quiesced,
|
|
ReplaySupported: snapshot.ReplaySupported,
|
|
Current: snapshot.Current,
|
|
Children: vmwareSnapshotTreeData(snapshot.Children),
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func vmwareNetworkAdaptersData(adapters []InventoryVMNetworkAdapter) []unifiedresources.VMwareNetworkAdapterData {
|
|
if len(adapters) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]unifiedresources.VMwareNetworkAdapterData, 0, len(adapters))
|
|
for _, adapter := range adapters {
|
|
item := unifiedresources.VMwareNetworkAdapterData{
|
|
NIC: strings.TrimSpace(adapter.NIC),
|
|
Label: strings.TrimSpace(adapter.Label),
|
|
Type: strings.TrimSpace(adapter.Type),
|
|
MACType: strings.TrimSpace(adapter.MACType),
|
|
MACAddress: strings.TrimSpace(adapter.MACAddress),
|
|
PCISlotNumber: cloneInt64Pointer(adapter.PCISlotNumber),
|
|
BackingType: strings.TrimSpace(adapter.BackingType),
|
|
NetworkID: strings.TrimSpace(adapter.NetworkID),
|
|
NetworkName: strings.TrimSpace(adapter.NetworkName),
|
|
DistributedSwitchUUID: strings.TrimSpace(adapter.DistributedSwitchUUID),
|
|
DistributedPort: strings.TrimSpace(adapter.DistributedPort),
|
|
OpaqueNetworkType: strings.TrimSpace(adapter.OpaqueNetworkType),
|
|
OpaqueNetworkID: strings.TrimSpace(adapter.OpaqueNetworkID),
|
|
HostDevice: strings.TrimSpace(adapter.HostDevice),
|
|
State: strings.TrimSpace(adapter.State),
|
|
StartConnected: adapter.StartConnected,
|
|
AllowGuestControl: adapter.AllowGuestControl,
|
|
WakeOnLANEnabled: adapter.WakeOnLANEnabled,
|
|
UPTCompatibility: adapter.UPTCompatibility,
|
|
UPTV2Compatibility: adapter.UPTV2Compatibility,
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func vmwareVirtualDisksData(disks []InventoryVMVirtualDisk) []unifiedresources.VMwareVirtualDiskData {
|
|
if len(disks) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]unifiedresources.VMwareVirtualDiskData, 0, len(disks))
|
|
for _, disk := range disks {
|
|
item := unifiedresources.VMwareVirtualDiskData{
|
|
Disk: strings.TrimSpace(disk.Disk),
|
|
Label: strings.TrimSpace(disk.Label),
|
|
Type: strings.TrimSpace(disk.Type),
|
|
IDEPrimary: cloneBoolPointer(disk.IDEPrimary),
|
|
IDEMaster: cloneBoolPointer(disk.IDEMaster),
|
|
SCSIBus: cloneInt64Pointer(disk.SCSIBus),
|
|
SCSIUnit: cloneInt64Pointer(disk.SCSIUnit),
|
|
SATABus: cloneInt64Pointer(disk.SATABus),
|
|
SATAUnit: cloneInt64Pointer(disk.SATAUnit),
|
|
NVMEBus: cloneInt64Pointer(disk.NVMEBus),
|
|
NVMEUnit: cloneInt64Pointer(disk.NVMEUnit),
|
|
BackingType: strings.TrimSpace(disk.BackingType),
|
|
VMDKFile: strings.TrimSpace(disk.VMDKFile),
|
|
DatastoreName: strings.TrimSpace(disk.DatastoreName),
|
|
CapacityBytes: cloneInt64Pointer(disk.CapacityBytes),
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func vmwareToolsData(tools *InventoryVMTools) *unifiedresources.VMwareToolsData {
|
|
if tools == nil {
|
|
return nil
|
|
}
|
|
return &unifiedresources.VMwareToolsData{
|
|
AutoUpdateSupported: cloneBoolPointer(tools.AutoUpdateSupported),
|
|
InstallAttemptCount: cloneInt64Pointer(tools.InstallAttemptCount),
|
|
ErrorMessage: strings.TrimSpace(tools.ErrorMessage),
|
|
VersionNumber: cloneInt64Pointer(tools.VersionNumber),
|
|
Version: strings.TrimSpace(tools.Version),
|
|
UpgradePolicy: strings.TrimSpace(tools.UpgradePolicy),
|
|
VersionStatus: strings.TrimSpace(tools.VersionStatus),
|
|
InstallType: strings.TrimSpace(tools.InstallType),
|
|
RunState: strings.TrimSpace(tools.RunState),
|
|
GuestRebootRequested: cloneBoolPointer(tools.GuestRebootRequested),
|
|
GuestRebootComponents: cloneStringSlice(tools.GuestRebootComponents),
|
|
GuestRebootRequestTime: strings.TrimSpace(tools.GuestRebootRequestTime),
|
|
}
|
|
}
|
|
|
|
func vmwareVMHardwareData(hardware *InventoryVMHardware) *unifiedresources.VMwareVMHardwareData {
|
|
if hardware == nil {
|
|
return nil
|
|
}
|
|
return &unifiedresources.VMwareVMHardwareData{
|
|
GuestOS: strings.TrimSpace(hardware.GuestOS),
|
|
InstantCloneFrozen: cloneBoolPointer(hardware.InstantCloneFrozen),
|
|
Version: strings.TrimSpace(hardware.Version),
|
|
UpgradePolicy: strings.TrimSpace(hardware.UpgradePolicy),
|
|
UpgradeVersion: strings.TrimSpace(hardware.UpgradeVersion),
|
|
UpgradeStatus: strings.TrimSpace(hardware.UpgradeStatus),
|
|
UpgradeErrorMessage: strings.TrimSpace(hardware.UpgradeErrorMessage),
|
|
BootType: strings.TrimSpace(hardware.BootType),
|
|
EFILegacyBoot: cloneBoolPointer(hardware.EFILegacyBoot),
|
|
BootNetworkProtocol: strings.TrimSpace(hardware.BootNetworkProtocol),
|
|
BootDelayMilliseconds: cloneInt64Pointer(hardware.BootDelayMilliseconds),
|
|
BootRetry: cloneBoolPointer(hardware.BootRetry),
|
|
BootRetryDelayMilliseconds: cloneInt64Pointer(hardware.BootRetryDelayMilliseconds),
|
|
EnterSetupMode: cloneBoolPointer(hardware.EnterSetupMode),
|
|
BootDevices: vmwareBootDevicesData(hardware.BootDevices),
|
|
CPUCoresPerSocket: cloneInt64Pointer(hardware.CPUCoresPerSocket),
|
|
CPUHotAddEnabled: cloneBoolPointer(hardware.CPUHotAddEnabled),
|
|
CPUHotRemoveEnabled: cloneBoolPointer(hardware.CPUHotRemoveEnabled),
|
|
MemoryHotAddEnabled: cloneBoolPointer(hardware.MemoryHotAddEnabled),
|
|
MemoryHotAddIncrementMiB: cloneInt64Pointer(hardware.MemoryHotAddIncrementMiB),
|
|
MemoryHotAddLimitMiB: cloneInt64Pointer(hardware.MemoryHotAddLimitMiB),
|
|
}
|
|
}
|
|
|
|
func vmwareBootDevicesData(devices []InventoryVMBootDevice) []unifiedresources.VMwareBootDeviceData {
|
|
if len(devices) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]unifiedresources.VMwareBootDeviceData, 0, len(devices))
|
|
for _, device := range devices {
|
|
out = append(out, unifiedresources.VMwareBootDeviceData{
|
|
Type: strings.TrimSpace(device.Type),
|
|
NIC: strings.TrimSpace(device.NIC),
|
|
Disks: cloneStringSlice(device.Disks),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func vmwareNetworkAdapterMACAddresses(adapters []InventoryVMNetworkAdapter) []string {
|
|
if len(adapters) == 0 {
|
|
return nil
|
|
}
|
|
addresses := make([]string, 0, len(adapters))
|
|
for _, adapter := range adapters {
|
|
if mac := strings.TrimSpace(adapter.MACAddress); mac != "" {
|
|
addresses = append(addresses, mac)
|
|
}
|
|
}
|
|
return addresses
|
|
}
|
|
|
|
func diskMetric(total, used int64) *unifiedresources.MetricValue {
|
|
if total <= 0 {
|
|
return nil
|
|
}
|
|
totalCopy := total
|
|
usedCopy := used
|
|
percent := (float64(used) / float64(total)) * 100
|
|
return &unifiedresources.MetricValue{
|
|
Total: &totalCopy,
|
|
Used: &usedCopy,
|
|
Value: percent,
|
|
Percent: percent,
|
|
Unit: "bytes",
|
|
}
|
|
}
|
|
|
|
func inventoryMetricsResourceMetrics(in *InventoryMetrics) *unifiedresources.ResourceMetrics {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
|
|
metrics := &unifiedresources.ResourceMetrics{}
|
|
if in.CPUPercent != nil {
|
|
metrics.CPU = &unifiedresources.MetricValue{
|
|
Value: *in.CPUPercent,
|
|
Percent: *in.CPUPercent,
|
|
Unit: "percent",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.MemoryPercent != nil {
|
|
metrics.Memory = &unifiedresources.MetricValue{
|
|
Percent: *in.MemoryPercent,
|
|
Unit: "bytes",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
if in.MemoryUsedBytes != nil {
|
|
used := *in.MemoryUsedBytes
|
|
metrics.Memory.Used = &used
|
|
}
|
|
if in.MemoryTotalBytes != nil {
|
|
total := *in.MemoryTotalBytes
|
|
metrics.Memory.Total = &total
|
|
}
|
|
}
|
|
if in.NetInBytesPerSecond != nil {
|
|
metrics.NetIn = &unifiedresources.MetricValue{
|
|
Value: *in.NetInBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.NetOutBytesPerSecond != nil {
|
|
metrics.NetOut = &unifiedresources.MetricValue{
|
|
Value: *in.NetOutBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.DiskReadBytesPerSecond != nil {
|
|
metrics.DiskRead = &unifiedresources.MetricValue{
|
|
Value: *in.DiskReadBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.DiskWriteBytesPerSecond != nil {
|
|
metrics.DiskWrite = &unifiedresources.MetricValue{
|
|
Value: *in.DiskWriteBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
// Guest filesystem capacity / usage from /api/vcenter/vm/{vm}/guest/local-filesystem.
|
|
// Total/Used populate only when both are known; Percent populates from the
|
|
// adapter's computed value (or derived from used/total if absent).
|
|
if in.DiskTotalBytes != nil || in.DiskPercent != nil {
|
|
disk := &unifiedresources.MetricValue{
|
|
Unit: "bytes",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
if in.DiskUsedBytes != nil {
|
|
used := *in.DiskUsedBytes
|
|
disk.Used = &used
|
|
}
|
|
if in.DiskTotalBytes != nil {
|
|
total := *in.DiskTotalBytes
|
|
disk.Total = &total
|
|
}
|
|
switch {
|
|
case in.DiskPercent != nil:
|
|
disk.Percent = *in.DiskPercent
|
|
disk.Value = *in.DiskPercent
|
|
case in.DiskUsedBytes != nil && in.DiskTotalBytes != nil && *in.DiskTotalBytes > 0:
|
|
percent := float64(*in.DiskUsedBytes) / float64(*in.DiskTotalBytes) * 100
|
|
disk.Percent = percent
|
|
disk.Value = percent
|
|
}
|
|
metrics.Disk = disk
|
|
}
|
|
|
|
if metrics.CPU == nil &&
|
|
metrics.Memory == nil &&
|
|
metrics.NetIn == nil &&
|
|
metrics.NetOut == nil &&
|
|
metrics.DiskRead == nil &&
|
|
metrics.DiskWrite == nil &&
|
|
metrics.Disk == nil {
|
|
return nil
|
|
}
|
|
return metrics
|
|
}
|
|
|
|
func vmwareHostAgentData(snapshot *InventorySnapshot, host InventoryHost) *unifiedresources.AgentData {
|
|
if snapshot == nil {
|
|
return nil
|
|
}
|
|
|
|
agent := &unifiedresources.AgentData{
|
|
AgentID: vmwareSourceID(snapshot.ConnectionID, "host", host.Host),
|
|
Hostname: firstNonEmptyTrimmed(host.Name, host.Host),
|
|
MachineID: strings.TrimSpace(host.HostUUID),
|
|
Platform: "vmware-vsphere",
|
|
OSName: "VMware ESXi",
|
|
OSVersion: strings.TrimSpace(snapshot.VIRelease),
|
|
NetInRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.NetInBytesPerSecond }),
|
|
NetOutRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.NetOutBytesPerSecond }),
|
|
DiskReadRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.DiskReadBytesPerSecond }),
|
|
DiskWriteRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.DiskWriteBytesPerSecond }),
|
|
}
|
|
|
|
if host.Metrics != nil && host.Metrics.MemoryTotalBytes != nil {
|
|
total := *host.Metrics.MemoryTotalBytes
|
|
used := int64(0)
|
|
if host.Metrics.MemoryUsedBytes != nil {
|
|
used = *host.Metrics.MemoryUsedBytes
|
|
}
|
|
free := total - used
|
|
if free < 0 {
|
|
free = 0
|
|
}
|
|
agent.Memory = &unifiedresources.AgentMemoryMeta{
|
|
Total: total,
|
|
Used: used,
|
|
Free: free,
|
|
}
|
|
}
|
|
|
|
return agent
|
|
}
|
|
|
|
func inventoryMetricFloat64(metrics *InventoryMetrics, pick func(*InventoryMetrics) *float64) float64 {
|
|
if metrics == nil || pick == nil {
|
|
return 0
|
|
}
|
|
value := pick(metrics)
|
|
if value == nil {
|
|
return 0
|
|
}
|
|
return *value
|
|
}
|
|
|
|
// inventoryUptimeSeconds returns the uptime stamp on the InventoryMetrics
|
|
// payload, or 0 when unknown. Pulse's canonical Resource.Uptime is int64
|
|
// seconds; the vSphere adapter prefers guest OS uptime (sys.osUptime.latest
|
|
// from VMware Tools) and falls back to VMX-process uptime
|
|
// (sys.uptime.latest) for VMs without Tools and for ESXi hosts.
|
|
func inventoryUptimeSeconds(metrics *InventoryMetrics) int64 {
|
|
if metrics == nil || metrics.UptimeSeconds == nil {
|
|
return 0
|
|
}
|
|
value := *metrics.UptimeSeconds
|
|
if value < 0 {
|
|
return 0
|
|
}
|
|
return value
|
|
}
|
|
|
|
func cloneFloat64Pointer(in *float64) *float64 {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
value := *in
|
|
return &value
|
|
}
|
|
|
|
func cloneInt64Pointer(in *int64) *int64 {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
value := *in
|
|
return &value
|
|
}
|
|
|
|
func cloneBoolPointer(in *bool) *bool {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
value := *in
|
|
return &value
|
|
}
|
|
|
|
func cloneStringSlice(in []string) []string {
|
|
return cloneShallowSlice(in)
|
|
}
|
|
|
|
func firstNonEmptyTrimmed(values ...string) string {
|
|
for _, value := range values {
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func filterNonEmptyStrings(values ...string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
seen := make(map[string]struct{}, len(values))
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(trimmed)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, trimmed)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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)))
|
|
}
|