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