mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 08:57:12 +00:00
1186 lines
40 KiB
Go
1186 lines
40 KiB
Go
package vmware
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
type InventoryAlarm struct {
|
|
Alarm string `json:"alarm,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
OverallStatus string `json:"overall_status,omitempty"`
|
|
Acknowledged bool `json:"acknowledged,omitempty"`
|
|
TriggeredAt time.Time `json:"triggered_at,omitempty"`
|
|
}
|
|
|
|
type InventoryTask struct {
|
|
Task string `json:"task,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
DescriptionID string `json:"description_id,omitempty"`
|
|
StartedAt time.Time `json:"started_at,omitempty"`
|
|
CompletedAt time.Time `json:"completed_at,omitempty"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
}
|
|
|
|
type InventoryEvent struct {
|
|
Event string `json:"event,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
|
}
|
|
|
|
// InventoryMetrics captures the current runtime metric floor projected onto
|
|
// canonical Pulse metrics for VMware-backed hosts and VMs.
|
|
type InventoryMetrics struct {
|
|
CPUPercent *float64 `json:"cpu_percent,omitempty"`
|
|
MemoryPercent *float64 `json:"memory_percent,omitempty"`
|
|
MemoryUsedBytes *int64 `json:"memory_used_bytes,omitempty"`
|
|
MemoryTotalBytes *int64 `json:"memory_total_bytes,omitempty"`
|
|
NetInBytesPerSecond *float64 `json:"net_in_bytes_per_second,omitempty"`
|
|
NetOutBytesPerSecond *float64 `json:"net_out_bytes_per_second,omitempty"`
|
|
DiskReadBytesPerSecond *float64 `json:"disk_read_bytes_per_second,omitempty"`
|
|
DiskWriteBytesPerSecond *float64 `json:"disk_write_bytes_per_second,omitempty"`
|
|
}
|
|
|
|
// InventoryEnrichmentIssue captures one optional VMware read that degraded a
|
|
// successful base inventory refresh without invalidating the core phase-1
|
|
// resource floor.
|
|
type InventoryEnrichmentIssue struct {
|
|
Stage string `json:"stage,omitempty"`
|
|
EntityType string `json:"entity_type,omitempty"`
|
|
EntityID string `json:"entity_id,omitempty"`
|
|
Category string `json:"category,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// InventoryHost is the canonical phase-1 host summary returned by the vCenter
|
|
// Automation API list endpoint.
|
|
type InventoryHost struct {
|
|
Host string `json:"host"`
|
|
Name string `json:"name"`
|
|
ConnectionState string `json:"connection_state"`
|
|
PowerState string `json:"power_state,omitempty"`
|
|
HostUUID string `json:"host_uuid,omitempty"`
|
|
DatacenterID string `json:"datacenter_id,omitempty"`
|
|
DatacenterName string `json:"datacenter_name,omitempty"`
|
|
ComputeResourceID string `json:"compute_resource_id,omitempty"`
|
|
ComputeResourceName string `json:"compute_resource_name,omitempty"`
|
|
ClusterID string `json:"cluster_id,omitempty"`
|
|
ClusterName string `json:"cluster_name,omitempty"`
|
|
FolderID string `json:"folder_id,omitempty"`
|
|
FolderName string `json:"folder_name,omitempty"`
|
|
DatastoreIDs []string `json:"datastore_ids,omitempty"`
|
|
DatastoreNames []string `json:"datastore_names,omitempty"`
|
|
OverallStatus string `json:"overall_status,omitempty"`
|
|
TriggeredAlarms []InventoryAlarm `json:"triggered_alarms,omitempty"`
|
|
RecentTasks []InventoryTask `json:"recent_tasks,omitempty"`
|
|
RecentEvents []InventoryEvent `json:"recent_events,omitempty"`
|
|
Metrics *InventoryMetrics `json:"metrics,omitempty"`
|
|
}
|
|
|
|
// InventoryVM is the canonical phase-1 VM summary returned by the vCenter
|
|
// Automation API list endpoint.
|
|
type InventoryVM struct {
|
|
VM string `json:"vm"`
|
|
Name string `json:"name"`
|
|
PowerState string `json:"power_state"`
|
|
CPUCount int `json:"cpu_count,omitempty"`
|
|
MemorySizeMiB int64 `json:"memory_size_mib,omitempty"`
|
|
DatacenterID string `json:"datacenter_id,omitempty"`
|
|
DatacenterName string `json:"datacenter_name,omitempty"`
|
|
ComputeResourceID string `json:"compute_resource_id,omitempty"`
|
|
ComputeResourceName string `json:"compute_resource_name,omitempty"`
|
|
ClusterID string `json:"cluster_id,omitempty"`
|
|
ClusterName string `json:"cluster_name,omitempty"`
|
|
FolderID string `json:"folder_id,omitempty"`
|
|
FolderName string `json:"folder_name,omitempty"`
|
|
ResourcePoolID string `json:"resource_pool_id,omitempty"`
|
|
ResourcePoolName string `json:"resource_pool_name,omitempty"`
|
|
RuntimeHostID string `json:"runtime_host_id,omitempty"`
|
|
RuntimeHostName string `json:"runtime_host_name,omitempty"`
|
|
DatastoreIDs []string `json:"datastore_ids,omitempty"`
|
|
DatastoreNames []string `json:"datastore_names,omitempty"`
|
|
InstanceUUID string `json:"instance_uuid,omitempty"`
|
|
BIOSUUID string `json:"bios_uuid,omitempty"`
|
|
GuestOSFamily string `json:"guest_os_family,omitempty"`
|
|
GuestHostname string `json:"guest_hostname,omitempty"`
|
|
GuestIPAddresses []string `json:"guest_ip_addresses,omitempty"`
|
|
OverallStatus string `json:"overall_status,omitempty"`
|
|
TriggeredAlarms []InventoryAlarm `json:"triggered_alarms,omitempty"`
|
|
RecentTasks []InventoryTask `json:"recent_tasks,omitempty"`
|
|
RecentEvents []InventoryEvent `json:"recent_events,omitempty"`
|
|
SnapshotCount int `json:"snapshot_count,omitempty"`
|
|
Metrics *InventoryMetrics `json:"metrics,omitempty"`
|
|
}
|
|
|
|
// InventoryDatastore is the canonical phase-1 datastore summary returned by
|
|
// the vCenter Automation API list endpoint.
|
|
type InventoryDatastore struct {
|
|
Datastore string `json:"datastore"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
FreeSpace int64 `json:"free_space,omitempty"`
|
|
Capacity int64 `json:"capacity,omitempty"`
|
|
DatacenterID string `json:"datacenter_id,omitempty"`
|
|
DatacenterName string `json:"datacenter_name,omitempty"`
|
|
FolderID string `json:"folder_id,omitempty"`
|
|
FolderName string `json:"folder_name,omitempty"`
|
|
HostIDs []string `json:"host_ids,omitempty"`
|
|
HostNames []string `json:"host_names,omitempty"`
|
|
VMIDs []string `json:"vm_ids,omitempty"`
|
|
VMNames []string `json:"vm_names,omitempty"`
|
|
Accessible *bool `json:"accessible,omitempty"`
|
|
MultipleHostAccess *bool `json:"multiple_host_access,omitempty"`
|
|
MaintenanceMode string `json:"maintenance_mode,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
OverallStatus string `json:"overall_status,omitempty"`
|
|
TriggeredAlarms []InventoryAlarm `json:"triggered_alarms,omitempty"`
|
|
RecentTasks []InventoryTask `json:"recent_tasks,omitempty"`
|
|
RecentEvents []InventoryEvent `json:"recent_events,omitempty"`
|
|
}
|
|
|
|
// InventorySnapshot captures the projected inventory floor for one vCenter
|
|
// connection at one point in time.
|
|
type InventorySnapshot struct {
|
|
ConnectionID string
|
|
ConnectionName string
|
|
VCenterHost string
|
|
VIRelease string
|
|
CollectedAt time.Time
|
|
Hosts []InventoryHost
|
|
VMs []InventoryVM
|
|
Datastores []InventoryDatastore
|
|
EnrichmentIssues []InventoryEnrichmentIssue
|
|
}
|
|
|
|
// ProviderMetadata carries operator-owned vCenter connection labels onto the
|
|
// projected resource graph.
|
|
type ProviderMetadata struct {
|
|
ConnectionID string
|
|
ConnectionName string
|
|
VCenterHost string
|
|
}
|
|
|
|
// Fetcher loads a VMware inventory snapshot from a concrete source.
|
|
type Fetcher interface {
|
|
Fetch(ctx context.Context) (*InventorySnapshot, error)
|
|
}
|
|
|
|
type fetcherCloser interface {
|
|
Close()
|
|
}
|
|
|
|
// APIFetcher loads inventory from the live VMware client.
|
|
type APIFetcher struct {
|
|
Client *Client
|
|
Metadata ProviderMetadata
|
|
}
|
|
|
|
// Fetch implements Fetcher.
|
|
func (f *APIFetcher) Fetch(ctx context.Context) (*InventorySnapshot, error) {
|
|
if f == nil || f.Client == nil {
|
|
return nil, fmt.Errorf("vmware api fetcher client is nil")
|
|
}
|
|
snapshot, err := f.Client.CollectInventory(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if snapshot == nil {
|
|
return nil, fmt.Errorf("vmware api fetcher returned nil inventory")
|
|
}
|
|
snapshot.ConnectionID = strings.TrimSpace(f.Metadata.ConnectionID)
|
|
snapshot.ConnectionName = strings.TrimSpace(f.Metadata.ConnectionName)
|
|
snapshot.VCenterHost = strings.TrimSpace(f.Metadata.VCenterHost)
|
|
return snapshot, nil
|
|
}
|
|
|
|
// Close releases idle resources held by the underlying VMware client.
|
|
func (f *APIFetcher) Close() {
|
|
if f == nil || f.Client == nil {
|
|
return
|
|
}
|
|
f.Client.Close()
|
|
}
|
|
|
|
// FixtureFetcher loads inventory from static fixtures for tests.
|
|
type FixtureFetcher struct {
|
|
Snapshot InventorySnapshot
|
|
}
|
|
|
|
// Fetch implements Fetcher.
|
|
func (f *FixtureFetcher) Fetch(context.Context) (*InventorySnapshot, error) {
|
|
if f == nil {
|
|
return nil, nil
|
|
}
|
|
return cloneInventorySnapshot(&f.Snapshot), nil
|
|
}
|
|
|
|
// Provider converts VMware inventory snapshots into unified resources.
|
|
type Provider struct {
|
|
fetcher Fetcher
|
|
lastSnapshot *InventorySnapshot
|
|
mu sync.Mutex
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewLiveProvider returns a provider backed by a concrete fetcher.
|
|
func NewLiveProvider(fetcher Fetcher) *Provider {
|
|
return &Provider{
|
|
fetcher: fetcher,
|
|
now: func() time.Time {
|
|
return time.Now().UTC()
|
|
},
|
|
}
|
|
}
|
|
|
|
// NewAPIProvider returns a provider backed by the live VMware API client.
|
|
func NewAPIProvider(metadata ProviderMetadata, client *Client) *Provider {
|
|
return NewLiveProvider(&APIFetcher{
|
|
Client: client,
|
|
Metadata: metadata,
|
|
})
|
|
}
|
|
|
|
// NewProvider returns a fixture-backed provider.
|
|
func NewProvider(snapshot InventorySnapshot) *Provider {
|
|
if snapshot.CollectedAt.IsZero() {
|
|
snapshot.CollectedAt = time.Now().UTC()
|
|
}
|
|
provider := NewLiveProvider(&FixtureFetcher{Snapshot: snapshot})
|
|
provider.lastSnapshot = cloneInventorySnapshot(&snapshot)
|
|
return provider
|
|
}
|
|
|
|
// NewDefaultProvider returns a provider loaded with the default VMware fixtures.
|
|
func NewDefaultProvider() *Provider {
|
|
return NewProvider(DefaultFixtures())
|
|
}
|
|
|
|
// Refresh fetches and caches the latest snapshot.
|
|
func (p *Provider) Refresh(ctx context.Context) error {
|
|
if p == nil {
|
|
return fmt.Errorf("vmware provider is nil")
|
|
}
|
|
if p.fetcher == nil {
|
|
return fmt.Errorf("vmware provider fetcher is nil")
|
|
}
|
|
|
|
snapshot, err := p.fetcher.Fetch(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("refresh vmware inventory: %w", err)
|
|
}
|
|
if snapshot == nil {
|
|
return fmt.Errorf("vmware provider fetcher returned nil inventory")
|
|
}
|
|
|
|
sortInventorySnapshot(snapshot)
|
|
|
|
p.mu.Lock()
|
|
p.lastSnapshot = cloneInventorySnapshot(snapshot)
|
|
p.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Close releases resources held by the active fetcher, if supported.
|
|
func (p *Provider) Close() {
|
|
if p == nil || p.fetcher == nil {
|
|
return
|
|
}
|
|
if closer, ok := p.fetcher.(fetcherCloser); ok {
|
|
closer.Close()
|
|
}
|
|
}
|
|
|
|
// Snapshot returns a defensive copy of the cached inventory snapshot.
|
|
func (p *Provider) Snapshot() *InventorySnapshot {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
p.mu.Lock()
|
|
snapshot := cloneInventorySnapshot(p.lastSnapshot)
|
|
p.mu.Unlock()
|
|
return snapshot
|
|
}
|
|
|
|
// FixtureRecords projects a VMware fixture snapshot into canonical unified
|
|
// resource ingest records without consulting the runtime feature flag.
|
|
func FixtureRecords(snapshot InventorySnapshot) []unifiedresources.IngestRecord {
|
|
return vmwareRecordsFromSnapshot(&snapshot, nil)
|
|
}
|
|
|
|
// Records returns canonical VMware unified resources if the integration is enabled.
|
|
func (p *Provider) Records() []unifiedresources.IngestRecord {
|
|
if p == nil || !IsFeatureEnabled() {
|
|
return nil
|
|
}
|
|
|
|
return vmwareRecordsFromSnapshot(p.Snapshot(), p.now)
|
|
}
|
|
|
|
func vmwareRecordsFromSnapshot(snapshot *InventorySnapshot, now func() time.Time) []unifiedresources.IngestRecord {
|
|
if snapshot == nil {
|
|
return nil
|
|
}
|
|
|
|
collectedAt := snapshot.CollectedAt
|
|
if collectedAt.IsZero() {
|
|
if now != nil {
|
|
collectedAt = now().UTC()
|
|
} else {
|
|
collectedAt = time.Now().UTC()
|
|
}
|
|
}
|
|
|
|
connectionName := firstNonEmptyTrimmed(snapshot.ConnectionName, snapshot.VCenterHost, snapshot.ConnectionID)
|
|
vcenterHost := strings.TrimSpace(snapshot.VCenterHost)
|
|
records := make([]unifiedresources.IngestRecord, 0, len(snapshot.Hosts)+len(snapshot.VMs)+len(snapshot.Datastores))
|
|
hostSourceIDsByManagedObject := make(map[string]string, len(snapshot.Hosts))
|
|
for _, host := range snapshot.Hosts {
|
|
hostID := strings.TrimSpace(host.Host)
|
|
if hostID == "" {
|
|
continue
|
|
}
|
|
hostSourceIDsByManagedObject[hostID] = vmwareSourceID(snapshot.ConnectionID, "host", hostID)
|
|
}
|
|
|
|
for _, host := range snapshot.Hosts {
|
|
name := firstNonEmptyTrimmed(host.Name, host.Host)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
incidents := hostIncidents(host)
|
|
resource := unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Technology: "vmware",
|
|
Name: name,
|
|
Status: unifiedresources.IncidentsStatus(hostStatus(host), incidents),
|
|
LastSeen: collectedAt,
|
|
UpdatedAt: collectedAt,
|
|
Incidents: incidents,
|
|
Metrics: inventoryMetricsResourceMetrics(host.Metrics),
|
|
Agent: vmwareHostAgentData(snapshot, host),
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: strings.TrimSpace(snapshot.ConnectionID),
|
|
ConnectionName: connectionName,
|
|
VCenterHost: vcenterHost,
|
|
ManagedObjectID: strings.TrimSpace(host.Host),
|
|
EntityType: "host",
|
|
HostUUID: strings.TrimSpace(host.HostUUID),
|
|
DatacenterID: strings.TrimSpace(host.DatacenterID),
|
|
DatacenterName: strings.TrimSpace(host.DatacenterName),
|
|
ComputeResourceID: strings.TrimSpace(host.ComputeResourceID),
|
|
ComputeResourceName: strings.TrimSpace(host.ComputeResourceName),
|
|
ClusterID: strings.TrimSpace(host.ClusterID),
|
|
ClusterName: strings.TrimSpace(host.ClusterName),
|
|
FolderID: strings.TrimSpace(host.FolderID),
|
|
FolderName: strings.TrimSpace(host.FolderName),
|
|
ConnectionState: strings.TrimSpace(host.ConnectionState),
|
|
PowerState: strings.TrimSpace(host.PowerState),
|
|
OverallStatus: strings.TrimSpace(host.OverallStatus),
|
|
DatastoreIDs: cloneStringSlice(host.DatastoreIDs),
|
|
DatastoreNames: cloneStringSlice(host.DatastoreNames),
|
|
ActiveAlarmCount: len(host.TriggeredAlarms),
|
|
ActiveAlarmSummary: vmwareAlarmSummary(host.TriggeredAlarms),
|
|
RecentTaskCount: len(host.RecentTasks),
|
|
RecentTaskSummary: vmwareRecentTaskSummary(host.RecentTasks),
|
|
},
|
|
Tags: filterNonEmptyStrings(
|
|
"vmware",
|
|
"vsphere",
|
|
"host",
|
|
"source:vcenter",
|
|
tagWithValue("connection", strings.ToLower(connectionName)),
|
|
tagWithValue("power", strings.ToLower(strings.TrimSpace(host.PowerState))),
|
|
tagWithValue("state", strings.ToLower(strings.TrimSpace(host.ConnectionState))),
|
|
),
|
|
}
|
|
identity := unifiedresources.ResourceIdentity{
|
|
DMIUUID: strings.TrimSpace(host.HostUUID),
|
|
Hostnames: filterNonEmptyStrings(name),
|
|
ClusterName: vmwareClusterHint(host.ClusterName, host.ComputeResourceName),
|
|
}
|
|
records = append(records, unifiedresources.IngestRecord{
|
|
SourceID: vmwareSourceID(snapshot.ConnectionID, "host", host.Host),
|
|
Resource: resource,
|
|
Identity: identity,
|
|
})
|
|
}
|
|
|
|
for _, vm := range snapshot.VMs {
|
|
name := firstNonEmptyTrimmed(vm.Name, vm.VM)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
incidents := vmIncidents(vm)
|
|
resource := unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Technology: "vmware",
|
|
Name: name,
|
|
Status: unifiedresources.IncidentsStatus(vmStatus(vm), incidents),
|
|
LastSeen: collectedAt,
|
|
UpdatedAt: collectedAt,
|
|
Incidents: incidents,
|
|
Metrics: inventoryMetricsResourceMetrics(vm.Metrics),
|
|
ParentName: strings.TrimSpace(vm.RuntimeHostName),
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: strings.TrimSpace(snapshot.ConnectionID),
|
|
ConnectionName: connectionName,
|
|
VCenterHost: vcenterHost,
|
|
ManagedObjectID: strings.TrimSpace(vm.VM),
|
|
EntityType: "vm",
|
|
DatacenterID: strings.TrimSpace(vm.DatacenterID),
|
|
DatacenterName: strings.TrimSpace(vm.DatacenterName),
|
|
ComputeResourceID: strings.TrimSpace(vm.ComputeResourceID),
|
|
ComputeResourceName: strings.TrimSpace(vm.ComputeResourceName),
|
|
ClusterID: strings.TrimSpace(vm.ClusterID),
|
|
ClusterName: strings.TrimSpace(vm.ClusterName),
|
|
FolderID: strings.TrimSpace(vm.FolderID),
|
|
FolderName: strings.TrimSpace(vm.FolderName),
|
|
ResourcePoolID: strings.TrimSpace(vm.ResourcePoolID),
|
|
ResourcePoolName: strings.TrimSpace(vm.ResourcePoolName),
|
|
RuntimeHostID: strings.TrimSpace(vm.RuntimeHostID),
|
|
RuntimeHostName: strings.TrimSpace(vm.RuntimeHostName),
|
|
PowerState: strings.TrimSpace(vm.PowerState),
|
|
CPUCount: vm.CPUCount,
|
|
MemorySizeMiB: vm.MemorySizeMiB,
|
|
DatastoreIDs: cloneStringSlice(vm.DatastoreIDs),
|
|
DatastoreNames: cloneStringSlice(vm.DatastoreNames),
|
|
InstanceUUID: strings.TrimSpace(vm.InstanceUUID),
|
|
BIOSUUID: strings.TrimSpace(vm.BIOSUUID),
|
|
GuestOSFamily: strings.TrimSpace(vm.GuestOSFamily),
|
|
GuestHostname: strings.TrimSpace(vm.GuestHostname),
|
|
GuestIPAddresses: cloneStringSlice(vm.GuestIPAddresses),
|
|
OverallStatus: strings.TrimSpace(vm.OverallStatus),
|
|
ActiveAlarmCount: len(vm.TriggeredAlarms),
|
|
ActiveAlarmSummary: vmwareAlarmSummary(vm.TriggeredAlarms),
|
|
RecentTaskCount: len(vm.RecentTasks),
|
|
RecentTaskSummary: vmwareRecentTaskSummary(vm.RecentTasks),
|
|
SnapshotCount: vm.SnapshotCount,
|
|
},
|
|
Tags: filterNonEmptyStrings(
|
|
"vmware",
|
|
"vsphere",
|
|
"vm",
|
|
"source:vcenter",
|
|
tagWithValue("connection", strings.ToLower(connectionName)),
|
|
tagWithValue("power", strings.ToLower(strings.TrimSpace(vm.PowerState))),
|
|
),
|
|
}
|
|
identity := unifiedresources.ResourceIdentity{
|
|
MachineID: firstNonEmptyTrimmed(vm.InstanceUUID, vm.BIOSUUID),
|
|
Hostnames: uniqueSortedTrimmedStrings([]string{name, vm.GuestHostname}),
|
|
IPAddresses: uniqueSortedTrimmedStrings(vm.GuestIPAddresses),
|
|
ClusterName: vmwareClusterHint(vm.ClusterName, vm.ComputeResourceName),
|
|
}
|
|
records = append(records, unifiedresources.IngestRecord{
|
|
SourceID: vmwareSourceID(snapshot.ConnectionID, "vm", vm.VM),
|
|
ParentSourceID: hostSourceIDsByManagedObject[strings.TrimSpace(vm.RuntimeHostID)],
|
|
Resource: resource,
|
|
Identity: identity,
|
|
})
|
|
}
|
|
|
|
for _, datastore := range snapshot.Datastores {
|
|
name := firstNonEmptyTrimmed(datastore.Name, datastore.Datastore)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
used := datastore.Capacity - datastore.FreeSpace
|
|
if used < 0 {
|
|
used = 0
|
|
}
|
|
incidents := datastoreIncidents(datastore)
|
|
resource := unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeStorage,
|
|
Technology: "vmware",
|
|
Name: name,
|
|
Status: unifiedresources.IncidentsStatus(datastoreStatus(datastore), incidents),
|
|
LastSeen: collectedAt,
|
|
UpdatedAt: collectedAt,
|
|
Incidents: incidents,
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
Disk: diskMetric(datastore.Capacity, used),
|
|
},
|
|
Storage: &unifiedresources.StorageMeta{
|
|
Type: normalizeDatastoreType(datastore.Type),
|
|
Platform: "vmware-vsphere",
|
|
Topology: "datastore",
|
|
Enabled: vmwareDatastoreEnabled(datastore),
|
|
Active: vmwareDatastoreActive(datastore),
|
|
Shared: vmwareDatastoreShared(datastore),
|
|
Nodes: cloneStringSlice(datastore.HostNames),
|
|
ConsumerCount: len(datastore.VMNames),
|
|
ConsumerTypes: vmwareDatastoreConsumerTypes(datastore),
|
|
TopConsumers: vmwareDatastoreTopConsumers(datastore),
|
|
},
|
|
VMware: &unifiedresources.VMwareData{
|
|
ConnectionID: strings.TrimSpace(snapshot.ConnectionID),
|
|
ConnectionName: connectionName,
|
|
VCenterHost: vcenterHost,
|
|
ManagedObjectID: strings.TrimSpace(datastore.Datastore),
|
|
EntityType: "datastore",
|
|
DatacenterID: strings.TrimSpace(datastore.DatacenterID),
|
|
DatacenterName: strings.TrimSpace(datastore.DatacenterName),
|
|
FolderID: strings.TrimSpace(datastore.FolderID),
|
|
FolderName: strings.TrimSpace(datastore.FolderName),
|
|
DatastoreType: strings.TrimSpace(datastore.Type),
|
|
DatastoreURL: strings.TrimSpace(datastore.URL),
|
|
DatastoreAccessible: cloneBoolPointer(datastore.Accessible),
|
|
MultipleHostAccess: cloneBoolPointer(datastore.MultipleHostAccess),
|
|
MaintenanceMode: strings.TrimSpace(datastore.MaintenanceMode),
|
|
OverallStatus: strings.TrimSpace(datastore.OverallStatus),
|
|
ActiveAlarmCount: len(datastore.TriggeredAlarms),
|
|
ActiveAlarmSummary: vmwareAlarmSummary(datastore.TriggeredAlarms),
|
|
RecentTaskCount: len(datastore.RecentTasks),
|
|
RecentTaskSummary: vmwareRecentTaskSummary(datastore.RecentTasks),
|
|
},
|
|
Tags: filterNonEmptyStrings(
|
|
"vmware",
|
|
"vsphere",
|
|
"datastore",
|
|
"source:vcenter",
|
|
tagWithValue("connection", strings.ToLower(connectionName)),
|
|
tagWithValue("type", strings.ToLower(strings.TrimSpace(datastore.Type))),
|
|
),
|
|
}
|
|
records = append(records, unifiedresources.IngestRecord{
|
|
SourceID: vmwareSourceID(snapshot.ConnectionID, "datastore", datastore.Datastore),
|
|
Resource: resource,
|
|
})
|
|
}
|
|
|
|
return records
|
|
}
|
|
|
|
func sortInventorySnapshot(snapshot *InventorySnapshot) {
|
|
if snapshot == nil {
|
|
return
|
|
}
|
|
sort.Slice(snapshot.Hosts, func(i, j int) bool {
|
|
return vmwareSortKey(snapshot.Hosts[i].Host, snapshot.Hosts[i].Name) < vmwareSortKey(snapshot.Hosts[j].Host, snapshot.Hosts[j].Name)
|
|
})
|
|
sort.Slice(snapshot.VMs, func(i, j int) bool {
|
|
return vmwareSortKey(snapshot.VMs[i].VM, snapshot.VMs[i].Name) < vmwareSortKey(snapshot.VMs[j].VM, snapshot.VMs[j].Name)
|
|
})
|
|
sort.Slice(snapshot.Datastores, func(i, j int) bool {
|
|
return vmwareSortKey(snapshot.Datastores[i].Datastore, snapshot.Datastores[i].Name) < vmwareSortKey(snapshot.Datastores[j].Datastore, snapshot.Datastores[j].Name)
|
|
})
|
|
sort.Slice(snapshot.EnrichmentIssues, func(i, j int) bool {
|
|
return inventoryEnrichmentIssueSortKey(snapshot.EnrichmentIssues[i]) <
|
|
inventoryEnrichmentIssueSortKey(snapshot.EnrichmentIssues[j])
|
|
})
|
|
}
|
|
|
|
func cloneInventorySnapshot(in *InventorySnapshot) *InventorySnapshot {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := *in
|
|
out.Hosts = cloneInventoryHosts(in.Hosts)
|
|
out.VMs = cloneInventoryVMs(in.VMs)
|
|
out.Datastores = cloneInventoryDatastores(in.Datastores)
|
|
out.EnrichmentIssues = cloneInventoryEnrichmentIssues(in.EnrichmentIssues)
|
|
return &out
|
|
}
|
|
|
|
func cloneInventoryHosts(in []InventoryHost) []InventoryHost {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]InventoryHost, len(in))
|
|
for i := range in {
|
|
out[i] = in[i]
|
|
out[i].DatastoreIDs = cloneStringSlice(in[i].DatastoreIDs)
|
|
out[i].DatastoreNames = cloneStringSlice(in[i].DatastoreNames)
|
|
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
|
|
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
|
|
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
|
|
out[i].Metrics = cloneInventoryMetrics(in[i].Metrics)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneInventoryVMs(in []InventoryVM) []InventoryVM {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]InventoryVM, len(in))
|
|
for i := range in {
|
|
out[i] = in[i]
|
|
out[i].DatastoreIDs = cloneStringSlice(in[i].DatastoreIDs)
|
|
out[i].DatastoreNames = cloneStringSlice(in[i].DatastoreNames)
|
|
out[i].GuestIPAddresses = cloneStringSlice(in[i].GuestIPAddresses)
|
|
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
|
|
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
|
|
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
|
|
out[i].Metrics = cloneInventoryMetrics(in[i].Metrics)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneInventoryDatastores(in []InventoryDatastore) []InventoryDatastore {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]InventoryDatastore, len(in))
|
|
for i := range in {
|
|
out[i] = in[i]
|
|
out[i].HostIDs = cloneStringSlice(in[i].HostIDs)
|
|
out[i].HostNames = cloneStringSlice(in[i].HostNames)
|
|
out[i].VMIDs = cloneStringSlice(in[i].VMIDs)
|
|
out[i].VMNames = cloneStringSlice(in[i].VMNames)
|
|
out[i].Accessible = cloneBoolPointer(in[i].Accessible)
|
|
out[i].MultipleHostAccess = cloneBoolPointer(in[i].MultipleHostAccess)
|
|
out[i].TriggeredAlarms = cloneInventoryAlarms(in[i].TriggeredAlarms)
|
|
out[i].RecentTasks = cloneInventoryTasks(in[i].RecentTasks)
|
|
out[i].RecentEvents = cloneInventoryEvents(in[i].RecentEvents)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneInventoryAlarms(in []InventoryAlarm) []InventoryAlarm {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]InventoryAlarm, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func cloneInventoryTasks(in []InventoryTask) []InventoryTask {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]InventoryTask, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func cloneInventoryEvents(in []InventoryEvent) []InventoryEvent {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]InventoryEvent, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func cloneInventoryEnrichmentIssues(in []InventoryEnrichmentIssue) []InventoryEnrichmentIssue {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]InventoryEnrichmentIssue, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func inventoryEnrichmentIssueSortKey(issue InventoryEnrichmentIssue) string {
|
|
return strings.ToLower(strings.TrimSpace(issue.Stage)) + "\x00" +
|
|
strings.ToLower(strings.TrimSpace(issue.EntityType)) + "\x00" +
|
|
strings.ToLower(strings.TrimSpace(issue.EntityID)) + "\x00" +
|
|
strings.ToLower(strings.TrimSpace(issue.Category)) + "\x00" +
|
|
strings.ToLower(strings.TrimSpace(issue.Message))
|
|
}
|
|
|
|
func cloneInventoryMetrics(in *InventoryMetrics) *InventoryMetrics {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := *in
|
|
out.CPUPercent = cloneFloat64Pointer(in.CPUPercent)
|
|
out.MemoryPercent = cloneFloat64Pointer(in.MemoryPercent)
|
|
out.MemoryUsedBytes = cloneInt64Pointer(in.MemoryUsedBytes)
|
|
out.MemoryTotalBytes = cloneInt64Pointer(in.MemoryTotalBytes)
|
|
out.NetInBytesPerSecond = cloneFloat64Pointer(in.NetInBytesPerSecond)
|
|
out.NetOutBytesPerSecond = cloneFloat64Pointer(in.NetOutBytesPerSecond)
|
|
out.DiskReadBytesPerSecond = cloneFloat64Pointer(in.DiskReadBytesPerSecond)
|
|
out.DiskWriteBytesPerSecond = cloneFloat64Pointer(in.DiskWriteBytesPerSecond)
|
|
return &out
|
|
}
|
|
|
|
func vmwareSourceID(connectionID, entityType, managedObjectID string) string {
|
|
parts := filterNonEmptyStrings(strings.TrimSpace(connectionID), strings.TrimSpace(entityType), strings.TrimSpace(managedObjectID))
|
|
return strings.Join(parts, ":")
|
|
}
|
|
|
|
// SourceID returns the canonical VMware source identifier used for projected
|
|
// records, metrics targets, and mock/runtime history.
|
|
func SourceID(connectionID, entityType, managedObjectID string) string {
|
|
return vmwareSourceID(connectionID, entityType, managedObjectID)
|
|
}
|
|
|
|
func hostStatus(host InventoryHost) unifiedresources.ResourceStatus {
|
|
switch strings.ToUpper(strings.TrimSpace(host.ConnectionState)) {
|
|
case "CONNECTED":
|
|
switch strings.ToUpper(strings.TrimSpace(host.PowerState)) {
|
|
case "", "POWERED_ON":
|
|
return unifiedresources.StatusOnline
|
|
case "POWERED_OFF":
|
|
return unifiedresources.StatusOffline
|
|
default:
|
|
return unifiedresources.StatusWarning
|
|
}
|
|
case "DISCONNECTED", "NOT_RESPONDING":
|
|
return unifiedresources.StatusOffline
|
|
default:
|
|
return unifiedresources.StatusUnknown
|
|
}
|
|
}
|
|
|
|
func vmStatus(vm InventoryVM) unifiedresources.ResourceStatus {
|
|
switch strings.ToUpper(strings.TrimSpace(vm.PowerState)) {
|
|
case "POWERED_ON":
|
|
return unifiedresources.StatusOnline
|
|
case "POWERED_OFF", "SUSPENDED":
|
|
return unifiedresources.StatusOffline
|
|
default:
|
|
return unifiedresources.StatusUnknown
|
|
}
|
|
}
|
|
|
|
func datastoreStatus(datastore InventoryDatastore) unifiedresources.ResourceStatus {
|
|
if strings.TrimSpace(datastore.Datastore) == "" && strings.TrimSpace(datastore.Name) == "" {
|
|
return unifiedresources.StatusUnknown
|
|
}
|
|
if datastore.Accessible != nil && !*datastore.Accessible {
|
|
return unifiedresources.StatusOffline
|
|
}
|
|
if mode := strings.ToLower(strings.TrimSpace(datastore.MaintenanceMode)); mode != "" && mode != "normal" {
|
|
return unifiedresources.StatusWarning
|
|
}
|
|
return unifiedresources.StatusOnline
|
|
}
|
|
|
|
func hostIncidents(host InventoryHost) []unifiedresources.ResourceIncident {
|
|
return appendVMwareAlarmsAndHealthIncidents("host", host.Host, strings.TrimSpace(host.OverallStatus), host.TriggeredAlarms)
|
|
}
|
|
|
|
func vmIncidents(vm InventoryVM) []unifiedresources.ResourceIncident {
|
|
return appendVMwareAlarmsAndHealthIncidents("vm", vm.VM, strings.TrimSpace(vm.OverallStatus), vm.TriggeredAlarms)
|
|
}
|
|
|
|
func datastoreIncidents(datastore InventoryDatastore) []unifiedresources.ResourceIncident {
|
|
return appendVMwareAlarmsAndHealthIncidents("datastore", datastore.Datastore, strings.TrimSpace(datastore.OverallStatus), datastore.TriggeredAlarms)
|
|
}
|
|
|
|
func appendVMwareAlarmsAndHealthIncidents(entityType, managedObjectID, overallStatus string, alarms []InventoryAlarm) []unifiedresources.ResourceIncident {
|
|
incidents := make([]unifiedresources.ResourceIncident, 0, len(alarms)+1)
|
|
for _, alarm := range alarms {
|
|
severity, ok := vmwareRiskLevel(alarm.OverallStatus)
|
|
if !ok {
|
|
continue
|
|
}
|
|
nativeID := firstNonEmptyTrimmed(alarm.Alarm, alarm.Name, managedObjectID)
|
|
summary := vmwareAlarmIncidentSummary(entityType, managedObjectID, alarm)
|
|
startedAt := alarm.TriggeredAt
|
|
incidents = append(incidents, unifiedresources.ResourceIncident{
|
|
Provider: "vmware",
|
|
NativeID: nativeID,
|
|
Code: "vmware_alarm_state",
|
|
Severity: severity,
|
|
Source: string(unifiedresources.SourceVMware),
|
|
Summary: summary,
|
|
StartedAt: startedAt,
|
|
})
|
|
}
|
|
if len(incidents) == 0 {
|
|
if severity, ok := vmwareRiskLevel(overallStatus); ok {
|
|
incidents = append(incidents, unifiedresources.ResourceIncident{
|
|
Provider: "vmware",
|
|
NativeID: firstNonEmptyTrimmed(managedObjectID, entityType),
|
|
Code: "vmware_health_state",
|
|
Severity: severity,
|
|
Source: string(unifiedresources.SourceVMware),
|
|
Summary: vmwareOverallStatusSummary(entityType, overallStatus),
|
|
})
|
|
}
|
|
}
|
|
return incidents
|
|
}
|
|
|
|
func vmwareRiskLevel(status string) (storagehealth.RiskLevel, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
|
case "red":
|
|
return storagehealth.RiskCritical, true
|
|
case "yellow":
|
|
return storagehealth.RiskWarning, true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func vmwareAlarmIncidentSummary(entityType, managedObjectID string, alarm InventoryAlarm) string {
|
|
entityLabel := vmwareEntityLabel(entityType)
|
|
alarmName := firstNonEmptyTrimmed(alarm.Name, alarm.Alarm)
|
|
status := strings.ToLower(strings.TrimSpace(alarm.OverallStatus))
|
|
if alarmName == "" {
|
|
alarmName = "VMware alarm"
|
|
}
|
|
if status == "" {
|
|
status = "active"
|
|
}
|
|
if ref := strings.TrimSpace(managedObjectID); ref != "" {
|
|
return fmt.Sprintf("%s %s has VMware alarm %s (%s)", entityLabel, ref, alarmName, status)
|
|
}
|
|
return fmt.Sprintf("%s has VMware alarm %s (%s)", entityLabel, alarmName, status)
|
|
}
|
|
|
|
func vmwareOverallStatusSummary(entityType, overallStatus string) string {
|
|
entityLabel := vmwareEntityLabel(entityType)
|
|
status := strings.ToLower(strings.TrimSpace(overallStatus))
|
|
if status == "" {
|
|
status = "degraded"
|
|
}
|
|
return fmt.Sprintf("%s has VMware overall status %s", entityLabel, status)
|
|
}
|
|
|
|
func vmwareEntityLabel(entityType string) string {
|
|
switch strings.ToLower(strings.TrimSpace(entityType)) {
|
|
case "host":
|
|
return "Host"
|
|
case "vm":
|
|
return "VM"
|
|
case "datastore":
|
|
return "Datastore"
|
|
default:
|
|
return "Resource"
|
|
}
|
|
}
|
|
|
|
func vmwareAlarmSummary(alarms []InventoryAlarm) string {
|
|
if len(alarms) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(alarms))
|
|
for _, alarm := range alarms {
|
|
name := firstNonEmptyTrimmed(alarm.Name, alarm.Alarm)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
status := strings.ToLower(strings.TrimSpace(alarm.OverallStatus))
|
|
if status == "" {
|
|
parts = append(parts, name)
|
|
continue
|
|
}
|
|
parts = append(parts, name+" ("+status+")")
|
|
if len(parts) == 3 {
|
|
break
|
|
}
|
|
}
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
if len(alarms) > len(parts) {
|
|
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(alarms)-len(parts))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func vmwareRecentTaskSummary(tasks []InventoryTask) string {
|
|
if len(tasks) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, 0, len(tasks))
|
|
for _, task := range tasks {
|
|
name := firstNonEmptyTrimmed(task.Name, task.DescriptionID, task.Task)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
state := strings.ToLower(strings.TrimSpace(task.State))
|
|
if state == "" {
|
|
parts = append(parts, name)
|
|
} else {
|
|
parts = append(parts, name+" ("+state+")")
|
|
}
|
|
if len(parts) == 3 {
|
|
break
|
|
}
|
|
}
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
if len(tasks) > len(parts) {
|
|
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(tasks)-len(parts))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func normalizeDatastoreType(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func vmwareClusterHint(clusterName, computeResourceName string) string {
|
|
return firstNonEmptyTrimmed(clusterName, computeResourceName)
|
|
}
|
|
|
|
func vmwareDatastoreEnabled(datastore InventoryDatastore) bool {
|
|
if datastore.Accessible == nil {
|
|
return true
|
|
}
|
|
return *datastore.Accessible
|
|
}
|
|
|
|
func vmwareDatastoreActive(datastore InventoryDatastore) bool {
|
|
if datastore.Accessible != nil && !*datastore.Accessible {
|
|
return false
|
|
}
|
|
mode := strings.ToLower(strings.TrimSpace(datastore.MaintenanceMode))
|
|
return mode == "" || mode == "normal"
|
|
}
|
|
|
|
func vmwareDatastoreShared(datastore InventoryDatastore) bool {
|
|
if datastore.MultipleHostAccess != nil {
|
|
return *datastore.MultipleHostAccess
|
|
}
|
|
return len(datastore.HostNames) > 1
|
|
}
|
|
|
|
func vmwareDatastoreConsumerTypes(datastore InventoryDatastore) []string {
|
|
if len(datastore.VMNames) == 0 {
|
|
return nil
|
|
}
|
|
return []string{string(unifiedresources.ResourceTypeVM)}
|
|
}
|
|
|
|
func vmwareDatastoreTopConsumers(datastore InventoryDatastore) []unifiedresources.StorageConsumerMeta {
|
|
if len(datastore.VMNames) == 0 {
|
|
return nil
|
|
}
|
|
consumers := make([]unifiedresources.StorageConsumerMeta, 0, len(datastore.VMNames))
|
|
for _, name := range datastore.VMNames {
|
|
if strings.TrimSpace(name) == "" {
|
|
continue
|
|
}
|
|
consumer := unifiedresources.StorageConsumerMeta{
|
|
ResourceType: unifiedresources.ResourceTypeVM,
|
|
Name: strings.TrimSpace(name),
|
|
}
|
|
consumers = append(consumers, consumer)
|
|
if len(consumers) == 5 {
|
|
break
|
|
}
|
|
}
|
|
if len(consumers) == 0 {
|
|
return nil
|
|
}
|
|
return consumers
|
|
}
|
|
|
|
func diskMetric(total, used int64) *unifiedresources.MetricValue {
|
|
if total <= 0 {
|
|
return nil
|
|
}
|
|
totalCopy := total
|
|
usedCopy := used
|
|
percent := (float64(used) / float64(total)) * 100
|
|
return &unifiedresources.MetricValue{
|
|
Total: &totalCopy,
|
|
Used: &usedCopy,
|
|
Value: percent,
|
|
Percent: percent,
|
|
Unit: "bytes",
|
|
}
|
|
}
|
|
|
|
func inventoryMetricsResourceMetrics(in *InventoryMetrics) *unifiedresources.ResourceMetrics {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
|
|
metrics := &unifiedresources.ResourceMetrics{}
|
|
if in.CPUPercent != nil {
|
|
metrics.CPU = &unifiedresources.MetricValue{
|
|
Value: *in.CPUPercent,
|
|
Percent: *in.CPUPercent,
|
|
Unit: "percent",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.MemoryPercent != nil {
|
|
metrics.Memory = &unifiedresources.MetricValue{
|
|
Percent: *in.MemoryPercent,
|
|
Unit: "bytes",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
if in.MemoryUsedBytes != nil {
|
|
used := *in.MemoryUsedBytes
|
|
metrics.Memory.Used = &used
|
|
}
|
|
if in.MemoryTotalBytes != nil {
|
|
total := *in.MemoryTotalBytes
|
|
metrics.Memory.Total = &total
|
|
}
|
|
}
|
|
if in.NetInBytesPerSecond != nil {
|
|
metrics.NetIn = &unifiedresources.MetricValue{
|
|
Value: *in.NetInBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.NetOutBytesPerSecond != nil {
|
|
metrics.NetOut = &unifiedresources.MetricValue{
|
|
Value: *in.NetOutBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.DiskReadBytesPerSecond != nil {
|
|
metrics.DiskRead = &unifiedresources.MetricValue{
|
|
Value: *in.DiskReadBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
if in.DiskWriteBytesPerSecond != nil {
|
|
metrics.DiskWrite = &unifiedresources.MetricValue{
|
|
Value: *in.DiskWriteBytesPerSecond,
|
|
Unit: "bytes/s",
|
|
Source: unifiedresources.SourceVMware,
|
|
}
|
|
}
|
|
|
|
if metrics.CPU == nil &&
|
|
metrics.Memory == nil &&
|
|
metrics.NetIn == nil &&
|
|
metrics.NetOut == nil &&
|
|
metrics.DiskRead == nil &&
|
|
metrics.DiskWrite == nil {
|
|
return nil
|
|
}
|
|
return metrics
|
|
}
|
|
|
|
func vmwareHostAgentData(snapshot *InventorySnapshot, host InventoryHost) *unifiedresources.AgentData {
|
|
if snapshot == nil {
|
|
return nil
|
|
}
|
|
|
|
agent := &unifiedresources.AgentData{
|
|
AgentID: vmwareSourceID(snapshot.ConnectionID, "host", host.Host),
|
|
Hostname: firstNonEmptyTrimmed(host.Name, host.Host),
|
|
MachineID: strings.TrimSpace(host.HostUUID),
|
|
Platform: "vmware-vsphere",
|
|
OSName: "VMware ESXi",
|
|
OSVersion: strings.TrimSpace(snapshot.VIRelease),
|
|
NetInRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.NetInBytesPerSecond }),
|
|
NetOutRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.NetOutBytesPerSecond }),
|
|
DiskReadRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.DiskReadBytesPerSecond }),
|
|
DiskWriteRate: inventoryMetricFloat64(host.Metrics, func(m *InventoryMetrics) *float64 { return m.DiskWriteBytesPerSecond }),
|
|
}
|
|
|
|
if host.Metrics != nil && host.Metrics.MemoryTotalBytes != nil {
|
|
total := *host.Metrics.MemoryTotalBytes
|
|
used := int64(0)
|
|
if host.Metrics.MemoryUsedBytes != nil {
|
|
used = *host.Metrics.MemoryUsedBytes
|
|
}
|
|
free := total - used
|
|
if free < 0 {
|
|
free = 0
|
|
}
|
|
agent.Memory = &unifiedresources.AgentMemoryMeta{
|
|
Total: total,
|
|
Used: used,
|
|
Free: free,
|
|
}
|
|
}
|
|
|
|
return agent
|
|
}
|
|
|
|
func inventoryMetricFloat64(metrics *InventoryMetrics, pick func(*InventoryMetrics) *float64) float64 {
|
|
if metrics == nil || pick == nil {
|
|
return 0
|
|
}
|
|
value := pick(metrics)
|
|
if value == nil {
|
|
return 0
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func cloneFloat64Pointer(in *float64) *float64 {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
value := *in
|
|
return &value
|
|
}
|
|
|
|
func cloneInt64Pointer(in *int64) *int64 {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
value := *in
|
|
return &value
|
|
}
|
|
|
|
func cloneBoolPointer(in *bool) *bool {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
value := *in
|
|
return &value
|
|
}
|
|
|
|
func cloneStringSlice(in []string) []string {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
out := make([]string, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|
|
|
|
func firstNonEmptyTrimmed(values ...string) string {
|
|
for _, value := range values {
|
|
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func filterNonEmptyStrings(values ...string) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
seen := make(map[string]struct{}, len(values))
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(trimmed)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, trimmed)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func tagWithValue(prefix, value string) string {
|
|
prefix = strings.TrimSpace(prefix)
|
|
value = strings.TrimSpace(value)
|
|
if prefix == "" || value == "" {
|
|
return ""
|
|
}
|
|
return prefix + ":" + value
|
|
}
|
|
|
|
func vmwareSortKey(id, name string) string {
|
|
return firstNonEmptyTrimmed(strings.ToLower(strings.TrimSpace(id)), strings.ToLower(strings.TrimSpace(name)))
|
|
}
|