mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 21:47:20 +00:00
ce8c8f918c
Demo fixture assembly duplicated freshness and operator-link ordering. Reuse the live monitor builder, retain explicit in-memory links and capture indexed metrics targets without persisting fixture identities. vSphere group keys also changed when display metadata arrived, remounting open drawers and discarding History selection. Keep groups on canonical parent IDs and use explicit cluster metadata instead of a VM's own name.
698 lines
25 KiB
Go
698 lines
25 KiB
Go
package mock
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
|
|
)
|
|
|
|
const DefaultPlatformPollIntervalSeconds = 60
|
|
|
|
type PlatformFixtures struct {
|
|
TrueNAS truenas.FixtureSnapshot
|
|
VMware vmware.InventorySnapshot
|
|
}
|
|
|
|
type TrueNASConnectionFixture struct {
|
|
ID string
|
|
Name string
|
|
Host string
|
|
Port int
|
|
APIKey string
|
|
UseHTTPS bool
|
|
Enabled bool
|
|
PollIntervalSeconds int
|
|
CollectedAt time.Time
|
|
ResourceID string
|
|
Systems int
|
|
StoragePools int
|
|
Datasets int
|
|
Apps int
|
|
Disks int
|
|
RecoveryArtifacts int
|
|
}
|
|
|
|
type VMwareConnectionFixture struct {
|
|
ID string
|
|
Name string
|
|
Host string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
Enabled bool
|
|
PollIntervalSeconds int
|
|
CollectedAt time.Time
|
|
Hosts int
|
|
VMs int
|
|
Datastores int
|
|
Networks int
|
|
VIRelease string
|
|
}
|
|
|
|
func defaultPlatformFixtures() PlatformFixtures {
|
|
return PlatformFixtures{
|
|
TrueNAS: truenas.DefaultFixtures(),
|
|
VMware: vmware.DefaultFixtures(),
|
|
}
|
|
}
|
|
|
|
func rebasePlatformFixtures(fixtures PlatformFixtures, now time.Time) PlatformFixtures {
|
|
target := now.UTC()
|
|
return PlatformFixtures{
|
|
TrueNAS: rebaseTrueNASPlatformFixture(refreshTrueNASPlatformFixture(fixtures.TrueNAS, target), target),
|
|
VMware: rebaseVMwarePlatformFixture(refreshVMwarePlatformFixture(fixtures.VMware, target), target),
|
|
}
|
|
}
|
|
|
|
func DefaultTrueNASConnectionFixture() TrueNASConnectionFixture {
|
|
return defaultTrueNASConnectionFixture(currentOrDefaultPlatformFixtures())
|
|
}
|
|
|
|
func DefaultVMwareConnectionFixture() VMwareConnectionFixture {
|
|
fixtures := currentOrDefaultPlatformFixtures().VMware
|
|
|
|
return VMwareConnectionFixture{
|
|
ID: strings.TrimSpace(fixtures.ConnectionID),
|
|
Name: strings.TrimSpace(fixtures.ConnectionName),
|
|
Host: strings.TrimSpace(fixtures.VCenterHost),
|
|
Port: 443,
|
|
Username: "administrator@vsphere.local",
|
|
Password: "mock-vcenter-password",
|
|
Enabled: true,
|
|
PollIntervalSeconds: DefaultPlatformPollIntervalSeconds,
|
|
CollectedAt: fixtures.CollectedAt,
|
|
Hosts: len(fixtures.Hosts),
|
|
VMs: len(fixtures.VMs),
|
|
Datastores: len(fixtures.Datastores),
|
|
Networks: len(fixtures.Networks),
|
|
VIRelease: strings.TrimSpace(fixtures.VIRelease),
|
|
}
|
|
}
|
|
|
|
func defaultTrueNASConnectionFixture(fixtures PlatformFixtures) TrueNASConnectionFixture {
|
|
snapshot := fixtures.TrueNAS
|
|
collectedAt := trueNASCollectedAt(snapshot)
|
|
host := strings.TrimSpace(snapshot.System.Hostname)
|
|
|
|
return TrueNASConnectionFixture{
|
|
ID: "truenas-mock-1",
|
|
Name: "Archive NAS",
|
|
Host: host,
|
|
Port: 443,
|
|
APIKey: "mock-truenas-api-key",
|
|
UseHTTPS: true,
|
|
Enabled: true,
|
|
PollIntervalSeconds: DefaultPlatformPollIntervalSeconds,
|
|
CollectedAt: collectedAt,
|
|
ResourceID: host,
|
|
Systems: 1,
|
|
StoragePools: len(snapshot.Pools),
|
|
Datasets: len(snapshot.Datasets),
|
|
Apps: len(snapshot.Apps),
|
|
Disks: len(snapshot.Disks),
|
|
RecoveryArtifacts: len(snapshot.ZFSSnapshots) + len(snapshot.ReplicationTasks),
|
|
}
|
|
}
|
|
|
|
// SupplementalRecords serves the fixture graph's provider-backed records while
|
|
// mock mode is on and nothing once it is off. The router's mock adapters stay
|
|
// registered for a moment after leaving mock mode, and a live refresh that
|
|
// still reached them must not ingest and alert on fixture resources.
|
|
func SupplementalRecords(source unifiedresources.DataSource) []unifiedresources.IngestRecord {
|
|
if !IsMockEnabled() {
|
|
return nil
|
|
}
|
|
return CurrentFixtureGraph().SupplementalRecords(source)
|
|
}
|
|
|
|
// SupplementalChanges serves the fixture graph's provider activity while mock
|
|
// mode is on and nothing once it is off, for the same reason.
|
|
func SupplementalChanges(source unifiedresources.DataSource) []unifiedresources.ResourceChange {
|
|
if !IsMockEnabled() {
|
|
return nil
|
|
}
|
|
return CurrentFixtureGraph().SupplementalChanges(source)
|
|
}
|
|
|
|
func SupplementalOwnedSources() []unifiedresources.DataSource {
|
|
return []unifiedresources.DataSource{
|
|
unifiedresources.SourceTrueNAS,
|
|
unifiedresources.SourceVMware,
|
|
unifiedresources.SourceAvailability,
|
|
}
|
|
}
|
|
|
|
// unifiedSnapshotMemo caches the package-level UnifiedResourceSnapshot result
|
|
// per FixtureDataVersion. Building the snapshot constructs a full throwaway
|
|
// registry (ingest-clone in, List-clone out) — before this memo, every
|
|
// consumer paid that on every read, which dominated the demo's allocation
|
|
// profile.
|
|
var unifiedSnapshotMemo struct {
|
|
mu sync.Mutex
|
|
valid bool
|
|
version uint64
|
|
resources []unifiedresources.Resource
|
|
freshness time.Time
|
|
}
|
|
|
|
// UnifiedResourceSnapshot returns the current mock world as unified
|
|
// resources. The returned slice and the nested data of its elements are
|
|
// shared between callers — treat them as read-only. Every current consumer
|
|
// either ingests them into a registry (which deep-clones) or copies the slice
|
|
// before writing top-level fields.
|
|
func UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
|
|
if !IsMockEnabled() {
|
|
return nil, time.Time{}
|
|
}
|
|
|
|
unifiedSnapshotMemo.mu.Lock()
|
|
defer unifiedSnapshotMemo.mu.Unlock()
|
|
|
|
// Read the version before fetching the graph: a tick that lands between
|
|
// the two can only make the cached data newer than its token, which
|
|
// forces a harmless rebuild on the next call — never a stale serve.
|
|
version := FixtureDataVersion()
|
|
if unifiedSnapshotMemo.valid && unifiedSnapshotMemo.version == version {
|
|
return unifiedSnapshotMemo.resources, unifiedSnapshotMemo.freshness
|
|
}
|
|
|
|
resources, freshness := CurrentFixtureGraph().UnifiedResourceSnapshot()
|
|
unifiedSnapshotMemo.valid = true
|
|
unifiedSnapshotMemo.version = version
|
|
unifiedSnapshotMemo.resources = resources
|
|
unifiedSnapshotMemo.freshness = freshness
|
|
return resources, freshness
|
|
}
|
|
|
|
// UnifiedResourceSnapshotWithLinks returns the current mock world as unified
|
|
// resources with the operator's manual links applied where the live monitor's
|
|
// registry rebuild applies them: once the snapshot and every supplemental
|
|
// record source are in, and before availability. A chain whose members arrive
|
|
// as supplemental records is judged over the whole estate, so it settles on the
|
|
// row the live view keeps, and an availability check that names the folded
|
|
// resource by source resolves through the fold onto the primary. Folding the
|
|
// finished list instead loses both. The registry has no store, so nothing the
|
|
// fixtures ingest is persisted. Without links this is UnifiedResourceSnapshot;
|
|
// with links it builds afresh on every call, so callers cache it per fixture
|
|
// data version and link set, and its result is the caller's own.
|
|
func UnifiedResourceSnapshotWithLinks(links []unifiedresources.ResourceLink) ([]unifiedresources.Resource, time.Time) {
|
|
if len(links) == 0 {
|
|
return UnifiedResourceSnapshot()
|
|
}
|
|
if !IsMockEnabled() {
|
|
return nil, time.Time{}
|
|
}
|
|
return CurrentFixtureGraph().unifiedResourceSnapshot(unifiedresources.NewRegistryWithManualLinks(links))
|
|
}
|
|
|
|
func (g FixtureGraph) UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
|
|
return g.unifiedResourceSnapshot(unifiedresources.NewRegistry(nil))
|
|
}
|
|
|
|
func (g FixtureGraph) unifiedResourceSnapshot(registry *unifiedresources.ResourceRegistry) ([]unifiedresources.Resource, time.Time) {
|
|
// The demo uses the live generation builder for source ordering, operator
|
|
// links, availability projection and the final freshness verdict.
|
|
owned := SupplementalOwnedSources()
|
|
records := make(map[unifiedresources.DataSource][]unifiedresources.IngestRecord, len(owned))
|
|
for _, source := range owned {
|
|
records[source] = g.SupplementalRecords(source)
|
|
}
|
|
adapter := unifiedresources.NewMonitorAdapter(registry)
|
|
adapter.PopulateSnapshotAndSupplemental(unifiedresources.SnapshotWithoutSources(g.State, owned), records)
|
|
|
|
freshness := g.State.LastUpdate
|
|
for _, candidate := range []time.Time{
|
|
trueNASCollectedAt(g.PlatformFixtures.TrueNAS),
|
|
g.PlatformFixtures.VMware.CollectedAt,
|
|
availabilityFixturesFreshness(g.AvailabilityFixtures),
|
|
} {
|
|
if candidate.IsZero() {
|
|
continue
|
|
}
|
|
if freshness.IsZero() || candidate.After(freshness) {
|
|
freshness = candidate
|
|
}
|
|
}
|
|
|
|
resources, targets := adapter.GetAllWithMetricsTargets()
|
|
for i := range resources {
|
|
if target, ok := targets[resources[i].ID]; ok {
|
|
resources[i].MetricsTarget = &target
|
|
}
|
|
}
|
|
|
|
return resources, freshness
|
|
}
|
|
|
|
func (g FixtureGraph) SupplementalRecords(source unifiedresources.DataSource) []unifiedresources.IngestRecord {
|
|
switch normalizeSupplementalSource(source) {
|
|
case unifiedresources.SourceTrueNAS:
|
|
return truenas.FixtureRecords(g.PlatformFixtures.TrueNAS)
|
|
case unifiedresources.SourceVMware:
|
|
return vmware.FixtureRecords(g.PlatformFixtures.VMware)
|
|
case unifiedresources.SourceAvailability:
|
|
return availabilityFixtureRecords(g.AvailabilityFixtures, availabilityFixturesFreshness(g.AvailabilityFixtures))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (g FixtureGraph) SupplementalChanges(source unifiedresources.DataSource) []unifiedresources.ResourceChange {
|
|
switch normalizeSupplementalSource(source) {
|
|
case unifiedresources.SourceVMware:
|
|
return vmware.FixtureActivityChanges(g.PlatformFixtures.VMware)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func trueNASCollectedAt(fixtures truenas.FixtureSnapshot) time.Time {
|
|
if !fixtures.CollectedAt.IsZero() {
|
|
return fixtures.CollectedAt
|
|
}
|
|
return fixtures.System.CollectedAt
|
|
}
|
|
|
|
func rebaseTrueNASPlatformFixture(snapshot truenas.FixtureSnapshot, target time.Time) truenas.FixtureSnapshot {
|
|
out := cloneTrueNASFixtureSnapshot(snapshot)
|
|
anchor := trueNASCollectedAt(snapshot)
|
|
if anchor.IsZero() {
|
|
anchor = target
|
|
}
|
|
shift := target.Sub(anchor)
|
|
|
|
out.CollectedAt = target
|
|
out.System.CollectedAt = shiftTime(snapshot.System.CollectedAt, shift, target)
|
|
|
|
for i := range out.Alerts {
|
|
out.Alerts[i].Datetime = shiftTime(snapshot.Alerts[i].Datetime, shift, target)
|
|
}
|
|
for i := range out.Apps {
|
|
if out.Apps[i].Stats != nil {
|
|
out.Apps[i].Stats.CollectedAt = shiftTime(snapshot.Apps[i].Stats.CollectedAt, shift, target)
|
|
}
|
|
}
|
|
for i := range out.ZFSSnapshots {
|
|
if snapshot.ZFSSnapshots[i].CreatedAt != nil {
|
|
rebased := shiftTime(*snapshot.ZFSSnapshots[i].CreatedAt, shift, target)
|
|
out.ZFSSnapshots[i].CreatedAt = &rebased
|
|
}
|
|
}
|
|
for i := range out.ReplicationTasks {
|
|
if snapshot.ReplicationTasks[i].LastRun != nil {
|
|
rebased := shiftTime(*snapshot.ReplicationTasks[i].LastRun, shift, target)
|
|
out.ReplicationTasks[i].LastRun = &rebased
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func rebaseVMwarePlatformFixture(snapshot vmware.InventorySnapshot, target time.Time) vmware.InventorySnapshot {
|
|
out := cloneVMwareInventorySnapshot(snapshot)
|
|
anchor := snapshot.CollectedAt
|
|
if anchor.IsZero() {
|
|
anchor = target
|
|
}
|
|
shift := target.Sub(anchor)
|
|
|
|
out.CollectedAt = target
|
|
for i := range out.Hosts {
|
|
rebaseVMwareAlarms(out.Hosts[i].TriggeredAlarms, snapshot.Hosts[i].TriggeredAlarms, shift, target)
|
|
rebaseVMwareTasks(out.Hosts[i].RecentTasks, snapshot.Hosts[i].RecentTasks, shift, target)
|
|
rebaseVMwareEvents(out.Hosts[i].RecentEvents, snapshot.Hosts[i].RecentEvents, shift, target)
|
|
}
|
|
for i := range out.VMs {
|
|
rebaseVMwareAlarms(out.VMs[i].TriggeredAlarms, snapshot.VMs[i].TriggeredAlarms, shift, target)
|
|
rebaseVMwareTasks(out.VMs[i].RecentTasks, snapshot.VMs[i].RecentTasks, shift, target)
|
|
rebaseVMwareEvents(out.VMs[i].RecentEvents, snapshot.VMs[i].RecentEvents, shift, target)
|
|
}
|
|
for i := range out.Datastores {
|
|
rebaseVMwareAlarms(out.Datastores[i].TriggeredAlarms, snapshot.Datastores[i].TriggeredAlarms, shift, target)
|
|
rebaseVMwareTasks(out.Datastores[i].RecentTasks, snapshot.Datastores[i].RecentTasks, shift, target)
|
|
rebaseVMwareEvents(out.Datastores[i].RecentEvents, snapshot.Datastores[i].RecentEvents, shift, target)
|
|
}
|
|
for i := range out.Networks {
|
|
rebaseVMwareAlarms(out.Networks[i].TriggeredAlarms, snapshot.Networks[i].TriggeredAlarms, shift, target)
|
|
rebaseVMwareTasks(out.Networks[i].RecentTasks, snapshot.Networks[i].RecentTasks, shift, target)
|
|
rebaseVMwareEvents(out.Networks[i].RecentEvents, snapshot.Networks[i].RecentEvents, shift, target)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func refreshTrueNASPlatformFixture(snapshot truenas.FixtureSnapshot, at time.Time) truenas.FixtureSnapshot {
|
|
out := cloneTrueNASFixtureSnapshot(snapshot)
|
|
hostname := strings.TrimSpace(out.System.Hostname)
|
|
if hostname != "" {
|
|
out.System.CPUPercent = SampleMetric("agent", hostname, "cpu", at)
|
|
out.System.DiskReadRate = SampleMetric("agent", hostname, "diskread", at)
|
|
out.System.DiskWriteRate = SampleMetric("agent", hostname, "diskwrite", at)
|
|
out.System.NetInRate = SampleMetric("agent", hostname, "netin", at)
|
|
out.System.NetOutRate = SampleMetric("agent", hostname, "netout", at)
|
|
if out.System.MemoryTotalBytes > 0 {
|
|
memoryPercent := SampleMetric("agent", hostname, "memory", at)
|
|
usedBytes := bytesFromPercent(out.System.MemoryTotalBytes, memoryPercent)
|
|
out.System.MemoryAvailableBytes = out.System.MemoryTotalBytes - usedBytes
|
|
if out.System.MemoryAvailableBytes < 0 {
|
|
out.System.MemoryAvailableBytes = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := range out.Pools {
|
|
usage := SampleMetric("storage", TrueNASPoolMetricID(out.System.Hostname, out.Pools[i].Name), "usage", at)
|
|
applyTrueNASCapacityUsage(&out.Pools[i].UsedBytes, &out.Pools[i].FreeBytes, out.Pools[i].TotalBytes, usage)
|
|
}
|
|
|
|
for i := range out.Datasets {
|
|
totalBytes := out.Datasets[i].UsedBytes + out.Datasets[i].AvailBytes
|
|
usage := SampleMetric("storage", TrueNASDatasetMetricID(out.System.Hostname, out.Datasets[i].Name), "usage", at)
|
|
usedBytes := bytesFromPercent(totalBytes, usage)
|
|
out.Datasets[i].UsedBytes = usedBytes
|
|
out.Datasets[i].AvailBytes = totalBytes - usedBytes
|
|
if out.Datasets[i].AvailBytes < 0 {
|
|
out.Datasets[i].AvailBytes = 0
|
|
}
|
|
}
|
|
|
|
for i := range out.Disks {
|
|
resourceID := trueNASDiskMetricID(out.Disks[i])
|
|
if resourceID == "" {
|
|
continue
|
|
}
|
|
out.Disks[i].Temperature = int(math.Round(SampleMetric("disk", resourceID, "smart_temp", at)))
|
|
refreshTrueNASDiskAggregate(&out.Disks[i].TemperatureAggregate, out.Disks[i].Temperature)
|
|
}
|
|
|
|
for i := range out.Apps {
|
|
stats := out.Apps[i].Stats
|
|
if stats == nil {
|
|
continue
|
|
}
|
|
|
|
appID := TrueNASAppMetricID(out.System.Hostname, out.Apps[i])
|
|
if appID == "" {
|
|
continue
|
|
}
|
|
|
|
stats.CollectedAt = at
|
|
stopped := strings.EqualFold(strings.TrimSpace(out.Apps[i].State), "stopped")
|
|
if stopped {
|
|
stats.CPUPercent = 0
|
|
stats.NetInRate = 0
|
|
stats.NetOutRate = 0
|
|
stats.DiskReadRate = 0
|
|
stats.DiskWriteRate = 0
|
|
stats.BlockReadBytes = 0
|
|
stats.BlockWriteBytes = 0
|
|
if out.System.MemoryTotalBytes > 0 {
|
|
idlePercent := clampFloat(SampleMetric("dockerContainer", appID, "memory", at)*0.12, 0.5, 8)
|
|
stats.MemoryBytes = bytesFromPercent(out.System.MemoryTotalBytes, idlePercent)
|
|
}
|
|
} else {
|
|
stats.CPUPercent = SampleMetric("dockerContainer", appID, "cpu", at)
|
|
stats.NetInRate = SampleMetric("dockerContainer", appID, "netin", at)
|
|
stats.NetOutRate = SampleMetric("dockerContainer", appID, "netout", at)
|
|
stats.DiskReadRate = SampleMetric("dockerContainer", appID, "diskread", at)
|
|
stats.DiskWriteRate = SampleMetric("dockerContainer", appID, "diskwrite", at)
|
|
stats.BlockReadBytes = int64(math.Round(stats.DiskReadRate * math.Max(1, float64(stats.IntervalSeconds))))
|
|
stats.BlockWriteBytes = int64(math.Round(stats.DiskWriteRate * math.Max(1, float64(stats.IntervalSeconds))))
|
|
if out.System.MemoryTotalBytes > 0 {
|
|
memoryPercent := SampleMetric("dockerContainer", appID, "memory", at)
|
|
stats.MemoryBytes = bytesFromPercent(out.System.MemoryTotalBytes, memoryPercent)
|
|
}
|
|
}
|
|
refreshTrueNASAppInterfaces(stats)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func refreshVMwarePlatformFixture(snapshot vmware.InventorySnapshot, at time.Time) vmware.InventorySnapshot {
|
|
out := cloneVMwareInventorySnapshot(snapshot)
|
|
for i := range out.Hosts {
|
|
refreshVMwareInventoryMetrics(out.Hosts[i].Metrics, "agent", vmware.SourceID(out.ConnectionID, "host", out.Hosts[i].Host), at)
|
|
}
|
|
for i := range out.VMs {
|
|
resourceID := vmware.SourceID(out.ConnectionID, "vm", out.VMs[i].VM)
|
|
refreshVMwareInventoryMetrics(out.VMs[i].Metrics, "vm", resourceID, at)
|
|
if strings.EqualFold(strings.TrimSpace(out.VMs[i].PowerState), "powered_off") && out.VMs[i].Metrics != nil {
|
|
*out.VMs[i].Metrics.CPUPercent = 0
|
|
*out.VMs[i].Metrics.NetInBytesPerSecond = 0
|
|
*out.VMs[i].Metrics.NetOutBytesPerSecond = 0
|
|
*out.VMs[i].Metrics.DiskReadBytesPerSecond = 0
|
|
*out.VMs[i].Metrics.DiskWriteBytesPerSecond = 0
|
|
// A powered-off VM has no current uptime, and VMware Tools is
|
|
// obviously not reporting guest filesystem usage. Drop the
|
|
// pointers entirely so the frontend renders "—" rather than 0,
|
|
// matching how Pulse signals "no data" for an offline guest.
|
|
out.VMs[i].Metrics.UptimeSeconds = nil
|
|
out.VMs[i].Metrics.DiskUsedBytes = nil
|
|
out.VMs[i].Metrics.DiskTotalBytes = nil
|
|
out.VMs[i].Metrics.DiskPercent = nil
|
|
}
|
|
}
|
|
for i := range out.Datastores {
|
|
total := out.Datastores[i].Capacity
|
|
if total <= 0 {
|
|
continue
|
|
}
|
|
usage := SampleMetric("storage", vmware.SourceID(out.ConnectionID, "datastore", out.Datastores[i].Datastore), "usage", at)
|
|
used := bytesFromPercent(total, usage)
|
|
out.Datastores[i].FreeSpace = total - used
|
|
if out.Datastores[i].FreeSpace < 0 {
|
|
out.Datastores[i].FreeSpace = 0
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func applyTrueNASCapacityUsage(usedBytes *int64, freeBytes *int64, totalBytes int64, usagePercent float64) {
|
|
if usedBytes == nil || freeBytes == nil || totalBytes <= 0 {
|
|
return
|
|
}
|
|
used := bytesFromPercent(totalBytes, usagePercent)
|
|
*usedBytes = used
|
|
*freeBytes = totalBytes - used
|
|
if *freeBytes < 0 {
|
|
*freeBytes = 0
|
|
}
|
|
}
|
|
|
|
func bytesFromPercent(totalBytes int64, usagePercent float64) int64 {
|
|
if totalBytes <= 0 {
|
|
return 0
|
|
}
|
|
usage := clampFloat(usagePercent, 0, 100)
|
|
used := int64(math.Round((float64(totalBytes) * usage) / 100.0))
|
|
if used < 0 {
|
|
return 0
|
|
}
|
|
if used > totalBytes {
|
|
return totalBytes
|
|
}
|
|
return used
|
|
}
|
|
|
|
func refreshTrueNASDiskAggregate(aggregate *truenas.DiskTemperatureAggregate, current int) {
|
|
if aggregate == nil {
|
|
return
|
|
}
|
|
currentFloat := float64(current)
|
|
if aggregate.WindowDays <= 0 {
|
|
aggregate.WindowDays = 7
|
|
}
|
|
if aggregate.MinCelsius <= 0 || aggregate.MinCelsius > currentFloat {
|
|
aggregate.MinCelsius = math.Max(25, currentFloat-4)
|
|
}
|
|
if aggregate.MaxCelsius <= 0 || aggregate.MaxCelsius < currentFloat {
|
|
aggregate.MaxCelsius = math.Min(95, currentFloat+5)
|
|
}
|
|
aggregate.AvgCelsius = clampFloat(currentFloat-0.8, aggregate.MinCelsius, aggregate.MaxCelsius)
|
|
}
|
|
|
|
func trueNASDiskMetricID(disk truenas.Disk) string {
|
|
resourceID := strings.TrimSpace(disk.Serial)
|
|
if resourceID == "" {
|
|
resourceID = strings.TrimSpace(disk.ID)
|
|
}
|
|
if resourceID == "" {
|
|
resourceID = strings.TrimSpace(disk.Name)
|
|
}
|
|
return resourceID
|
|
}
|
|
|
|
func refreshTrueNASAppInterfaces(stats *truenas.AppStats) {
|
|
if stats == nil || len(stats.Interfaces) == 0 {
|
|
return
|
|
}
|
|
|
|
var totalRx float64
|
|
var totalTx float64
|
|
for _, iface := range stats.Interfaces {
|
|
totalRx += iface.RxBytesPS
|
|
totalTx += iface.TxBytesPS
|
|
}
|
|
|
|
for i := range stats.Interfaces {
|
|
rxShare := 1 / float64(len(stats.Interfaces))
|
|
txShare := 1 / float64(len(stats.Interfaces))
|
|
if totalRx > 0 {
|
|
rxShare = stats.Interfaces[i].RxBytesPS / totalRx
|
|
}
|
|
if totalTx > 0 {
|
|
txShare = stats.Interfaces[i].TxBytesPS / totalTx
|
|
}
|
|
stats.Interfaces[i].RxBytesPS = stats.NetInRate * rxShare
|
|
stats.Interfaces[i].TxBytesPS = stats.NetOutRate * txShare
|
|
}
|
|
}
|
|
|
|
func refreshVMwareInventoryMetrics(metrics *vmware.InventoryMetrics, resourceClass, resourceID string, at time.Time) {
|
|
if metrics == nil || strings.TrimSpace(resourceID) == "" {
|
|
return
|
|
}
|
|
|
|
memoryTotal := int64(0)
|
|
if metrics.MemoryTotalBytes != nil {
|
|
memoryTotal = *metrics.MemoryTotalBytes
|
|
}
|
|
|
|
*ensureFloat64Ptr(&metrics.CPUPercent) = SampleMetric(resourceClass, resourceID, "cpu", at)
|
|
*ensureFloat64Ptr(&metrics.MemoryPercent) = SampleMetric(resourceClass, resourceID, "memory", at)
|
|
*ensureFloat64Ptr(&metrics.NetInBytesPerSecond) = SampleMetric(resourceClass, resourceID, "netin", at)
|
|
*ensureFloat64Ptr(&metrics.NetOutBytesPerSecond) = SampleMetric(resourceClass, resourceID, "netout", at)
|
|
*ensureFloat64Ptr(&metrics.DiskReadBytesPerSecond) = SampleMetric(resourceClass, resourceID, "diskread", at)
|
|
*ensureFloat64Ptr(&metrics.DiskWriteBytesPerSecond) = SampleMetric(resourceClass, resourceID, "diskwrite", at)
|
|
if memoryTotal > 0 {
|
|
*ensureInt64Ptr(&metrics.MemoryUsedBytes) = bytesFromPercent(memoryTotal, *metrics.MemoryPercent)
|
|
}
|
|
|
|
// Uptime: a per-resource stable base age (1-30 days) that climbs forward
|
|
// with `at`. Matches what a real vCenter would report from
|
|
// sys.uptime.latest (and sys.osUptime.latest for VMs with Tools running).
|
|
*ensureInt64Ptr(&metrics.UptimeSeconds) = mockUptimeSeconds(resourceClass, resourceID, at)
|
|
|
|
// Guest filesystem usage: only meaningful for VMs (hosts don't have a
|
|
// `guest` shape in vSphere). For VMs we synthesize a stable total
|
|
// capacity and let SampleMetric oscillate usage naturally so the table
|
|
// renders realistic-looking bars instead of always-empty cells.
|
|
if resourceClass == "vm" {
|
|
total := mockGuestDiskTotalBytes(resourceID)
|
|
usage := clampFloat(SampleMetric(resourceClass, resourceID, "diskusage", at), 0, 100)
|
|
*ensureInt64Ptr(&metrics.DiskTotalBytes) = total
|
|
*ensureInt64Ptr(&metrics.DiskUsedBytes) = bytesFromPercent(total, usage)
|
|
*ensureFloat64Ptr(&metrics.DiskPercent) = usage
|
|
}
|
|
}
|
|
|
|
// mockUptimeSeconds returns a stable, slowly-incrementing uptime for a
|
|
// vSphere mock resource. The base age is derived from the resource ID so
|
|
// each host / VM has its own multi-day uptime, then `at` shifts it forward
|
|
// so the column ticks naturally between snapshots.
|
|
func mockUptimeSeconds(resourceClass, resourceID string, at time.Time) int64 {
|
|
seed := fnv64a(resourceClass + "|" + resourceID + "|uptime")
|
|
// Base age between 1 hour and 30 days
|
|
const minSeconds = uint64(3600)
|
|
const maxSeconds = uint64(30 * 24 * 60 * 60)
|
|
span := maxSeconds - minSeconds
|
|
baseSeconds := int64(minSeconds + (seed % span))
|
|
if at.IsZero() {
|
|
return baseSeconds
|
|
}
|
|
// Anchor the climb at a stable epoch so successive refreshes ramp up
|
|
// without resetting; use 2026-01-01 UTC.
|
|
anchor := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
if at.Before(anchor) {
|
|
return baseSeconds
|
|
}
|
|
return baseSeconds + int64(at.Sub(anchor)/time.Second)
|
|
}
|
|
|
|
// mockGuestDiskTotalBytes returns a per-VM stable guest filesystem total
|
|
// (between 32 GiB and 256 GiB) so the same VM always appears at the same
|
|
// size across refreshes.
|
|
func mockGuestDiskTotalBytes(resourceID string) int64 {
|
|
const (
|
|
minGiB = int64(32)
|
|
maxGiB = int64(256)
|
|
)
|
|
seed := fnv64a(resourceID + "|disktotal")
|
|
gib := minGiB + int64(seed%uint64(maxGiB-minGiB+1))
|
|
return gib * 1024 * 1024 * 1024
|
|
}
|
|
|
|
// fnv64a is a tiny deterministic hash so mock helpers can derive stable
|
|
// per-resource seeds without pulling in a math/rand source.
|
|
func fnv64a(input string) uint64 {
|
|
const (
|
|
offset uint64 = 14695981039346656037
|
|
prime uint64 = 1099511628211
|
|
)
|
|
hash := offset
|
|
for i := 0; i < len(input); i++ {
|
|
hash ^= uint64(input[i])
|
|
hash *= prime
|
|
}
|
|
return hash
|
|
}
|
|
|
|
func ensureFloat64Ptr(target **float64) *float64 {
|
|
if *target == nil {
|
|
*target = new(float64)
|
|
}
|
|
return *target
|
|
}
|
|
|
|
func ensureInt64Ptr(target **int64) *int64 {
|
|
if *target == nil {
|
|
*target = new(int64)
|
|
}
|
|
return *target
|
|
}
|
|
|
|
func rebaseVMwareAlarms(out []vmware.InventoryAlarm, in []vmware.InventoryAlarm, shift time.Duration, target time.Time) {
|
|
for i := range out {
|
|
out[i].TriggeredAt = shiftTime(in[i].TriggeredAt, shift, target)
|
|
}
|
|
}
|
|
|
|
func rebaseVMwareTasks(out []vmware.InventoryTask, in []vmware.InventoryTask, shift time.Duration, target time.Time) {
|
|
for i := range out {
|
|
out[i].StartedAt = shiftTime(in[i].StartedAt, shift, target)
|
|
out[i].CompletedAt = shiftTime(in[i].CompletedAt, shift, target)
|
|
}
|
|
}
|
|
|
|
func rebaseVMwareEvents(out []vmware.InventoryEvent, in []vmware.InventoryEvent, shift time.Duration, target time.Time) {
|
|
for i := range out {
|
|
out[i].CreatedAt = shiftTime(in[i].CreatedAt, shift, target)
|
|
}
|
|
}
|
|
|
|
func shiftTime(value time.Time, shift time.Duration, fallback time.Time) time.Time {
|
|
if value.IsZero() {
|
|
return fallback
|
|
}
|
|
return value.Add(shift)
|
|
}
|
|
|
|
func normalizeSupplementalSource(source unifiedresources.DataSource) unifiedresources.DataSource {
|
|
switch strings.ToLower(strings.TrimSpace(string(source))) {
|
|
case "truenas":
|
|
return unifiedresources.SourceTrueNAS
|
|
case "vmware", "vmware-vsphere":
|
|
return unifiedresources.SourceVMware
|
|
case "availability", "network-endpoint", "network-endpoints":
|
|
return unifiedresources.SourceAvailability
|
|
default:
|
|
return ""
|
|
}
|
|
}
|