Files
pulse/internal/mock/platform_fixtures.go
T
rcourtman ce8c8f918c Share demo generation and stabilize workload parent groups
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.
2026-10-12 11:26:29 +01:00

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 ""
}
}