mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-01 21:10:13 +00:00
Addresses #101 v4.23.0 introduced a regression where systems with only NVMe temperatures (no CPU sensor) would display "No CPU sensor" in the UI. This was caused by the Available flag being set to true when NVMe temps existed, even without CPU data, triggering the error message in the frontend. Backend changes: - Add HasCPU and HasNVMe boolean fields to Temperature model - Extend CPU sensor detection to support more chip types: zenpower, k8temp, acpitz, it87 (case-insensitive matching) - HasCPU is set based on CPU chip detection (coretemp, k10temp, etc.), not value thresholds - This prevents false negatives when sensors report 0°C during resets - CPU temperature values now accepted even when 0 (checked with !IsNaN instead of > 0) - extractTempInput returns NaN instead of 0 when no data found - Available flag means "any temperature data exists" for backward compatibility - Update mock generator to properly set the new flags - Add unit tests for NVMe-only and 0°C scenarios to prevent regression - Removed amd_energy from CPU chip list (power sensor, not temperature) Frontend changes: - Add hasCPU and hasNVMe optional fields to Temperature interface - Update NodeSummaryTable to check hasCPU flag with fallback to available for backward compatibility with older API responses - Update NodeCard temperature display logic with same fallback pattern - Systems with only NVMe temps now show "-" instead of error message - Fallback ensures UI works with both old and new API responses Testing: - All unit tests pass including NVMe-only and 0°C test cases - Fix prevents false "no CPU sensor" errors when sensors temporarily report 0°C - Fix eliminates false "no CPU sensor" errors for NVMe-only systems
2972 lines
86 KiB
Go
2972 lines
86 KiB
Go
package mock
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
type MockConfig struct {
|
|
NodeCount int
|
|
VMsPerNode int
|
|
LXCsPerNode int
|
|
DockerHostCount int
|
|
DockerContainersPerHost int
|
|
RandomMetrics bool
|
|
HighLoadNodes []string // Specific nodes to simulate high load
|
|
StoppedPercent float64 // Percentage of guests that should be stopped
|
|
}
|
|
|
|
const dockerConnectionPrefix = "docker-"
|
|
|
|
var DefaultConfig = MockConfig{
|
|
NodeCount: 7, // Test the 5-9 node range by default
|
|
VMsPerNode: 5,
|
|
LXCsPerNode: 8,
|
|
DockerHostCount: 3,
|
|
DockerContainersPerHost: 12,
|
|
RandomMetrics: true,
|
|
StoppedPercent: 0.2,
|
|
}
|
|
|
|
var appNames = []string{
|
|
"jellyfin", "plex", "nextcloud", "pihole", "homeassistant",
|
|
"gitlab", "postgres", "mysql", "redis", "nginx",
|
|
"traefik", "portainer", "grafana", "prometheus", "influxdb",
|
|
"sonarr", "radarr", "transmission", "deluge", "sabnzbd",
|
|
"bitwarden", "vaultwarden", "wireguard", "openvpn", "cloudflare",
|
|
"minecraft", "valheim", "terraria", "factorio", "csgo",
|
|
"webserver", "database", "cache", "loadbalancer", "firewall",
|
|
"docker", "kubernetes", "rancher", "jenkins", "gitea",
|
|
"syncthing", "seafile", "owncloud", "minio", "sftp",
|
|
}
|
|
|
|
var dockerHostPrefixes = []string{
|
|
"nebula", "orion", "aurora", "atlas", "zephyr",
|
|
"draco", "phoenix", "hydra", "pegasus", "lyra",
|
|
}
|
|
|
|
var dockerOperatingSystems = []string{
|
|
"Debian GNU/Linux 12 (bookworm)",
|
|
"Ubuntu 24.04.1 LTS",
|
|
"Ubuntu 22.04.4 LTS",
|
|
"Fedora Linux 40",
|
|
"Alpine Linux 3.19",
|
|
}
|
|
|
|
var dockerKernelVersions = []string{
|
|
"6.8.12-1-amd64",
|
|
"6.6.32-1-lts",
|
|
"6.5.0-27-generic",
|
|
"6.1.55-1-arm64",
|
|
}
|
|
|
|
var dockerArchitectures = []string{"x86_64", "aarch64"}
|
|
|
|
var dockerVersions = []string{
|
|
"27.3.1",
|
|
"26.1.3",
|
|
"25.0.6",
|
|
}
|
|
|
|
var dockerAgentVersions = []string{
|
|
"0.1.0",
|
|
"0.1.0-dev",
|
|
}
|
|
|
|
// Common tags used for VMs and containers
|
|
var commonTags = []string{
|
|
"production", "staging", "development", "testing",
|
|
"web", "database", "cache", "queue", "storage",
|
|
"frontend", "backend", "api", "microservice",
|
|
"docker", "kubernetes", "monitoring", "logging",
|
|
"backup", "critical", "important", "experimental",
|
|
"media", "gaming", "home", "automation",
|
|
"public", "private", "dmz", "internal",
|
|
"linux", "windows", "debian", "ubuntu", "alpine",
|
|
"managed", "unmanaged", "legacy", "deprecated",
|
|
"team-a", "team-b", "customer-1", "project-x",
|
|
}
|
|
|
|
var vmMountpoints = []string{
|
|
"/",
|
|
"/var",
|
|
"/home",
|
|
"/srv",
|
|
"/opt",
|
|
"/data",
|
|
"/backup",
|
|
"/logs",
|
|
"/mnt/data",
|
|
"/mnt/backup",
|
|
}
|
|
|
|
var vmFilesystemTypes = []string{"ext4", "xfs", "btrfs", "zfs"}
|
|
var vmDevices = []string{"vda", "vdb", "vdc", "sda", "sdb", "sdc", "nvme0n1", "nvme1n1"}
|
|
|
|
func generateVirtualDisks() ([]models.Disk, models.Disk) {
|
|
diskCount := 1 + rand.Intn(3)
|
|
disks := make([]models.Disk, 0, diskCount)
|
|
usedMounts := make(map[string]struct{})
|
|
var total int64
|
|
var used int64
|
|
|
|
for i := 0; i < diskCount; i++ {
|
|
mount := "/"
|
|
if i == 0 {
|
|
usedMounts[mount] = struct{}{}
|
|
} else {
|
|
candidate := vmMountpoints[rand.Intn(len(vmMountpoints))]
|
|
for _, taken := usedMounts[candidate]; taken && len(usedMounts) < len(vmMountpoints); _, taken = usedMounts[candidate] {
|
|
candidate = vmMountpoints[rand.Intn(len(vmMountpoints))]
|
|
}
|
|
mount = candidate
|
|
usedMounts[mount] = struct{}{}
|
|
}
|
|
|
|
sizeGiB := 40 + rand.Intn(360) // 40 - 400 GiB
|
|
totalBytes := int64(sizeGiB) * 1024 * 1024 * 1024
|
|
usage := 0.25 + rand.Float64()*0.6
|
|
usedBytes := int64(float64(totalBytes) * usage)
|
|
|
|
device := vmDevices[i%len(vmDevices)]
|
|
fsType := vmFilesystemTypes[rand.Intn(len(vmFilesystemTypes))]
|
|
|
|
disks = append(disks, models.Disk{
|
|
Total: totalBytes,
|
|
Used: usedBytes,
|
|
Free: totalBytes - usedBytes,
|
|
Usage: usage * 100,
|
|
Mountpoint: mount,
|
|
Type: fsType,
|
|
Device: fmt.Sprintf("/dev/%s", device),
|
|
})
|
|
|
|
total += totalBytes
|
|
used += usedBytes
|
|
}
|
|
|
|
aggregated := models.Disk{
|
|
Total: total,
|
|
Used: used,
|
|
Free: total - used,
|
|
}
|
|
if total > 0 {
|
|
aggregated.Usage = float64(used) / float64(total) * 100
|
|
}
|
|
|
|
return disks, aggregated
|
|
}
|
|
|
|
func GenerateMockData(config MockConfig) models.StateSnapshot {
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
data := models.StateSnapshot{
|
|
Nodes: generateNodes(config),
|
|
DockerHosts: generateDockerHosts(config),
|
|
VMs: []models.VM{},
|
|
Containers: []models.Container{},
|
|
PhysicalDisks: []models.PhysicalDisk{},
|
|
LastUpdate: time.Now(),
|
|
ConnectionHealth: make(map[string]bool),
|
|
Stats: models.Stats{},
|
|
ActiveAlerts: []models.Alert{},
|
|
}
|
|
|
|
// Generate physical disks for each node
|
|
for _, node := range data.Nodes {
|
|
data.PhysicalDisks = append(data.PhysicalDisks, generateDisksForNode(node)...)
|
|
}
|
|
|
|
for _, host := range data.DockerHosts {
|
|
data.ConnectionHealth[dockerConnectionPrefix+host.ID] = host.Status != "offline"
|
|
}
|
|
|
|
// Generate VMs and containers for each node
|
|
vmidCounter := 100
|
|
for nodeIdx, node := range data.Nodes {
|
|
// Determine node specialty for more realistic distribution
|
|
nodeRole := "mixed"
|
|
if nodeIdx > 0 {
|
|
roleRand := rand.Float64()
|
|
if roleRand < 0.3 {
|
|
nodeRole = "vm-heavy" // 30% chance of being VM-focused
|
|
} else if roleRand < 0.5 {
|
|
nodeRole = "container-heavy" // 20% chance of being container-focused
|
|
} else if roleRand < 0.6 {
|
|
nodeRole = "light" // 10% chance of having few guests
|
|
}
|
|
// 40% remain mixed
|
|
}
|
|
|
|
// Calculate VM count based on node role
|
|
vmCount := config.VMsPerNode
|
|
lxcCount := config.LXCsPerNode
|
|
|
|
switch nodeRole {
|
|
case "vm-heavy":
|
|
vmCount = config.VMsPerNode + rand.Intn(config.VMsPerNode) // 100-200% of base
|
|
lxcCount = config.LXCsPerNode/2 + rand.Intn(config.LXCsPerNode/2) // 50-75% of base
|
|
case "container-heavy":
|
|
vmCount = rand.Intn(config.VMsPerNode/2 + 1) // 0-50% of base
|
|
lxcCount = config.LXCsPerNode*2 + rand.Intn(config.LXCsPerNode) // 200-300% of base
|
|
case "light":
|
|
vmCount = rand.Intn(config.VMsPerNode/2 + 1) // 0-50% of base
|
|
lxcCount = rand.Intn(config.LXCsPerNode/2 + 1) // 0-50% of base
|
|
default: // mixed
|
|
// Add some variation
|
|
vmCount = config.VMsPerNode + rand.Intn(5) - 2 // +/- 2
|
|
lxcCount = config.LXCsPerNode + rand.Intn(7) - 3 // +/- 3
|
|
}
|
|
|
|
// Ensure at least some activity on most nodes
|
|
if nodeIdx < 3 && vmCount == 0 && lxcCount == 0 {
|
|
if rand.Float64() < 0.5 {
|
|
vmCount = 1 + rand.Intn(2)
|
|
} else {
|
|
lxcCount = 2 + rand.Intn(3)
|
|
}
|
|
}
|
|
|
|
if node.Status == "offline" {
|
|
// Create placeholder offline guests with zeroed metrics
|
|
if vmCount == 0 {
|
|
vmCount = 1
|
|
}
|
|
for i := 0; i < vmCount; i++ {
|
|
vm := generateVM(node.Name, node.Instance, vmidCounter, config)
|
|
vm.Status = "stopped"
|
|
vm.CPU = 0
|
|
vm.Memory.Used = 0
|
|
vm.Memory.Usage = 0
|
|
vm.Memory.Free = vm.Memory.Total
|
|
vm.Memory.SwapUsed = 0
|
|
vm.Disk.Used = 0
|
|
vm.Disk.Free = vm.Disk.Total
|
|
vm.Disk.Usage = -1
|
|
vm.NetworkIn = 0
|
|
vm.NetworkOut = 0
|
|
vm.DiskRead = 0
|
|
vm.DiskWrite = 0
|
|
vm.Uptime = 0
|
|
data.VMs = append(data.VMs, vm)
|
|
vmidCounter++
|
|
}
|
|
|
|
if lxcCount == 0 {
|
|
lxcCount = 1
|
|
}
|
|
for i := 0; i < lxcCount; i++ {
|
|
lxc := generateContainer(node.Name, node.Instance, vmidCounter, config)
|
|
lxc.Status = "stopped"
|
|
lxc.CPU = 0
|
|
lxc.Memory.Used = 0
|
|
lxc.Memory.Usage = 0
|
|
lxc.Memory.Free = lxc.Memory.Total
|
|
lxc.Memory.SwapUsed = 0
|
|
lxc.Disk.Used = 0
|
|
lxc.Disk.Free = lxc.Disk.Total
|
|
lxc.Disk.Usage = -1
|
|
lxc.NetworkIn = 0
|
|
lxc.NetworkOut = 0
|
|
lxc.DiskRead = 0
|
|
lxc.DiskWrite = 0
|
|
lxc.Uptime = 0
|
|
data.Containers = append(data.Containers, lxc)
|
|
vmidCounter++
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Generate VMs
|
|
for i := 0; i < vmCount; i++ {
|
|
vm := generateVM(node.Name, node.Instance, vmidCounter, config)
|
|
data.VMs = append(data.VMs, vm)
|
|
vmidCounter++
|
|
}
|
|
|
|
// Generate containers
|
|
for i := 0; i < lxcCount; i++ {
|
|
lxc := generateContainer(node.Name, node.Instance, vmidCounter, config)
|
|
data.Containers = append(data.Containers, lxc)
|
|
vmidCounter++
|
|
}
|
|
|
|
// Set connection health
|
|
data.ConnectionHealth[fmt.Sprintf("pve-%s", node.Name)] = true
|
|
}
|
|
|
|
// Generate storage for each node
|
|
data.Storage = generateStorage(data.Nodes)
|
|
|
|
// Generate Ceph cluster data if Ceph-backed storage is present
|
|
data.CephClusters = generateCephClusters(data.Nodes, data.Storage)
|
|
|
|
// Generate PBS instances and backups
|
|
data.PBSInstances = generatePBSInstances()
|
|
data.PBSBackups = generatePBSBackups(data.VMs, data.Containers)
|
|
|
|
// Set PBS connection health
|
|
for _, pbs := range data.PBSInstances {
|
|
data.ConnectionHealth[fmt.Sprintf("pbs-%s", pbs.Name)] = true
|
|
}
|
|
|
|
// Generate PMG instances and mail data
|
|
data.PMGInstances = generatePMGInstances()
|
|
for _, pmg := range data.PMGInstances {
|
|
data.ConnectionHealth[fmt.Sprintf("pmg-%s", pmg.Name)] = true
|
|
}
|
|
|
|
// Generate backups for VMs and containers
|
|
data.PVEBackups = models.PVEBackups{
|
|
BackupTasks: []models.BackupTask{},
|
|
StorageBackups: generateBackups(data.VMs, data.Containers),
|
|
GuestSnapshots: generateSnapshots(data.VMs, data.Containers),
|
|
}
|
|
|
|
// Calculate stats
|
|
data.Stats.StartTime = time.Now()
|
|
data.Stats.Uptime = 0
|
|
data.Stats.Version = "v4.9.0-mock"
|
|
|
|
return data
|
|
}
|
|
|
|
func generateNodes(config MockConfig) []models.Node {
|
|
nodes := make([]models.Node, 0, config.NodeCount)
|
|
|
|
// First 5 nodes are part of the cluster
|
|
clusterNodeCount := 5
|
|
if config.NodeCount < 5 {
|
|
clusterNodeCount = config.NodeCount
|
|
}
|
|
|
|
// Generate clustered nodes
|
|
for i := 0; i < clusterNodeCount; i++ {
|
|
nodeName := fmt.Sprintf("pve%d", i+1)
|
|
isHighLoad := false
|
|
for _, n := range config.HighLoadNodes {
|
|
if n == nodeName {
|
|
isHighLoad = true
|
|
break
|
|
}
|
|
}
|
|
|
|
node := generateNode(nodeName, isHighLoad, config)
|
|
node.Instance = "mock-cluster" // Part of cluster
|
|
node.DisplayName = fmt.Sprintf("%s (%s)", node.Instance, nodeName)
|
|
node.IsClusterMember = true
|
|
node.ClusterName = "mock-cluster"
|
|
// ID format matches real system: instance-nodename
|
|
node.ID = fmt.Sprintf("%s-%s", node.Instance, nodeName)
|
|
|
|
// Make pve3 offline to test offline node handling
|
|
if nodeName == "pve3" {
|
|
node.Status = "offline"
|
|
node.CPU = 0
|
|
node.Memory.Used = 0
|
|
node.Memory.Usage = 0
|
|
node.Memory.Free = node.Memory.Total
|
|
node.Uptime = 0
|
|
node.LoadAverage = []float64{0, 0, 0}
|
|
node.Disk.Used = 0
|
|
node.Disk.Usage = 0
|
|
node.Disk.Free = node.Disk.Total
|
|
if node.Temperature != nil {
|
|
node.Temperature = &models.Temperature{Available: false}
|
|
}
|
|
node.ConnectionHealth = "offline"
|
|
} else {
|
|
// For cluster nodes, since one is offline, the cluster is degraded
|
|
node.ConnectionHealth = "degraded"
|
|
}
|
|
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
// Generate standalone nodes (if we have more than 5 nodes)
|
|
for i := clusterNodeCount; i < config.NodeCount; i++ {
|
|
nodeName := fmt.Sprintf("standalone%d", i-clusterNodeCount+1)
|
|
isHighLoad := false
|
|
for _, n := range config.HighLoadNodes {
|
|
if n == nodeName {
|
|
isHighLoad = true
|
|
break
|
|
}
|
|
}
|
|
|
|
node := generateNode(nodeName, isHighLoad, config)
|
|
node.Instance = nodeName // Standalone - instance matches name
|
|
node.DisplayName = node.Instance
|
|
node.IsClusterMember = false
|
|
node.ClusterName = "" // Empty for standalone
|
|
node.ConnectionHealth = "healthy" // Standalone nodes are healthy if online
|
|
// ID format matches real system: instance-nodename (same when standalone)
|
|
node.ID = fmt.Sprintf("%s-%s", node.Instance, nodeName)
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
func generateNode(name string, highLoad bool, config MockConfig) models.Node {
|
|
baseLoad := 0.15
|
|
if highLoad {
|
|
baseLoad = 0.75
|
|
}
|
|
|
|
cpu := baseLoad + rand.Float64()*0.2
|
|
if !config.RandomMetrics {
|
|
cpu = baseLoad
|
|
}
|
|
|
|
// Memory in GB
|
|
totalMem := int64(32 + rand.Intn(96)) // 32-128 GB
|
|
usedMem := int64(float64(totalMem) * (baseLoad + rand.Float64()*0.3))
|
|
|
|
// Disk in GB
|
|
totalDisk := int64(500 + rand.Intn(2000)) // 500-2500 GB
|
|
usedDisk := int64(float64(totalDisk) * (0.3 + rand.Float64()*0.4))
|
|
|
|
// Generate CPU info
|
|
coreCounts := []int{4, 8, 12, 16, 24, 32, 48, 64}
|
|
cores := coreCounts[rand.Intn(len(coreCounts))]
|
|
|
|
// Generate realistic version information
|
|
pveVersions := []string{"8.2.4", "8.2.2", "8.1.10", "8.0.12", "7.4-18"}
|
|
kernelVersions := []string{
|
|
"6.8.12-1-pve",
|
|
"6.8.8-2-pve",
|
|
"6.5.13-5-pve",
|
|
"6.2.16-20-pve",
|
|
"5.15.143-1-pve",
|
|
}
|
|
|
|
// Generate temperature data
|
|
temp := generateNodeTemperature(cores)
|
|
|
|
return models.Node{
|
|
Name: name,
|
|
DisplayName: name,
|
|
Instance: "", // Set by generateNodes based on cluster/standalone
|
|
Type: "pve",
|
|
Status: "online",
|
|
Uptime: int64(86400 * (1 + rand.Intn(30))), // 1-30 days
|
|
CPU: cpu,
|
|
PVEVersion: pveVersions[rand.Intn(len(pveVersions))],
|
|
KernelVersion: kernelVersions[rand.Intn(len(kernelVersions))],
|
|
Memory: models.Memory{
|
|
Total: totalMem * 1024 * 1024 * 1024, // Convert to bytes
|
|
Used: usedMem * 1024 * 1024 * 1024,
|
|
Free: (totalMem - usedMem) * 1024 * 1024 * 1024,
|
|
Usage: float64(usedMem) / float64(totalMem) * 100,
|
|
},
|
|
Disk: models.Disk{
|
|
Total: totalDisk * 1024 * 1024 * 1024, // Convert to bytes
|
|
Used: usedDisk * 1024 * 1024 * 1024,
|
|
Free: (totalDisk - usedDisk) * 1024 * 1024 * 1024,
|
|
Usage: float64(usedDisk) / float64(totalDisk) * 100,
|
|
},
|
|
CPUInfo: models.CPUInfo{
|
|
Model: "Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz",
|
|
Cores: cores,
|
|
Sockets: cores / 4, // Assume 4 cores per socket
|
|
MHz: "2400",
|
|
},
|
|
Temperature: temp,
|
|
Host: fmt.Sprintf("https://%s.local:8006", name),
|
|
ID: "", // Will be set by generateNodes to match real format: instance-nodename
|
|
}
|
|
}
|
|
|
|
// generateNodeTemperature generates realistic temperature data for a node
|
|
func generateNodeTemperature(cores int) *models.Temperature {
|
|
// 30% chance of no temperature data (sensors not available)
|
|
if rand.Float64() < 0.3 {
|
|
return &models.Temperature{
|
|
Available: false,
|
|
HasCPU: false,
|
|
HasNVMe: false,
|
|
}
|
|
}
|
|
|
|
// Generate CPU package temperature (40-75°C normal range)
|
|
cpuPackage := 40.0 + rand.Float64()*35.0
|
|
if rand.Float64() < 0.1 { // 10% chance of high temp
|
|
cpuPackage = 75.0 + rand.Float64()*15.0 // 75-90°C
|
|
}
|
|
|
|
// Generate core temperatures (similar to package, with variation)
|
|
coreTemps := make([]models.CoreTemp, cores)
|
|
maxTemp := cpuPackage
|
|
for i := 0; i < cores; i++ {
|
|
coreTemp := cpuPackage + (rand.Float64()-0.5)*10.0 // ±5°C from package
|
|
if coreTemp < 30.0 {
|
|
coreTemp = 30.0
|
|
}
|
|
if coreTemp > maxTemp {
|
|
maxTemp = coreTemp
|
|
}
|
|
coreTemps[i] = models.CoreTemp{
|
|
Core: i,
|
|
Temp: coreTemp,
|
|
}
|
|
}
|
|
|
|
// Generate NVMe temperatures (0-2 NVMe drives)
|
|
numNVMe := rand.Intn(3)
|
|
nvmeTemps := make([]models.NVMeTemp, numNVMe)
|
|
for i := 0; i < numNVMe; i++ {
|
|
nvmeTemp := 35.0 + rand.Float64()*40.0 // 35-75°C normal range
|
|
if rand.Float64() < 0.05 { // 5% chance of high temp
|
|
nvmeTemp = 75.0 + rand.Float64()*10.0 // 75-85°C
|
|
}
|
|
nvmeTemps[i] = models.NVMeTemp{
|
|
Device: fmt.Sprintf("nvme%d", i),
|
|
Temp: nvmeTemp,
|
|
}
|
|
}
|
|
|
|
return &models.Temperature{
|
|
CPUPackage: cpuPackage,
|
|
CPUMax: maxTemp,
|
|
Cores: coreTemps,
|
|
NVMe: nvmeTemps,
|
|
Available: true,
|
|
HasCPU: true,
|
|
HasNVMe: len(nvmeTemps) > 0,
|
|
LastUpdate: time.Now(),
|
|
}
|
|
}
|
|
|
|
// generateRealisticIO generates more realistic I/O values
|
|
// Most systems are idle or have very low I/O
|
|
func generateRealisticIO(ioType string) int64 {
|
|
chance := rand.Float64()
|
|
|
|
switch ioType {
|
|
case "disk-read":
|
|
if chance < 0.60 { // 60% are idle
|
|
return 0
|
|
} else if chance < 0.85 { // 25% have low activity
|
|
return int64(rand.Intn(5)) * 1024 * 1024 // 0-5 MB/s
|
|
} else if chance < 0.95 { // 10% moderate
|
|
return int64(5+rand.Intn(20)) * 1024 * 1024 // 5-25 MB/s
|
|
} else { // 5% high activity
|
|
return int64(25+rand.Intn(75)) * 1024 * 1024 // 25-100 MB/s
|
|
}
|
|
|
|
case "disk-write":
|
|
if chance < 0.70 { // 70% are idle (writes are less common)
|
|
return 0
|
|
} else if chance < 0.90 { // 20% have low activity
|
|
return int64(rand.Intn(3)) * 1024 * 1024 // 0-3 MB/s
|
|
} else if chance < 0.97 { // 7% moderate
|
|
return int64(3+rand.Intn(15)) * 1024 * 1024 // 3-18 MB/s
|
|
} else { // 3% high activity
|
|
return int64(18+rand.Intn(32)) * 1024 * 1024 // 18-50 MB/s
|
|
}
|
|
|
|
case "network-in":
|
|
if chance < 0.50 { // 50% are idle
|
|
return 0
|
|
} else if chance < 0.80 { // 30% have low activity
|
|
return int64(rand.Intn(10)) * 1024 * 1024 / 8 // 0-10 Mbps
|
|
} else if chance < 0.93 { // 13% moderate
|
|
return int64(10+rand.Intn(90)) * 1024 * 1024 / 8 // 10-100 Mbps
|
|
} else { // 7% high activity
|
|
return int64(100+rand.Intn(400)) * 1024 * 1024 / 8 // 100-500 Mbps
|
|
}
|
|
|
|
case "network-out":
|
|
if chance < 0.55 { // 55% are idle
|
|
return 0
|
|
} else if chance < 0.82 { // 27% have low activity
|
|
return int64(rand.Intn(5)) * 1024 * 1024 / 8 // 0-5 Mbps
|
|
} else if chance < 0.94 { // 12% moderate
|
|
return int64(5+rand.Intn(45)) * 1024 * 1024 / 8 // 5-50 Mbps
|
|
} else { // 6% high activity
|
|
return int64(50+rand.Intn(200)) * 1024 * 1024 / 8 // 50-250 Mbps
|
|
}
|
|
|
|
// Container I/O (generally lower than VMs)
|
|
case "disk-read-ct":
|
|
if chance < 0.65 { // 65% are idle
|
|
return 0
|
|
} else if chance < 0.90 { // 25% have low activity
|
|
return int64(rand.Intn(3)) * 1024 * 1024 // 0-3 MB/s
|
|
} else if chance < 0.97 { // 7% moderate
|
|
return int64(3+rand.Intn(12)) * 1024 * 1024 // 3-15 MB/s
|
|
} else { // 3% high activity
|
|
return int64(15+rand.Intn(35)) * 1024 * 1024 // 15-50 MB/s
|
|
}
|
|
|
|
case "disk-write-ct":
|
|
if chance < 0.75 { // 75% are idle
|
|
return 0
|
|
} else if chance < 0.92 { // 17% have low activity
|
|
return int64(rand.Intn(2)) * 1024 * 1024 // 0-2 MB/s
|
|
} else if chance < 0.98 { // 6% moderate
|
|
return int64(2+rand.Intn(8)) * 1024 * 1024 // 2-10 MB/s
|
|
} else { // 2% high activity
|
|
return int64(10+rand.Intn(20)) * 1024 * 1024 // 10-30 MB/s
|
|
}
|
|
|
|
case "network-in-ct":
|
|
if chance < 0.55 { // 55% are idle
|
|
return 0
|
|
} else if chance < 0.85 { // 30% have low activity
|
|
return int64(rand.Intn(5)) * 1024 * 1024 / 8 // 0-5 Mbps
|
|
} else if chance < 0.96 { // 11% moderate
|
|
return int64(5+rand.Intn(25)) * 1024 * 1024 / 8 // 5-30 Mbps
|
|
} else { // 4% high activity
|
|
return int64(30+rand.Intn(70)) * 1024 * 1024 / 8 // 30-100 Mbps
|
|
}
|
|
|
|
case "network-out-ct":
|
|
if chance < 0.60 { // 60% are idle
|
|
return 0
|
|
} else if chance < 0.87 { // 27% have low activity
|
|
return int64(rand.Intn(3)) * 1024 * 1024 / 8 // 0-3 Mbps
|
|
} else if chance < 0.96 { // 9% moderate
|
|
return int64(3+rand.Intn(17)) * 1024 * 1024 / 8 // 3-20 Mbps
|
|
} else { // 4% high activity
|
|
return int64(20+rand.Intn(80)) * 1024 * 1024 / 8 // 20-100 Mbps
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func generateVM(nodeName string, instance string, vmid int, config MockConfig) models.VM {
|
|
name := generateGuestName("vm")
|
|
status := "running"
|
|
if rand.Float64() < config.StoppedPercent {
|
|
status = "stopped"
|
|
}
|
|
|
|
cpu := float64(0)
|
|
mem := models.Memory{}
|
|
uptime := int64(0)
|
|
|
|
if status == "running" {
|
|
// More realistic CPU usage: mostly low with occasional spikes
|
|
cpuRand := rand.Float64()
|
|
if cpuRand < 0.85 { // 85% of VMs have low CPU
|
|
cpu = rand.Float64() * 0.25 // 0-25%
|
|
} else if cpuRand < 0.97 { // 12% moderate CPU
|
|
cpu = 0.25 + rand.Float64()*0.35 // 25-60%
|
|
} else { // 3% high CPU (can trigger alerts at 80%)
|
|
cpu = 0.60 + rand.Float64()*0.25 // 60-85%
|
|
}
|
|
|
|
totalMem := int64((4 + rand.Intn(28)) * 1024 * 1024 * 1024) // 4-32 GB
|
|
// More realistic memory usage: most VMs use 30-65% memory
|
|
var memUsage float64
|
|
memRand := rand.Float64()
|
|
if memRand < 0.85 { // 85% typical usage
|
|
memUsage = 0.3 + rand.Float64()*0.35 // 30-65%
|
|
} else if memRand < 0.97 { // 12% moderate usage
|
|
memUsage = 0.65 + rand.Float64()*0.15 // 65-80%
|
|
} else { // 3% high memory (can trigger alerts at 85%)
|
|
memUsage = 0.80 + rand.Float64()*0.1 // 80-90%
|
|
}
|
|
usedMem := int64(float64(totalMem) * memUsage)
|
|
balloon := totalMem
|
|
if rand.Float64() < 0.7 {
|
|
balloon = int64(float64(totalMem) * (0.65 + rand.Float64()*0.25))
|
|
}
|
|
|
|
swapTotal := int64(0)
|
|
swapUsed := int64(0)
|
|
if rand.Float64() < 0.6 {
|
|
swapTotal = int64((1 + rand.Intn(5)) * 1024 * 1024 * 1024) // 1-5 GB
|
|
swapUsed = int64(float64(swapTotal) * (0.1 + rand.Float64()*0.4))
|
|
}
|
|
|
|
mem = models.Memory{
|
|
Total: totalMem,
|
|
Used: usedMem,
|
|
Free: totalMem - usedMem,
|
|
Usage: memUsage * 100,
|
|
Balloon: balloon,
|
|
SwapTotal: swapTotal,
|
|
SwapUsed: swapUsed,
|
|
}
|
|
uptime = int64(3600 * (1 + rand.Intn(720))) // 1-720 hours
|
|
}
|
|
|
|
// Disk stats
|
|
virtualDisks, aggregatedDisk := generateVirtualDisks()
|
|
diskStatusReason := ""
|
|
|
|
if status != "running" {
|
|
diskStatusReason = "vm-stopped"
|
|
aggregatedDisk.Usage = -1
|
|
aggregatedDisk.Used = 0
|
|
aggregatedDisk.Free = aggregatedDisk.Total
|
|
virtualDisks = nil
|
|
} else if rand.Float64() < 0.1 {
|
|
// Simulate agent issues where detailed disks are unavailable
|
|
diskStatusReason = "agent-not-running"
|
|
aggregatedDisk.Usage = -1
|
|
aggregatedDisk.Used = 0
|
|
aggregatedDisk.Free = aggregatedDisk.Total
|
|
virtualDisks = nil
|
|
}
|
|
|
|
// Generate ID matching production logic: standalone uses "node-vmid", cluster uses "instance-node-vmid"
|
|
var vmID string
|
|
if instance == nodeName {
|
|
vmID = fmt.Sprintf("%s-%d", nodeName, vmid)
|
|
} else {
|
|
vmID = fmt.Sprintf("%s-%s-%d", instance, nodeName, vmid)
|
|
}
|
|
|
|
osName, osVersion := generateGuestOSMetadata()
|
|
ipAddresses, networkIfaces := generateGuestNetworkInfo()
|
|
|
|
vm := models.VM{
|
|
Name: name,
|
|
VMID: vmid,
|
|
Node: nodeName,
|
|
Instance: instance,
|
|
Type: "qemu",
|
|
Status: status,
|
|
CPU: cpu,
|
|
CPUs: 2 + rand.Intn(6), // 2-8 cores
|
|
Memory: mem,
|
|
Disk: aggregatedDisk,
|
|
Disks: virtualDisks,
|
|
DiskStatusReason: diskStatusReason,
|
|
DiskRead: generateRealisticIO("disk-read"),
|
|
DiskWrite: generateRealisticIO("disk-write"),
|
|
NetworkIn: generateRealisticIO("network-in"),
|
|
NetworkOut: generateRealisticIO("network-out"),
|
|
Uptime: uptime,
|
|
ID: vmID,
|
|
Tags: generateTags(),
|
|
IPAddresses: ipAddresses,
|
|
OSName: osName,
|
|
OSVersion: osVersion,
|
|
NetworkInterfaces: networkIfaces,
|
|
}
|
|
|
|
if status != "running" {
|
|
vm.CPU = 0
|
|
vm.Memory.Usage = 0
|
|
vm.Memory.SwapUsed = 0
|
|
vm.Memory.Used = 0
|
|
vm.Memory.Free = vm.Memory.Total
|
|
vm.Disk.Used = 0
|
|
vm.Disk.Free = vm.Disk.Total
|
|
vm.Disk.Usage = -1
|
|
vm.NetworkIn = 0
|
|
vm.NetworkOut = 0
|
|
vm.DiskRead = 0
|
|
vm.DiskWrite = 0
|
|
vm.Uptime = 0
|
|
}
|
|
|
|
return vm
|
|
}
|
|
|
|
func generateGuestNetworkInfo() ([]string, []models.GuestNetworkInterface) {
|
|
ifaceCount := 1 + rand.Intn(2)
|
|
ipSet := make(map[string]struct{})
|
|
ipAddresses := make([]string, 0, ifaceCount*2)
|
|
interfaces := make([]models.GuestNetworkInterface, 0, ifaceCount)
|
|
|
|
for i := 0; i < ifaceCount; i++ {
|
|
name := fmt.Sprintf("eth%d", i)
|
|
if rand.Float64() < 0.3 {
|
|
name = fmt.Sprintf("ens%d", i+3)
|
|
}
|
|
|
|
mac := fmt.Sprintf("52:54:%02x:%02x:%02x:%02x",
|
|
rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256))
|
|
|
|
addrCount := 1 + rand.Intn(2)
|
|
addresses := make([]string, 0, addrCount)
|
|
|
|
for len(addresses) < addrCount {
|
|
var ip string
|
|
if rand.Float64() < 0.2 {
|
|
ip = fmt.Sprintf("fd00:%x:%x::%x", rand.Intn(1<<16), rand.Intn(1<<16), rand.Intn(1<<16))
|
|
} else {
|
|
ip = fmt.Sprintf("10.%d.%d.%d", rand.Intn(200)+1, rand.Intn(254), rand.Intn(254))
|
|
}
|
|
|
|
if _, exists := ipSet[ip]; exists {
|
|
continue
|
|
}
|
|
ipSet[ip] = struct{}{}
|
|
addresses = append(addresses, ip)
|
|
ipAddresses = append(ipAddresses, ip)
|
|
}
|
|
|
|
rxBytes := int64(rand.Int63n(8*1024*1024*1024) + rand.Int63n(256*1024*1024))
|
|
txBytes := int64(rand.Int63n(6*1024*1024*1024) + rand.Int63n(256*1024*1024))
|
|
|
|
interfaces = append(interfaces, models.GuestNetworkInterface{
|
|
Name: name,
|
|
MAC: mac,
|
|
Addresses: addresses,
|
|
RXBytes: rxBytes,
|
|
TXBytes: txBytes,
|
|
})
|
|
}
|
|
|
|
sort.Strings(ipAddresses)
|
|
|
|
return ipAddresses, interfaces
|
|
}
|
|
|
|
func generateGuestOSMetadata() (string, string) {
|
|
variants := []struct {
|
|
Name string
|
|
Version string
|
|
}{
|
|
{"Ubuntu", "22.04 LTS"},
|
|
{"Ubuntu", "24.04 LTS"},
|
|
{"Debian GNU/Linux", "12 (Bookworm)"},
|
|
{"Debian GNU/Linux", "11 (Bullseye)"},
|
|
{"CentOS Stream", "9"},
|
|
{"Rocky Linux", "9.3"},
|
|
{"AlmaLinux", "8.10"},
|
|
{"Fedora", fmt.Sprintf("%d", 38+rand.Intn(3))},
|
|
{"Windows Server", "2019"},
|
|
{"Windows Server", "2022"},
|
|
{"Arch Linux", "rolling"},
|
|
}
|
|
|
|
choice := variants[rand.Intn(len(variants))]
|
|
return choice.Name, choice.Version
|
|
}
|
|
|
|
func generateDockerHosts(config MockConfig) []models.DockerHost {
|
|
hostCount := config.DockerHostCount
|
|
if hostCount <= 0 {
|
|
return []models.DockerHost{}
|
|
}
|
|
|
|
now := time.Now()
|
|
hosts := make([]models.DockerHost, 0, hostCount)
|
|
|
|
for i := 0; i < hostCount; i++ {
|
|
agentVersion := dockerAgentVersions[rand.Intn(len(dockerAgentVersions))]
|
|
prefix := dockerHostPrefixes[i%len(dockerHostPrefixes)]
|
|
hostname := fmt.Sprintf("%s-%d", prefix, i+1)
|
|
hostID := fmt.Sprintf("%s-mock", hostname)
|
|
|
|
cpus := []int{4, 6, 8, 12, 16}[rand.Intn(5)]
|
|
totalMemoryBytes := int64((16 + rand.Intn(64)) * 1024 * 1024 * 1024) // 16-79 GB
|
|
interval := 30
|
|
uptime := int64(86400*(3+rand.Intn(25))) + int64(rand.Intn(3600))
|
|
|
|
containers := generateDockerContainers(hostname, i, config)
|
|
|
|
// Optionally ensure the second host looks degraded for UI coverage
|
|
if i == 1 && len(containers) > 0 && hostCount > 1 {
|
|
idx := rand.Intn(len(containers))
|
|
containers[idx].Health = "unhealthy"
|
|
containers[idx].CPUPercent = clampFloat(containers[idx].CPUPercent+35, 5, 190)
|
|
}
|
|
|
|
status := "online"
|
|
if hostCount > 2 && i == hostCount-1 {
|
|
status = "offline"
|
|
}
|
|
|
|
lastSeen := now.Add(-time.Duration(rand.Intn(20)) * time.Second)
|
|
if status == "offline" {
|
|
lastSeen = now.Add(-time.Duration(5+rand.Intn(20)) * time.Minute)
|
|
for idx := range containers {
|
|
exitCode := containers[idx].ExitCode
|
|
if exitCode == 0 {
|
|
exitCode = []int{0, 1, 137}[rand.Intn(3)]
|
|
}
|
|
containers[idx].State = "exited"
|
|
containers[idx].Health = ""
|
|
containers[idx].CPUPercent = 0
|
|
containers[idx].MemoryUsage = 0
|
|
containers[idx].MemoryPercent = 0
|
|
containers[idx].UptimeSeconds = 0
|
|
finished := now.Add(-time.Duration(rand.Intn(72)+1) * time.Hour)
|
|
containers[idx].StartedAt = nil
|
|
containers[idx].FinishedAt = &finished
|
|
containers[idx].Status = fmt.Sprintf("Exited (%d) %s ago", exitCode, formatDurationForStatus(now.Sub(finished)))
|
|
}
|
|
}
|
|
|
|
if status != "offline" {
|
|
running := 0
|
|
unhealthy := 0
|
|
for _, ct := range containers {
|
|
if strings.ToLower(ct.State) == "running" {
|
|
running++
|
|
healthState := strings.ToLower(ct.Health)
|
|
if healthState == "unhealthy" || healthState == "starting" {
|
|
unhealthy++
|
|
}
|
|
}
|
|
}
|
|
|
|
if running == 0 {
|
|
status = "offline"
|
|
lastSeen = now.Add(-time.Duration(3+rand.Intn(5)) * time.Minute)
|
|
} else if unhealthy > 0 || float64(len(containers)-running)/float64(len(containers)) > 0.35 {
|
|
status = "degraded"
|
|
if lastSeen.After(now.Add(-30 * time.Second)) {
|
|
lastSeen = now.Add(-35 * time.Second)
|
|
}
|
|
} else if status != "offline" {
|
|
status = "online"
|
|
}
|
|
}
|
|
|
|
host := models.DockerHost{
|
|
ID: hostID,
|
|
AgentID: fmt.Sprintf("agent-%s", randomHexString(6)),
|
|
Hostname: hostname,
|
|
DisplayName: humanizeHostDisplayName(hostname),
|
|
MachineID: randomHexString(32),
|
|
OS: dockerOperatingSystems[rand.Intn(len(dockerOperatingSystems))],
|
|
KernelVersion: dockerKernelVersions[rand.Intn(len(dockerKernelVersions))],
|
|
Architecture: dockerArchitectures[rand.Intn(len(dockerArchitectures))],
|
|
DockerVersion: dockerVersions[rand.Intn(len(dockerVersions))],
|
|
CPUs: cpus,
|
|
TotalMemoryBytes: totalMemoryBytes,
|
|
UptimeSeconds: uptime,
|
|
Status: status,
|
|
LastSeen: lastSeen,
|
|
IntervalSeconds: interval,
|
|
AgentVersion: agentVersion,
|
|
Containers: containers,
|
|
}
|
|
|
|
hosts = append(hosts, host)
|
|
}
|
|
|
|
return hosts
|
|
}
|
|
|
|
func generateDockerContainers(hostName string, hostIdx int, config MockConfig) []models.DockerContainer {
|
|
base := config.DockerContainersPerHost
|
|
if base < 1 {
|
|
base = 6
|
|
}
|
|
variation := rand.Intn(5) - 2
|
|
count := base + variation
|
|
if count < 3 {
|
|
count = 3
|
|
}
|
|
|
|
now := time.Now()
|
|
containers := make([]models.DockerContainer, 0, count)
|
|
nameUsage := make(map[string]int)
|
|
|
|
for i := 0; i < count; i++ {
|
|
baseName := appNames[rand.Intn(len(appNames))]
|
|
nameUsage[baseName]++
|
|
containerName := baseName
|
|
if nameUsage[baseName] > 1 {
|
|
containerName = fmt.Sprintf("%s-%d", baseName, nameUsage[baseName])
|
|
}
|
|
|
|
id := fmt.Sprintf("%s-%s", hostName, randomHexString(12))
|
|
running := rand.Float64() >= config.StoppedPercent
|
|
if running && rand.Float64() < 0.05 {
|
|
running = false // small chance of paused/exited container regardless of stopped percent
|
|
}
|
|
|
|
memLimit := int64((256 + rand.Intn(4096)) * 1024 * 1024) // 256MB - ~4.25GB
|
|
if memLimit < 256*1024*1024 {
|
|
memLimit = 256 * 1024 * 1024
|
|
}
|
|
memPercent := clampFloat(20+rand.Float64()*65, 2, 96)
|
|
if !running {
|
|
memPercent = 0
|
|
}
|
|
memUsage := int64(float64(memLimit) * (memPercent / 100.0))
|
|
|
|
cpuPercent := clampFloat(rand.Float64()*70, 0.2, 180)
|
|
if !running {
|
|
cpuPercent = 0
|
|
}
|
|
|
|
restartCount := rand.Intn(4)
|
|
exitCode := 0
|
|
health := "healthy"
|
|
|
|
if rand.Float64() < 0.08 {
|
|
health = "starting"
|
|
}
|
|
if rand.Float64() < 0.08 {
|
|
health = "unhealthy"
|
|
}
|
|
|
|
var startedAt *time.Time
|
|
var finishedAt *time.Time
|
|
uptime := int64(0)
|
|
state := "running"
|
|
statusText := ""
|
|
createdAt := now.Add(-time.Duration(rand.Intn(60*24)) * time.Hour)
|
|
|
|
if running {
|
|
uptime = int64(3600 + rand.Intn(86400*14)) // 1 hour to ~14 days
|
|
start := now.Add(-time.Duration(uptime) * time.Second)
|
|
startedAt = &start
|
|
statusText = fmt.Sprintf("Up %s", formatDurationForStatus(time.Duration(uptime)*time.Second))
|
|
} else {
|
|
stateOptions := []string{"exited", "paused"}
|
|
state = stateOptions[rand.Intn(len(stateOptions))]
|
|
if state == "exited" {
|
|
exitCode = []int{0, 1, 137, 139}[rand.Intn(4)]
|
|
restartCount = 1 + rand.Intn(4)
|
|
finished := now.Add(-time.Duration(rand.Intn(72)+1) * time.Hour)
|
|
finishedAt = &finished
|
|
statusText = fmt.Sprintf("Exited (%d) %s ago", exitCode, formatDurationForStatus(now.Sub(finished)))
|
|
health = ""
|
|
} else {
|
|
statusText = "Paused"
|
|
if rand.Float64() < 0.3 {
|
|
health = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
container := models.DockerContainer{
|
|
ID: id,
|
|
Name: containerName,
|
|
Image: fmt.Sprintf("ghcr.io/mock/%s:%d.%d.%d", containerName, 1+rand.Intn(2), rand.Intn(10), rand.Intn(10)),
|
|
State: state,
|
|
Status: statusText,
|
|
Health: health,
|
|
CPUPercent: cpuPercent,
|
|
MemoryUsage: memUsage,
|
|
MemoryLimit: memLimit,
|
|
MemoryPercent: memPercent,
|
|
UptimeSeconds: uptime,
|
|
RestartCount: restartCount,
|
|
ExitCode: exitCode,
|
|
CreatedAt: createdAt,
|
|
StartedAt: startedAt,
|
|
FinishedAt: finishedAt,
|
|
Ports: generateDockerPorts(),
|
|
Labels: generateDockerLabels(containerName, hostName),
|
|
Networks: generateDockerNetworks(hostIdx, i),
|
|
}
|
|
|
|
containers = append(containers, container)
|
|
}
|
|
|
|
return containers
|
|
}
|
|
|
|
func generateDockerPorts() []models.DockerContainerPort {
|
|
if rand.Float64() < 0.45 {
|
|
return nil
|
|
}
|
|
|
|
portChoices := []int{80, 443, 3000, 3306, 5432, 6379, 8080, 9000}
|
|
count := 1 + rand.Intn(2)
|
|
ports := make([]models.DockerContainerPort, 0, count)
|
|
used := make(map[int]bool)
|
|
|
|
for len(ports) < count && len(used) < len(portChoices) {
|
|
private := portChoices[rand.Intn(len(portChoices))]
|
|
if used[private] {
|
|
continue
|
|
}
|
|
used[private] = true
|
|
|
|
port := models.DockerContainerPort{
|
|
PrivatePort: private,
|
|
Protocol: []string{"tcp", "udp"}[rand.Intn(2)],
|
|
}
|
|
|
|
if rand.Float64() < 0.75 {
|
|
port.PublicPort = 20000 + rand.Intn(20000)
|
|
port.IP = "0.0.0.0"
|
|
}
|
|
|
|
ports = append(ports, port)
|
|
}
|
|
|
|
return ports
|
|
}
|
|
|
|
func generateDockerNetworks(hostIdx, containerIdx int) []models.DockerContainerNetworkLink {
|
|
networks := []models.DockerContainerNetworkLink{{
|
|
Name: "bridge",
|
|
IPv4: fmt.Sprintf("172.18.%d.%d", hostIdx+10, containerIdx+20),
|
|
}}
|
|
|
|
if rand.Float64() < 0.3 {
|
|
networks = append(networks, models.DockerContainerNetworkLink{
|
|
Name: "frontend",
|
|
IPv4: fmt.Sprintf("10.%d.%d.%d", hostIdx+20, containerIdx+10, rand.Intn(200)+20),
|
|
})
|
|
}
|
|
|
|
if rand.Float64() < 0.2 {
|
|
networks[len(networks)-1].IPv6 = fmt.Sprintf("fd00:%x:%x::%x", hostIdx+1, containerIdx+1, rand.Intn(4000))
|
|
}
|
|
|
|
return networks
|
|
}
|
|
|
|
func generateDockerLabels(serviceName, hostName string) map[string]string {
|
|
labels := map[string]string{
|
|
"com.docker.compose.project": hostName,
|
|
"com.docker.compose.service": serviceName,
|
|
}
|
|
|
|
if rand.Float64() < 0.25 {
|
|
labels["environment"] = []string{"production", "staging", "development"}[rand.Intn(3)]
|
|
}
|
|
|
|
if rand.Float64() < 0.2 {
|
|
labels["traefik.enable"] = "true"
|
|
}
|
|
|
|
return labels
|
|
}
|
|
|
|
func formatDurationForStatus(d time.Duration) string {
|
|
if d < 0 {
|
|
d = -d
|
|
}
|
|
|
|
if d < time.Minute {
|
|
return "less than a minute"
|
|
}
|
|
|
|
if d < time.Hour {
|
|
return fmt.Sprintf("%dm", int(d.Minutes()))
|
|
}
|
|
|
|
if d < 24*time.Hour {
|
|
hours := int(d / time.Hour)
|
|
minutes := int((d % time.Hour) / time.Minute)
|
|
if minutes == 0 {
|
|
return fmt.Sprintf("%dh", hours)
|
|
}
|
|
return fmt.Sprintf("%dh%dm", hours, minutes)
|
|
}
|
|
|
|
days := int(d / (24 * time.Hour))
|
|
hours := int((d % (24 * time.Hour)) / time.Hour)
|
|
if hours == 0 {
|
|
return fmt.Sprintf("%dd", days)
|
|
}
|
|
return fmt.Sprintf("%dd%dh", days, hours)
|
|
}
|
|
|
|
func clampFloat(value, min, max float64) float64 {
|
|
if value < min {
|
|
return min
|
|
}
|
|
if value > max {
|
|
return max
|
|
}
|
|
return value
|
|
}
|
|
|
|
func randomHexString(n int) string {
|
|
const hexChars = "0123456789abcdef"
|
|
if n <= 0 {
|
|
return ""
|
|
}
|
|
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = hexChars[rand.Intn(len(hexChars))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func humanizeHostDisplayName(hostname string) string {
|
|
parts := strings.Split(hostname, "-")
|
|
for i, part := range parts {
|
|
if part == "" {
|
|
continue
|
|
}
|
|
runes := []rune(part)
|
|
runes[0] = unicode.ToUpper(runes[0])
|
|
parts[i] = string(runes)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func generateContainer(nodeName string, instance string, vmid int, config MockConfig) models.Container {
|
|
name := generateGuestName("lxc")
|
|
status := "running"
|
|
if rand.Float64() < config.StoppedPercent {
|
|
status = "stopped"
|
|
}
|
|
|
|
cpu := float64(0)
|
|
mem := models.Memory{}
|
|
uptime := int64(0)
|
|
|
|
if status == "running" {
|
|
// More realistic CPU for containers: mostly very low
|
|
cpuRand := rand.Float64()
|
|
if cpuRand < 0.90 { // 90% of containers have minimal CPU
|
|
cpu = rand.Float64() * 0.12 // 0-12%
|
|
} else if cpuRand < 0.98 { // 8% moderate CPU
|
|
cpu = 0.12 + rand.Float64()*0.28 // 12-40%
|
|
} else { // 2% higher CPU (can trigger alerts at 80%)
|
|
cpu = 0.40 + rand.Float64()*0.35 // 40-75%
|
|
}
|
|
|
|
totalMem := int64((512 + rand.Intn(7680)) * 1024 * 1024) // 512 MB - 8 GB
|
|
// More realistic memory for containers
|
|
var memUsage float64
|
|
memRand := rand.Float64()
|
|
if memRand < 0.90 { // 90% typical usage
|
|
memUsage = 0.35 + rand.Float64()*0.35 // 35-70%
|
|
} else if memRand < 0.98 { // 8% moderate usage
|
|
memUsage = 0.70 + rand.Float64()*0.12 // 70-82%
|
|
} else { // 2% high memory (can trigger alerts at 85%)
|
|
memUsage = 0.82 + rand.Float64()*0.08 // 82-90%
|
|
}
|
|
usedMem := int64(float64(totalMem) * memUsage)
|
|
balloon := totalMem
|
|
if rand.Float64() < 0.5 {
|
|
balloon = int64(float64(totalMem) * (0.7 + rand.Float64()*0.2))
|
|
}
|
|
|
|
swapTotal := int64(0)
|
|
swapUsed := int64(0)
|
|
if rand.Float64() < 0.4 {
|
|
swapTotal = int64((256 + rand.Intn(1024)) * 1024 * 1024) // 256MB - 1.25GB
|
|
swapUsed = int64(float64(swapTotal) * (0.1 + rand.Float64()*0.4))
|
|
}
|
|
|
|
mem = models.Memory{
|
|
Total: totalMem,
|
|
Used: usedMem,
|
|
Free: totalMem - usedMem,
|
|
Usage: memUsage * 100,
|
|
Balloon: balloon,
|
|
SwapTotal: swapTotal,
|
|
SwapUsed: swapUsed,
|
|
}
|
|
uptime = int64(3600 * (1 + rand.Intn(1440))) // 1-1440 hours (up to 60 days)
|
|
}
|
|
|
|
// Disk stats - containers typically smaller
|
|
totalDisk := int64((8 + rand.Intn(120)) * 1024 * 1024 * 1024) // 8-128 GB
|
|
usedDisk := int64(float64(totalDisk) * (0.1 + rand.Float64()*0.6))
|
|
|
|
// Generate ID matching production logic: standalone uses "node-vmid", cluster uses "instance-node-vmid"
|
|
var ctID string
|
|
if instance == nodeName {
|
|
ctID = fmt.Sprintf("%s-%d", nodeName, vmid)
|
|
} else {
|
|
ctID = fmt.Sprintf("%s-%s-%d", instance, nodeName, vmid)
|
|
}
|
|
|
|
ct := models.Container{
|
|
Name: name,
|
|
VMID: vmid,
|
|
Node: nodeName,
|
|
Instance: instance,
|
|
Type: "lxc",
|
|
Status: status,
|
|
CPU: cpu,
|
|
CPUs: 1 + rand.Intn(4), // 1-4 cores
|
|
Memory: mem,
|
|
Disk: models.Disk{
|
|
Total: totalDisk,
|
|
Used: usedDisk,
|
|
Free: totalDisk - usedDisk,
|
|
Usage: float64(usedDisk) / float64(totalDisk) * 100,
|
|
},
|
|
DiskRead: generateRealisticIO("disk-read-ct"),
|
|
DiskWrite: generateRealisticIO("disk-write-ct"),
|
|
NetworkIn: generateRealisticIO("network-in-ct"),
|
|
NetworkOut: generateRealisticIO("network-out-ct"),
|
|
Uptime: uptime,
|
|
ID: ctID,
|
|
Tags: generateTags(),
|
|
}
|
|
|
|
if status != "running" {
|
|
ct.CPU = 0
|
|
ct.Memory.Usage = 0
|
|
ct.Memory.SwapUsed = 0
|
|
ct.Memory.Used = 0
|
|
ct.Memory.Free = ct.Memory.Total
|
|
ct.Disk.Used = 0
|
|
ct.Disk.Free = ct.Disk.Total
|
|
ct.Disk.Usage = -1
|
|
ct.NetworkIn = 0
|
|
ct.NetworkOut = 0
|
|
ct.DiskRead = 0
|
|
ct.DiskWrite = 0
|
|
ct.Uptime = 0
|
|
}
|
|
|
|
return ct
|
|
}
|
|
|
|
func generateGuestName(prefix string) string {
|
|
return fmt.Sprintf("%s-%s-%d", prefix, appNames[rand.Intn(len(appNames))], rand.Intn(100))
|
|
}
|
|
|
|
// generateTags generates random tags for a guest
|
|
func generateTags() []string {
|
|
// 30% chance of no tags
|
|
if rand.Float64() < 0.3 {
|
|
return []string{}
|
|
}
|
|
|
|
// Generate 1-4 tags
|
|
numTags := 1 + rand.Intn(4)
|
|
tags := make([]string, 0, numTags)
|
|
usedTags := make(map[string]bool)
|
|
|
|
for len(tags) < numTags {
|
|
tag := commonTags[rand.Intn(len(commonTags))]
|
|
// Avoid duplicate tags
|
|
if !usedTags[tag] {
|
|
tags = append(tags, tag)
|
|
usedTags[tag] = true
|
|
}
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
// GenerateAlerts generates random alerts for testing
|
|
func GenerateAlerts(nodes []models.Node, vms []models.VM, containers []models.Container) []models.Alert {
|
|
alerts := []models.Alert{}
|
|
|
|
// Generate some node alerts
|
|
for _, node := range nodes {
|
|
// Add offline alert for offline nodes
|
|
if node.Status == "offline" {
|
|
alerts = append(alerts, models.Alert{
|
|
ID: fmt.Sprintf("node-offline-%s", node.ID),
|
|
Type: "connectivity",
|
|
Level: "critical",
|
|
ResourceID: node.ID,
|
|
ResourceName: node.Name,
|
|
Node: node.Name,
|
|
Message: fmt.Sprintf("Node '%s' is offline", node.Name),
|
|
Value: 0,
|
|
Threshold: 0,
|
|
StartTime: time.Now().Add(-time.Minute * 5), // Offline for 5 minutes
|
|
})
|
|
continue // Skip other checks for offline nodes
|
|
}
|
|
|
|
if node.CPU > 0.8 {
|
|
alerts = append(alerts, models.Alert{
|
|
ID: fmt.Sprintf("alert-%s-cpu", node.Name),
|
|
Type: "threshold",
|
|
Level: "warning",
|
|
ResourceID: node.ID,
|
|
ResourceName: node.Name,
|
|
Node: node.Name,
|
|
Message: fmt.Sprintf("Node %s CPU usage is %.0f%%", node.Name, node.CPU*100),
|
|
Value: node.CPU * 100,
|
|
Threshold: 80,
|
|
StartTime: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Generate some VM/container alerts
|
|
allGuests := make([]interface{}, 0, len(vms)+len(containers))
|
|
for _, vm := range vms {
|
|
allGuests = append(allGuests, vm)
|
|
}
|
|
for _, ct := range containers {
|
|
allGuests = append(allGuests, ct)
|
|
}
|
|
|
|
// Pick random guests to have alerts
|
|
numAlerts := rand.Intn(5) + 1
|
|
for i := 0; i < numAlerts && i < len(allGuests); i++ {
|
|
guestIdx := rand.Intn(len(allGuests))
|
|
|
|
var name, id string
|
|
var cpu float64
|
|
var memUsage float64
|
|
|
|
switch g := allGuests[guestIdx].(type) {
|
|
case models.VM:
|
|
name = g.Name
|
|
id = g.ID
|
|
cpu = g.CPU
|
|
memUsage = g.Memory.Usage
|
|
case models.Container:
|
|
name = g.Name
|
|
id = g.ID
|
|
cpu = g.CPU
|
|
memUsage = g.Memory.Usage
|
|
}
|
|
|
|
// Randomly choose alert type
|
|
switch rand.Intn(3) {
|
|
case 0: // CPU alert
|
|
if cpu > 0.7 {
|
|
alerts = append(alerts, models.Alert{
|
|
ID: fmt.Sprintf("alert-%s-cpu", id),
|
|
Type: "threshold",
|
|
Level: "warning",
|
|
ResourceID: id,
|
|
ResourceName: name,
|
|
Message: fmt.Sprintf("%s CPU usage is %.0f%%", name, cpu*100),
|
|
Value: cpu * 100,
|
|
Threshold: 70,
|
|
StartTime: time.Now(),
|
|
})
|
|
}
|
|
case 1: // Memory alert
|
|
if memUsage > 80 {
|
|
alerts = append(alerts, models.Alert{
|
|
ID: fmt.Sprintf("alert-%s-mem", id),
|
|
Type: "threshold",
|
|
Level: "warning",
|
|
ResourceID: id,
|
|
ResourceName: name,
|
|
Message: fmt.Sprintf("%s memory usage is %.0f%%", name, memUsage),
|
|
Value: memUsage,
|
|
Threshold: 80,
|
|
StartTime: time.Now(),
|
|
})
|
|
}
|
|
case 2: // Disk alert
|
|
diskUsage := 70 + rand.Float64()*25
|
|
alerts = append(alerts, models.Alert{
|
|
ID: fmt.Sprintf("alert-%s-disk", id),
|
|
Type: "threshold",
|
|
Level: "critical",
|
|
ResourceID: id,
|
|
ResourceName: name,
|
|
Message: fmt.Sprintf("%s disk usage is %.0f%%", name, diskUsage),
|
|
Value: diskUsage,
|
|
Threshold: 90,
|
|
StartTime: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return alerts
|
|
}
|
|
|
|
// generateZFSPoolWithIssues creates a ZFS pool with various issues for testing
|
|
func generateZFSPoolWithIssues(poolName string) *models.ZFSPool {
|
|
scenarios := []func() *models.ZFSPool{
|
|
// Degraded pool with device errors
|
|
func() *models.ZFSPool {
|
|
return &models.ZFSPool{
|
|
Name: poolName,
|
|
State: "DEGRADED",
|
|
Status: "Degraded",
|
|
Scan: "resilver in progress",
|
|
ReadErrors: 12,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 3,
|
|
Devices: []models.ZFSDevice{
|
|
{
|
|
Name: "sda2",
|
|
Type: "disk",
|
|
State: "ONLINE",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
},
|
|
{
|
|
Name: "sdb2",
|
|
Type: "disk",
|
|
State: "DEGRADED",
|
|
ReadErrors: 12,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 3,
|
|
},
|
|
},
|
|
}
|
|
},
|
|
// Pool with checksum errors
|
|
func() *models.ZFSPool {
|
|
return &models.ZFSPool{
|
|
Name: poolName,
|
|
State: "ONLINE",
|
|
Status: "Healthy",
|
|
Scan: "scrub in progress",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 7,
|
|
Devices: []models.ZFSDevice{
|
|
{
|
|
Name: "sda2",
|
|
Type: "disk",
|
|
State: "ONLINE",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 7,
|
|
},
|
|
},
|
|
}
|
|
},
|
|
// Faulted device
|
|
func() *models.ZFSPool {
|
|
return &models.ZFSPool{
|
|
Name: poolName,
|
|
State: "DEGRADED",
|
|
Status: "Degraded",
|
|
Scan: "none",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
Devices: []models.ZFSDevice{
|
|
{
|
|
Name: "mirror-0",
|
|
Type: "mirror",
|
|
State: "DEGRADED",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
},
|
|
{
|
|
Name: "sda2",
|
|
Type: "disk",
|
|
State: "ONLINE",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
},
|
|
{
|
|
Name: "sdb2",
|
|
Type: "disk",
|
|
State: "FAULTED",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
// Pick a random scenario
|
|
return scenarios[rand.Intn(len(scenarios))]()
|
|
}
|
|
|
|
// generateStorage generates mock storage data for nodes
|
|
func generateStorage(nodes []models.Node) []models.Storage {
|
|
var storage []models.Storage
|
|
storageTypes := []string{"dir", "zfspool", "lvm", "nfs", "cephfs"}
|
|
contentTypes := []string{"images", "vztmpl,iso", "rootdir", "backup", "snippets"}
|
|
|
|
for _, node := range nodes {
|
|
isOffline := node.Status != "online" || node.ConnectionHealth == "offline" || node.Uptime <= 0
|
|
// Local storage (always present)
|
|
localTotal := int64(500 * 1024 * 1024 * 1024) // 500GB
|
|
localUsed := int64(float64(localTotal) * (0.3 + rand.Float64()*0.5))
|
|
if isOffline {
|
|
localUsed = 0
|
|
}
|
|
localFree := localTotal - localUsed
|
|
localUsage := 0.0
|
|
if localTotal > 0 {
|
|
localUsage = float64(localUsed) / float64(localTotal) * 100
|
|
}
|
|
localStatus := "available"
|
|
localEnabled := true
|
|
localActive := true
|
|
if isOffline {
|
|
localStatus = "offline"
|
|
localEnabled = false
|
|
localActive = false
|
|
}
|
|
|
|
storage = append(storage, models.Storage{
|
|
ID: fmt.Sprintf("%s-%s-local", node.Instance, node.Name),
|
|
Name: "local",
|
|
Node: node.Name,
|
|
Instance: node.Instance,
|
|
Type: "dir",
|
|
Status: localStatus,
|
|
Total: localTotal,
|
|
Used: localUsed,
|
|
Free: localFree,
|
|
Usage: localUsage,
|
|
Content: "vztmpl,iso",
|
|
Shared: false,
|
|
Enabled: localEnabled,
|
|
Active: localActive,
|
|
})
|
|
|
|
// Local-zfs (common)
|
|
zfsTotal := int64(2 * 1024 * 1024 * 1024 * 1024) // 2TB
|
|
zfsUsed := int64(float64(zfsTotal) * (0.2 + rand.Float64()*0.6))
|
|
if isOffline {
|
|
zfsUsed = 0
|
|
}
|
|
zfsFree := zfsTotal - zfsUsed
|
|
zfsUsage := 0.0
|
|
if zfsTotal > 0 {
|
|
zfsUsage = float64(zfsUsed) / float64(zfsTotal) * 100
|
|
}
|
|
|
|
// Generate ZFS pool status
|
|
var zfsPool *models.ZFSPool
|
|
if rand.Float64() < 0.15 { // 15% chance of ZFS pool with issues
|
|
zfsPool = generateZFSPoolWithIssues("local-zfs")
|
|
} else if rand.Float64() < 0.95 { // Most ZFS pools are healthy
|
|
zfsPool = &models.ZFSPool{
|
|
Name: "rpool/data",
|
|
State: "ONLINE",
|
|
Status: "Healthy",
|
|
Scan: "none",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
Devices: []models.ZFSDevice{
|
|
{
|
|
Name: "sda2",
|
|
Type: "disk",
|
|
State: "ONLINE",
|
|
ReadErrors: 0,
|
|
WriteErrors: 0,
|
|
ChecksumErrors: 0,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
zfsStatus := "available"
|
|
zfsEnabled := true
|
|
zfsActive := true
|
|
if isOffline {
|
|
zfsStatus = "offline"
|
|
zfsEnabled = false
|
|
zfsActive = false
|
|
}
|
|
|
|
storage = append(storage, models.Storage{
|
|
ID: fmt.Sprintf("%s-%s-local-zfs", node.Instance, node.Name),
|
|
Name: "local-zfs",
|
|
Node: node.Name,
|
|
Instance: node.Instance,
|
|
Type: "zfspool",
|
|
Status: zfsStatus,
|
|
Total: zfsTotal,
|
|
Used: zfsUsed,
|
|
Free: zfsFree,
|
|
Usage: zfsUsage,
|
|
Content: "images,rootdir",
|
|
Shared: false,
|
|
Enabled: zfsEnabled,
|
|
Active: zfsActive,
|
|
ZFSPool: zfsPool,
|
|
})
|
|
|
|
// Add one more random storage per node
|
|
if rand.Float64() > 0.3 {
|
|
storageType := storageTypes[rand.Intn(len(storageTypes))]
|
|
storageName := fmt.Sprintf("storage-%s-%d", node.Name, rand.Intn(100))
|
|
total := int64((1 + rand.Intn(10)) * 1024 * 1024 * 1024 * 1024) // 1-10TB
|
|
used := int64(float64(total) * rand.Float64())
|
|
if isOffline {
|
|
used = 0
|
|
}
|
|
free := total - used
|
|
usage := 0.0
|
|
if total > 0 {
|
|
usage = float64(used) / float64(total) * 100
|
|
}
|
|
|
|
status := "available"
|
|
enabled := true
|
|
active := true
|
|
if isOffline {
|
|
status = "offline"
|
|
enabled = false
|
|
active = false
|
|
}
|
|
|
|
storage = append(storage, models.Storage{
|
|
ID: fmt.Sprintf("%s-%s-%s", node.Instance, node.Name, storageName),
|
|
Name: storageName,
|
|
Node: node.Name,
|
|
Instance: node.Instance,
|
|
Type: storageType,
|
|
Status: status,
|
|
Total: total,
|
|
Used: used,
|
|
Free: free,
|
|
Usage: usage,
|
|
Content: contentTypes[rand.Intn(len(contentTypes))],
|
|
Shared: storageType == "nfs" || storageType == "cephfs",
|
|
Enabled: enabled,
|
|
Active: active,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Add PBS storage for each node (simulating node-specific PBS namespaces)
|
|
// In real clusters, each node reports ALL PBS storage entries but with node-specific namespaces
|
|
// This matches real behavior where each node sees all PBS configurations
|
|
clusterNodes := []models.Node{}
|
|
for _, n := range nodes {
|
|
if n.Instance == "mock-cluster" {
|
|
clusterNodes = append(clusterNodes, n)
|
|
}
|
|
}
|
|
|
|
// Each cluster node reports ALL PBS storage entries (one for each node)
|
|
for _, node := range clusterNodes {
|
|
// Each node sees ALL PBS storage configurations
|
|
for _, pbsTargetNode := range clusterNodes {
|
|
pbsTotal := int64(950 * 1024 * 1024 * 1024) // ~950GB matching real PBS
|
|
pbsUsed := int64(float64(pbsTotal) * 0.14) // ~14% usage matching real data
|
|
storage = append(storage, models.Storage{
|
|
ID: fmt.Sprintf("%s-%s-pbs-%s", node.Instance, node.Name, pbsTargetNode.Name),
|
|
Name: fmt.Sprintf("pbs-%s", pbsTargetNode.Name),
|
|
Node: node.Name, // The node that reports this storage
|
|
Instance: node.Instance,
|
|
Type: "pbs",
|
|
Status: "available",
|
|
Total: pbsTotal,
|
|
Used: pbsUsed,
|
|
Free: pbsTotal - pbsUsed,
|
|
Usage: float64(pbsUsed) / float64(pbsTotal) * 100,
|
|
Content: "backup",
|
|
Shared: true, // PBS storage is shared cluster-wide (all nodes can access it)
|
|
Enabled: true,
|
|
Active: true,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(clusterNodes) > 0 {
|
|
nodeNames := make([]string, 0, len(clusterNodes))
|
|
nodeIDs := make([]string, 0, len(clusterNodes))
|
|
for _, clusterNode := range clusterNodes {
|
|
nodeNames = append(nodeNames, clusterNode.Name)
|
|
nodeIDs = append(nodeIDs, fmt.Sprintf("%s-%s", clusterNode.Instance, clusterNode.Name))
|
|
}
|
|
|
|
cephTotal := int64(120 * 1024 * 1024 * 1024 * 1024) // 120 TiB shared CephFS
|
|
cephUsed := int64(float64(cephTotal) * (0.52 + rand.Float64()*0.18))
|
|
storage = append(storage, models.Storage{
|
|
ID: fmt.Sprintf("%s-shared-cephfs", clusterNodes[0].Instance),
|
|
Name: "cephfs-shared",
|
|
Node: "shared",
|
|
Instance: clusterNodes[0].Instance,
|
|
Type: "cephfs",
|
|
Status: "available",
|
|
Total: cephTotal,
|
|
Used: cephUsed,
|
|
Free: cephTotal - cephUsed,
|
|
Usage: float64(cephUsed) / float64(cephTotal) * 100,
|
|
Content: "images,rootdir,backup",
|
|
Shared: true,
|
|
Enabled: true,
|
|
Active: true,
|
|
Nodes: nodeNames,
|
|
NodeIDs: nodeIDs,
|
|
NodeCount: len(nodeNames),
|
|
})
|
|
|
|
rbdTotal := int64(80 * 1024 * 1024 * 1024 * 1024) // 80 TiB shared RBD pool
|
|
rbdUsed := int64(float64(rbdTotal) * (0.48 + rand.Float64()*0.22))
|
|
storage = append(storage, models.Storage{
|
|
ID: fmt.Sprintf("%s-shared-rbd", clusterNodes[0].Instance),
|
|
Name: "ceph-rbd-pool",
|
|
Node: "shared",
|
|
Instance: clusterNodes[0].Instance,
|
|
Type: "rbd",
|
|
Status: "available",
|
|
Total: rbdTotal,
|
|
Used: rbdUsed,
|
|
Free: rbdTotal - rbdUsed,
|
|
Usage: float64(rbdUsed) / float64(rbdTotal) * 100,
|
|
Content: "images,rootdir",
|
|
Shared: true,
|
|
Enabled: true,
|
|
Active: true,
|
|
Nodes: nodeNames,
|
|
NodeIDs: nodeIDs,
|
|
NodeCount: len(nodeNames),
|
|
})
|
|
}
|
|
|
|
// Add a shared storage (NFS or CephFS)
|
|
if len(nodes) > 1 {
|
|
sharedTotal := int64(10 * 1024 * 1024 * 1024 * 1024) // 10TB
|
|
sharedUsed := int64(float64(sharedTotal) * (0.4 + rand.Float64()*0.3))
|
|
storage = append(storage, models.Storage{
|
|
ID: "shared-storage",
|
|
Name: "shared-storage",
|
|
Node: "shared", // Shared storage uses "shared" as node per production code
|
|
Instance: nodes[0].Instance,
|
|
Type: "nfs",
|
|
Status: "available",
|
|
Total: sharedTotal,
|
|
Used: sharedUsed,
|
|
Free: sharedTotal - sharedUsed,
|
|
Usage: float64(sharedUsed) / float64(sharedTotal) * 100,
|
|
Content: "images,rootdir,backup",
|
|
Shared: true,
|
|
Enabled: true,
|
|
Active: true,
|
|
})
|
|
}
|
|
|
|
return storage
|
|
}
|
|
|
|
func generateCephClusters(nodes []models.Node, storage []models.Storage) []models.CephCluster {
|
|
cephStorageByInstance := make(map[string][]models.Storage)
|
|
for _, st := range storage {
|
|
typeLower := strings.ToLower(strings.TrimSpace(st.Type))
|
|
if typeLower == "cephfs" || typeLower == "rbd" || typeLower == "ceph" {
|
|
cephStorageByInstance[st.Instance] = append(cephStorageByInstance[st.Instance], st)
|
|
}
|
|
}
|
|
|
|
if len(cephStorageByInstance) == 0 {
|
|
return nil
|
|
}
|
|
|
|
nodesByInstance := make(map[string][]models.Node)
|
|
for _, node := range nodes {
|
|
nodesByInstance[node.Instance] = append(nodesByInstance[node.Instance], node)
|
|
}
|
|
|
|
var clusters []models.CephCluster
|
|
for instanceName, cephStorages := range cephStorageByInstance {
|
|
instanceNodes := nodesByInstance[instanceName]
|
|
uniqueNodeNames := make(map[string]struct{})
|
|
for _, node := range instanceNodes {
|
|
if node.Name != "" {
|
|
uniqueNodeNames[node.Name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
var totalBytes int64
|
|
var usedBytes int64
|
|
for _, st := range cephStorages {
|
|
totalBytes += st.Total
|
|
usedBytes += st.Used
|
|
}
|
|
if totalBytes == 0 {
|
|
totalBytes = int64(120 * 1024 * 1024 * 1024 * 1024)
|
|
}
|
|
if usedBytes == 0 {
|
|
usedBytes = int64(float64(totalBytes) * 0.52)
|
|
}
|
|
availableBytes := totalBytes - usedBytes
|
|
usagePercent := float64(usedBytes) / float64(totalBytes) * 100
|
|
|
|
health := "HEALTH_OK"
|
|
healthMessage := ""
|
|
if rand.Float64() < 0.2 {
|
|
health = "HEALTH_WARN"
|
|
healthMessage = "1 PG degraded"
|
|
}
|
|
|
|
pools := make([]models.CephPool, 0, len(cephStorages))
|
|
for idx, st := range cephStorages {
|
|
percent := 0.0
|
|
if st.Total > 0 {
|
|
percent = float64(st.Used) / float64(st.Total) * 100
|
|
}
|
|
poolName := st.Name
|
|
if poolName == "" {
|
|
if idx == 0 {
|
|
poolName = "rbd"
|
|
} else if idx == 1 {
|
|
poolName = "cephfs-data"
|
|
} else {
|
|
poolName = fmt.Sprintf("pool-%d", idx+1)
|
|
}
|
|
}
|
|
pools = append(pools, models.CephPool{
|
|
ID: idx + 1,
|
|
Name: poolName,
|
|
StoredBytes: st.Used,
|
|
AvailableBytes: st.Free,
|
|
Objects: int64(1_200_000 + rand.Intn(800_000)),
|
|
PercentUsed: percent,
|
|
})
|
|
}
|
|
|
|
numMons := 1
|
|
if len(uniqueNodeNames) >= 3 {
|
|
numMons = 3
|
|
} else if len(uniqueNodeNames) == 2 {
|
|
numMons = 2
|
|
}
|
|
|
|
numMgrs := 1
|
|
if len(uniqueNodeNames) > 1 {
|
|
numMgrs = 2
|
|
}
|
|
|
|
numOSDs := maxInt(len(uniqueNodeNames)*3, 6)
|
|
numOSDsUp := numOSDs
|
|
if health != "HEALTH_OK" && numOSDsUp > 0 {
|
|
numOSDsUp--
|
|
}
|
|
|
|
services := []models.CephServiceStatus{
|
|
{Type: "mon", Running: numMons, Total: numMons},
|
|
{Type: "mgr", Running: numMgrs, Total: numMgrs},
|
|
}
|
|
|
|
if len(uniqueNodeNames) > 1 {
|
|
mdsRunning := 2
|
|
mdsTotal := 2
|
|
if rand.Float64() < 0.25 {
|
|
mdsRunning = 1
|
|
}
|
|
mds := models.CephServiceStatus{Type: "mds", Running: mdsRunning, Total: mdsTotal}
|
|
if mdsRunning < mdsTotal {
|
|
mds.Message = "1 standby"
|
|
}
|
|
services = append(services, mds)
|
|
}
|
|
|
|
cluster := models.CephCluster{
|
|
ID: fmt.Sprintf("%s-ceph", instanceName),
|
|
Instance: instanceName,
|
|
Name: fmt.Sprintf("%s Ceph", strings.Title(strings.ReplaceAll(instanceName, "-", " "))),
|
|
FSID: fmt.Sprintf("00000000-0000-4000-8000-%012d", rand.Int63n(1_000_000_000_000)),
|
|
Health: health,
|
|
HealthMessage: healthMessage,
|
|
TotalBytes: totalBytes,
|
|
UsedBytes: usedBytes,
|
|
AvailableBytes: availableBytes,
|
|
UsagePercent: usagePercent,
|
|
NumMons: numMons,
|
|
NumMgrs: numMgrs,
|
|
NumOSDs: numOSDs,
|
|
NumOSDsUp: numOSDsUp,
|
|
NumOSDsIn: numOSDs,
|
|
NumPGs: 512 + rand.Intn(256),
|
|
Pools: pools,
|
|
Services: services,
|
|
LastUpdated: time.Now().Add(-time.Duration(rand.Intn(180)) * time.Second),
|
|
}
|
|
|
|
clusters = append(clusters, cluster)
|
|
}
|
|
|
|
return clusters
|
|
}
|
|
|
|
func maxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// generateBackups generates mock backup data for VMs and containers
|
|
func generateBackups(vms []models.VM, containers []models.Container) []models.StorageBackup {
|
|
var backups []models.StorageBackup
|
|
backupFormats := []string{"vma.zst", "vma.lzo", "tar.zst", "tar.gz"}
|
|
|
|
// Generate backups for ~60% of VMs
|
|
for _, vm := range vms {
|
|
if rand.Float64() > 0.4 {
|
|
continue
|
|
}
|
|
|
|
// Generate 1-3 backups per VM
|
|
numBackups := 1 + rand.Intn(3)
|
|
for i := 0; i < numBackups; i++ {
|
|
backupTime := time.Now().Add(-time.Duration(rand.Intn(30*24)) * time.Hour)
|
|
backupSize := int64(vm.Disk.Total/10 + rand.Int63n(vm.Disk.Total/5)) // 10-30% of disk size
|
|
|
|
backup := models.StorageBackup{
|
|
ID: fmt.Sprintf("backup-%s-vm-%d-%d", vm.Node, vm.VMID, i),
|
|
Storage: "local",
|
|
Node: vm.Node,
|
|
Instance: vm.Instance,
|
|
Type: "qemu",
|
|
VMID: vm.VMID,
|
|
Time: backupTime,
|
|
CTime: backupTime.Unix(),
|
|
Size: backupSize,
|
|
Format: backupFormats[rand.Intn(len(backupFormats))],
|
|
Notes: fmt.Sprintf("Backup of %s", vm.Name),
|
|
Protected: rand.Float64() > 0.8, // 20% protected
|
|
Volid: fmt.Sprintf("local:backup/vzdump-qemu-%d-%s.%s", vm.VMID, backupTime.Format("2006_01_02-15_04_05"), backupFormats[0]),
|
|
IsPBS: false,
|
|
Verified: rand.Float64() > 0.3, // 70% verified
|
|
}
|
|
|
|
if backup.Verified {
|
|
backup.Verification = "OK"
|
|
}
|
|
|
|
backups = append(backups, backup)
|
|
}
|
|
}
|
|
|
|
// Generate backups for ~70% of containers
|
|
for _, ct := range containers {
|
|
if rand.Float64() > 0.3 {
|
|
continue
|
|
}
|
|
|
|
// Generate 1-2 backups per container
|
|
numBackups := 1 + rand.Intn(2)
|
|
for i := 0; i < numBackups; i++ {
|
|
backupTime := time.Now().Add(-time.Duration(rand.Intn(30*24)) * time.Hour)
|
|
backupSize := int64(ct.Disk.Total/20 + rand.Int63n(ct.Disk.Total/10)) // 5-15% of disk size
|
|
|
|
backup := models.StorageBackup{
|
|
ID: fmt.Sprintf("backup-%s-ct-%d-%d", ct.Node, int(ct.VMID), i),
|
|
Storage: "local",
|
|
Node: ct.Node,
|
|
Instance: ct.Instance,
|
|
Type: "lxc",
|
|
VMID: int(ct.VMID),
|
|
Time: backupTime,
|
|
CTime: backupTime.Unix(),
|
|
Size: backupSize,
|
|
Format: "tar.zst",
|
|
Notes: fmt.Sprintf("Backup of %s", ct.Name),
|
|
Protected: rand.Float64() > 0.9, // 10% protected
|
|
Volid: fmt.Sprintf("local:backup/vzdump-lxc-%d-%s.tar.zst", int(ct.VMID), backupTime.Format("2006_01_02-15_04_05")),
|
|
IsPBS: false,
|
|
Verified: rand.Float64() > 0.2, // 80% verified
|
|
}
|
|
|
|
if backup.Verified {
|
|
backup.Verification = "OK"
|
|
}
|
|
|
|
backups = append(backups, backup)
|
|
}
|
|
}
|
|
|
|
// Generate PMG host config backups (VMID=0)
|
|
// Add 2-4 PMG host backups
|
|
numPMGBackups := 2 + rand.Intn(3)
|
|
pmgNodes := []string{"pmg-01", "pmg-02", "mail-gateway"}
|
|
for i := 0; i < numPMGBackups; i++ {
|
|
backupTime := time.Now().Add(-time.Duration(rand.Intn(60*24)) * time.Hour)
|
|
nodeIdx := rand.Intn(len(pmgNodes))
|
|
|
|
backup := models.StorageBackup{
|
|
ID: fmt.Sprintf("backup-pmg-host-%d", i),
|
|
Storage: "local",
|
|
Node: pmgNodes[nodeIdx],
|
|
Type: "host", // This will now display as "Host" in the UI
|
|
VMID: 0, // Host backups have VMID=0
|
|
Time: backupTime,
|
|
CTime: backupTime.Unix(),
|
|
Size: int64(50*1024*1024 + rand.Intn(200*1024*1024)), // 50-250 MB
|
|
Format: "pmgbackup.tar.zst",
|
|
Notes: fmt.Sprintf("PMG host config backup - %s", pmgNodes[nodeIdx]),
|
|
Protected: rand.Float64() > 0.7, // 30% protected
|
|
Volid: fmt.Sprintf("local:backup/pmgbackup-%s-%s.tar.zst", pmgNodes[nodeIdx], backupTime.Format("2006_01_02-15_04_05")),
|
|
IsPBS: false,
|
|
Verified: rand.Float64() > 0.2, // 80% verified
|
|
}
|
|
|
|
if backup.Verified {
|
|
backup.Verification = "OK"
|
|
}
|
|
|
|
backups = append(backups, backup)
|
|
}
|
|
|
|
// Sort backups by time (newest first)
|
|
sort.Slice(backups, func(i, j int) bool {
|
|
return backups[i].Time.After(backups[j].Time)
|
|
})
|
|
|
|
return backups
|
|
}
|
|
|
|
// generatePBSInstances generates mock PBS instances
|
|
func generatePBSInstances() []models.PBSInstance {
|
|
pbsInstances := []models.PBSInstance{
|
|
{
|
|
ID: "pbs-main",
|
|
Name: "pbs-main",
|
|
Host: "192.168.0.10:8007",
|
|
Status: "online",
|
|
Version: "3.2.1",
|
|
CPU: 15.5 + rand.Float64()*10,
|
|
Memory: 45.2 + rand.Float64()*20,
|
|
MemoryUsed: int64(8 * 1024 * 1024 * 1024), // 8GB
|
|
MemoryTotal: int64(16 * 1024 * 1024 * 1024), // 16GB
|
|
Uptime: int64(86400 * 30), // 30 days
|
|
Datastores: []models.PBSDatastore{
|
|
{
|
|
Name: "backup-store",
|
|
Total: int64(10 * 1024 * 1024 * 1024 * 1024), // 10TB
|
|
Used: int64(4 * 1024 * 1024 * 1024 * 1024), // 4TB
|
|
Free: int64(6 * 1024 * 1024 * 1024 * 1024), // 6TB
|
|
Usage: 40.0,
|
|
Status: "available",
|
|
},
|
|
{
|
|
Name: "offsite-backup",
|
|
Total: int64(20 * 1024 * 1024 * 1024 * 1024), // 20TB
|
|
Used: int64(12 * 1024 * 1024 * 1024 * 1024), // 12TB
|
|
Free: int64(8 * 1024 * 1024 * 1024 * 1024), // 8TB
|
|
Usage: 60.0,
|
|
Status: "available",
|
|
},
|
|
},
|
|
ConnectionHealth: "healthy",
|
|
LastSeen: time.Now(),
|
|
},
|
|
}
|
|
|
|
// Add a secondary PBS if we have enough nodes
|
|
if rand.Float64() > 0.4 {
|
|
pbsInstances = append(pbsInstances, models.PBSInstance{
|
|
ID: "pbs-secondary",
|
|
Name: "pbs-secondary",
|
|
Host: "192.168.0.11:8007",
|
|
Status: "online",
|
|
Version: "3.2.0",
|
|
CPU: 10.2 + rand.Float64()*8,
|
|
Memory: 35.5 + rand.Float64()*15,
|
|
MemoryUsed: int64(4 * 1024 * 1024 * 1024), // 4GB
|
|
MemoryTotal: int64(8 * 1024 * 1024 * 1024), // 8GB
|
|
Uptime: int64(86400 * 15), // 15 days
|
|
Datastores: []models.PBSDatastore{
|
|
{
|
|
Name: "replica-store",
|
|
Total: int64(5 * 1024 * 1024 * 1024 * 1024), // 5TB
|
|
Used: int64(2 * 1024 * 1024 * 1024 * 1024), // 2TB
|
|
Free: int64(3 * 1024 * 1024 * 1024 * 1024), // 3TB
|
|
Usage: 40.0,
|
|
Status: "available",
|
|
},
|
|
},
|
|
ConnectionHealth: "healthy",
|
|
LastSeen: time.Now(),
|
|
})
|
|
}
|
|
|
|
return pbsInstances
|
|
}
|
|
|
|
// generatePBSBackups generates mock PBS backup data
|
|
func generatePBSBackups(vms []models.VM, containers []models.Container) []models.PBSBackup {
|
|
var backups []models.PBSBackup
|
|
pbsInstances := []string{"pbs-main"}
|
|
datastores := []string{"backup-store", "offsite-backup"}
|
|
owners := []string{"admin@pbs", "backup@pbs", "root@pam", "automation@pbs", "user1@pve", "service@pbs"}
|
|
|
|
// Add secondary PBS to list if it might exist
|
|
if rand.Float64() > 0.4 {
|
|
pbsInstances = append(pbsInstances, "pbs-secondary")
|
|
datastores = append(datastores, "replica-store")
|
|
}
|
|
|
|
// Generate PBS backups for ~50% of VMs
|
|
for _, vm := range vms {
|
|
if rand.Float64() > 0.5 {
|
|
continue
|
|
}
|
|
|
|
// Generate 2-4 PBS backups per VM
|
|
numBackups := 2 + rand.Intn(3)
|
|
for i := 0; i < numBackups; i++ {
|
|
backupTime := time.Now().Add(-time.Duration(rand.Intn(60*24)) * time.Hour)
|
|
|
|
backup := models.PBSBackup{
|
|
ID: fmt.Sprintf("pbs-backup-vm-%d-%d", vm.VMID, i),
|
|
Instance: pbsInstances[rand.Intn(len(pbsInstances))],
|
|
Datastore: datastores[rand.Intn(len(datastores))],
|
|
Namespace: "root",
|
|
BackupType: "vm",
|
|
VMID: fmt.Sprintf("%d", vm.VMID),
|
|
BackupTime: backupTime,
|
|
Size: int64(vm.Disk.Total/8 + rand.Int63n(vm.Disk.Total/4)),
|
|
Protected: rand.Float64() > 0.85, // 15% protected
|
|
Verified: rand.Float64() > 0.2, // 80% verified
|
|
Comment: fmt.Sprintf("Automated backup of %s", vm.Name),
|
|
Owner: owners[rand.Intn(len(owners))],
|
|
}
|
|
|
|
backups = append(backups, backup)
|
|
}
|
|
}
|
|
|
|
// Generate PBS backups for ~60% of containers
|
|
for _, ct := range containers {
|
|
if rand.Float64() > 0.4 {
|
|
continue
|
|
}
|
|
|
|
// Generate 1-3 PBS backups per container
|
|
numBackups := 1 + rand.Intn(3)
|
|
for i := 0; i < numBackups; i++ {
|
|
backupTime := time.Now().Add(-time.Duration(rand.Intn(45*24)) * time.Hour)
|
|
|
|
backup := models.PBSBackup{
|
|
ID: fmt.Sprintf("pbs-backup-ct-%d-%d", int(ct.VMID), i),
|
|
Instance: pbsInstances[rand.Intn(len(pbsInstances))],
|
|
Datastore: datastores[rand.Intn(len(datastores))],
|
|
Namespace: "root",
|
|
BackupType: "ct",
|
|
VMID: fmt.Sprintf("%d", int(ct.VMID)),
|
|
BackupTime: backupTime,
|
|
Size: int64(ct.Disk.Total/15 + rand.Int63n(ct.Disk.Total/8)),
|
|
Protected: rand.Float64() > 0.9, // 10% protected
|
|
Verified: rand.Float64() > 0.15, // 85% verified
|
|
Comment: fmt.Sprintf("Daily backup of %s", ct.Name),
|
|
Owner: owners[rand.Intn(len(owners))],
|
|
}
|
|
|
|
backups = append(backups, backup)
|
|
}
|
|
}
|
|
|
|
// Generate host config backups (VMID 0) - PMG/PVE host configs
|
|
// These are common when backing up Proxmox Mail Gateway hosts
|
|
hostBackupCount := 2 + rand.Intn(3) // 2-4 host backups
|
|
for i := 0; i < hostBackupCount; i++ {
|
|
backupTime := time.Now().Add(-time.Duration(rand.Intn(30*24)) * time.Hour)
|
|
|
|
backup := models.PBSBackup{
|
|
ID: fmt.Sprintf("pbs-backup-host-0-%d", i),
|
|
Instance: pbsInstances[rand.Intn(len(pbsInstances))],
|
|
Datastore: datastores[rand.Intn(len(datastores))],
|
|
Namespace: "root",
|
|
BackupType: "ct", // Host configs are stored as 'ct' type in PBS
|
|
VMID: "0", // VMID 0 indicates host config
|
|
BackupTime: backupTime,
|
|
Size: int64(50*1024*1024 + rand.Int63n(100*1024*1024)), // 50-150MB for host configs
|
|
Protected: rand.Float64() > 0.7, // 30% protected
|
|
Verified: rand.Float64() > 0.1, // 90% verified
|
|
Comment: "PMG host configuration backup",
|
|
Owner: "root@pam",
|
|
}
|
|
|
|
backups = append(backups, backup)
|
|
}
|
|
|
|
// Sort by time (newest first)
|
|
sort.Slice(backups, func(i, j int) bool {
|
|
return backups[i].BackupTime.After(backups[j].BackupTime)
|
|
})
|
|
|
|
return backups
|
|
}
|
|
|
|
func generatePMGInstances() []models.PMGInstance {
|
|
now := time.Now()
|
|
mailStats := models.PMGMailStats{
|
|
Timeframe: "day",
|
|
CountTotal: 2800 + rand.Float64()*400,
|
|
CountIn: 1800 + rand.Float64()*200,
|
|
CountOut: 900 + rand.Float64()*100,
|
|
SpamIn: 320 + rand.Float64()*40,
|
|
SpamOut: 45 + rand.Float64()*10,
|
|
VirusIn: 12 + rand.Float64()*3,
|
|
VirusOut: 3 + rand.Float64()*2,
|
|
BouncesIn: 18 + rand.Float64()*5,
|
|
BouncesOut: 6 + rand.Float64()*3,
|
|
BytesIn: 9.6e9 + rand.Float64()*1.5e9,
|
|
BytesOut: 3.2e9 + rand.Float64()*0.8e9,
|
|
GreylistCount: 210 + rand.Float64()*40,
|
|
JunkIn: 120 + rand.Float64()*20,
|
|
AverageProcessTimeMs: 480 + rand.Float64()*120,
|
|
RBLRejects: 140 + rand.Float64()*30,
|
|
PregreetRejects: 60 + rand.Float64()*15,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
mailPoints := make([]models.PMGMailCountPoint, 0, 24)
|
|
for i := 0; i < 24; i++ {
|
|
pointTime := now.Add(-time.Duration(23-i) * time.Hour)
|
|
baseCount := 80 + rand.Float64()*25
|
|
mailPoints = append(mailPoints, models.PMGMailCountPoint{
|
|
Timestamp: pointTime,
|
|
Count: baseCount + rand.Float64()*20,
|
|
CountIn: baseCount*0.65 + rand.Float64()*10,
|
|
CountOut: baseCount*0.35 + rand.Float64()*5,
|
|
SpamIn: baseCount*0.12 + rand.Float64()*4,
|
|
SpamOut: baseCount*0.02 + rand.Float64()*1,
|
|
VirusIn: rand.Float64() * 2,
|
|
VirusOut: rand.Float64(),
|
|
RBLRejects: rand.Float64() * 5,
|
|
Pregreet: rand.Float64() * 3,
|
|
BouncesIn: rand.Float64() * 4,
|
|
BouncesOut: rand.Float64() * 2,
|
|
Greylist: rand.Float64() * 6,
|
|
Index: i,
|
|
Timeframe: "hour",
|
|
WindowStart: pointTime,
|
|
WindowEnd: pointTime.Add(time.Hour),
|
|
})
|
|
}
|
|
|
|
spamBuckets := []models.PMGSpamBucket{
|
|
{Score: "0-2", Count: 950 + rand.Float64()*40},
|
|
{Score: "3-5", Count: 420 + rand.Float64()*25},
|
|
{Score: "6-8", Count: 180 + rand.Float64()*15},
|
|
{Score: "9-10", Count: 70 + rand.Float64()*10},
|
|
}
|
|
|
|
quarantine := models.PMGQuarantineTotals{
|
|
Spam: 25 + rand.Intn(30),
|
|
Virus: 2 + rand.Intn(6),
|
|
Attachment: 4 + rand.Intn(6),
|
|
Blacklisted: rand.Intn(8),
|
|
}
|
|
|
|
nodes := []models.PMGNodeStatus{
|
|
{
|
|
Name: "pmg-primary",
|
|
Status: "online",
|
|
Role: "master",
|
|
Uptime: int64(86400 * 18),
|
|
LoadAvg: fmt.Sprintf("%.2f", 0.75+rand.Float64()*0.25),
|
|
QueueStatus: &models.PMGQueueStatus{
|
|
Active: rand.Intn(5),
|
|
Deferred: rand.Intn(15),
|
|
Hold: rand.Intn(3),
|
|
Incoming: rand.Intn(8),
|
|
Total: 0, // Will be calculated below
|
|
OldestAge: int64(rand.Intn(3600)),
|
|
UpdatedAt: now,
|
|
},
|
|
},
|
|
}
|
|
// Calculate total for primary node
|
|
nodes[0].QueueStatus.Total = nodes[0].QueueStatus.Active + nodes[0].QueueStatus.Deferred +
|
|
nodes[0].QueueStatus.Hold + nodes[0].QueueStatus.Incoming
|
|
|
|
if rand.Float64() > 0.5 {
|
|
queueStatus := &models.PMGQueueStatus{
|
|
Active: rand.Intn(3),
|
|
Deferred: rand.Intn(10),
|
|
Hold: rand.Intn(2),
|
|
Incoming: rand.Intn(5),
|
|
Total: 0,
|
|
OldestAge: int64(rand.Intn(1800)),
|
|
UpdatedAt: now,
|
|
}
|
|
queueStatus.Total = queueStatus.Active + queueStatus.Deferred + queueStatus.Hold + queueStatus.Incoming
|
|
|
|
nodes = append(nodes, models.PMGNodeStatus{
|
|
Name: "pmg-secondary",
|
|
Status: "online",
|
|
Role: "node",
|
|
Uptime: int64(86400 * 9),
|
|
LoadAvg: fmt.Sprintf("%.2f", 0.55+rand.Float64()*0.2),
|
|
QueueStatus: queueStatus,
|
|
})
|
|
}
|
|
|
|
primary := models.PMGInstance{
|
|
ID: "pmg-main",
|
|
Name: "pmg-main",
|
|
Host: "https://pmg.mock.lan:8006",
|
|
Status: "online",
|
|
Version: "8.1-2",
|
|
Nodes: nodes,
|
|
MailStats: &mailStats,
|
|
MailCount: mailPoints,
|
|
SpamDistribution: spamBuckets,
|
|
Quarantine: &quarantine,
|
|
ConnectionHealth: "healthy",
|
|
LastSeen: now,
|
|
LastUpdated: now,
|
|
}
|
|
|
|
instances := []models.PMGInstance{primary}
|
|
|
|
if rand.Float64() > 0.65 {
|
|
backupNow := now.Add(-6 * time.Hour)
|
|
secondaryStats := mailStats
|
|
secondaryStats.CountTotal *= 0.6
|
|
secondaryStats.CountIn *= 0.6
|
|
secondaryStats.CountOut *= 0.6
|
|
secondaryStats.UpdatedAt = backupNow
|
|
|
|
edgeQueue := &models.PMGQueueStatus{
|
|
Active: rand.Intn(2),
|
|
Deferred: rand.Intn(5),
|
|
Hold: rand.Intn(1),
|
|
Incoming: rand.Intn(3),
|
|
Total: 0,
|
|
OldestAge: int64(rand.Intn(600)),
|
|
UpdatedAt: backupNow,
|
|
}
|
|
edgeQueue.Total = edgeQueue.Active + edgeQueue.Deferred + edgeQueue.Hold + edgeQueue.Incoming
|
|
|
|
instances = append(instances, models.PMGInstance{
|
|
ID: "pmg-edge",
|
|
Name: "pmg-edge",
|
|
Host: "https://pmg-edge.mock.lan:8006",
|
|
Status: "online",
|
|
Version: "8.0-7",
|
|
Nodes: []models.PMGNodeStatus{{
|
|
Name: "pmg-edge",
|
|
Status: "online",
|
|
Role: "standalone",
|
|
Uptime: int64(86400 * 4),
|
|
QueueStatus: edgeQueue,
|
|
}},
|
|
MailStats: &secondaryStats,
|
|
MailCount: mailPoints[:12],
|
|
SpamDistribution: spamBuckets,
|
|
Quarantine: &models.PMGQuarantineTotals{Spam: 8 + rand.Intn(5), Virus: rand.Intn(3)},
|
|
ConnectionHealth: "healthy",
|
|
LastSeen: backupNow,
|
|
LastUpdated: backupNow,
|
|
})
|
|
}
|
|
|
|
return instances
|
|
}
|
|
|
|
// generateSnapshots generates mock snapshot data for VMs and containers
|
|
func generateSnapshots(vms []models.VM, containers []models.Container) []models.GuestSnapshot {
|
|
var snapshots []models.GuestSnapshot
|
|
snapshotNames := []string{"before-upgrade", "production", "testing", "stable", "rollback-point", "pre-maintenance", "weekly-snapshot"}
|
|
|
|
// Generate snapshots for ~40% of VMs
|
|
for _, vm := range vms {
|
|
if rand.Float64() > 0.6 {
|
|
continue
|
|
}
|
|
|
|
// Generate 1-3 snapshots per VM
|
|
numSnapshots := 1 + rand.Intn(3)
|
|
for i := 0; i < numSnapshots; i++ {
|
|
snapshotTime := time.Now().Add(-time.Duration(rand.Intn(90*24)) * time.Hour)
|
|
|
|
snapshot := models.GuestSnapshot{
|
|
ID: fmt.Sprintf("snapshot-%s-vm-%d-%d", vm.Node, vm.VMID, i),
|
|
Name: snapshotNames[rand.Intn(len(snapshotNames))],
|
|
Node: vm.Node,
|
|
Instance: vm.Instance,
|
|
Type: "qemu",
|
|
VMID: vm.VMID,
|
|
Time: snapshotTime,
|
|
Description: fmt.Sprintf("Snapshot of %s taken on %s", vm.Name, snapshotTime.Format("2006-01-02")),
|
|
VMState: rand.Float64() > 0.5, // 50% include VM state
|
|
}
|
|
|
|
// Add parent relationship for some snapshots
|
|
if i > 0 && rand.Float64() > 0.5 {
|
|
snapshot.Parent = fmt.Sprintf("snapshot-%s-vm-%d-%d", vm.Node, vm.VMID, i-1)
|
|
}
|
|
|
|
snapshots = append(snapshots, snapshot)
|
|
}
|
|
}
|
|
|
|
// Generate snapshots for ~30% of containers
|
|
for _, ct := range containers {
|
|
if rand.Float64() > 0.7 {
|
|
continue
|
|
}
|
|
|
|
// Generate 1-2 snapshots per container
|
|
numSnapshots := 1 + rand.Intn(2)
|
|
for i := 0; i < numSnapshots; i++ {
|
|
snapshotTime := time.Now().Add(-time.Duration(rand.Intn(60*24)) * time.Hour)
|
|
|
|
snapshot := models.GuestSnapshot{
|
|
ID: fmt.Sprintf("snapshot-%s-ct-%d-%d", ct.Node, int(ct.VMID), i),
|
|
Name: snapshotNames[rand.Intn(len(snapshotNames))],
|
|
Node: ct.Node,
|
|
Instance: ct.Instance,
|
|
Type: "lxc",
|
|
VMID: int(ct.VMID),
|
|
Time: snapshotTime,
|
|
Description: fmt.Sprintf("Container snapshot for %s", ct.Name),
|
|
VMState: false, // Containers don't have VM state
|
|
}
|
|
|
|
snapshots = append(snapshots, snapshot)
|
|
}
|
|
}
|
|
|
|
// Sort by time (newest first)
|
|
sort.Slice(snapshots, func(i, j int) bool {
|
|
return snapshots[i].Time.After(snapshots[j].Time)
|
|
})
|
|
|
|
return snapshots
|
|
}
|
|
|
|
// UpdateMetrics simulates changing metrics over time
|
|
func UpdateMetrics(data *models.StateSnapshot, config MockConfig) {
|
|
updateDockerHosts(data, config)
|
|
|
|
if !config.RandomMetrics {
|
|
return
|
|
}
|
|
|
|
// Update node metrics
|
|
for i := range data.Nodes {
|
|
node := &data.Nodes[i]
|
|
// Small random walk for CPU with mean reversion
|
|
change := (rand.Float64() - 0.5) * 0.03
|
|
// Mean reversion: pull toward 20% CPU
|
|
meanReversion := (0.20 - node.CPU) * 0.05
|
|
node.CPU += change + meanReversion
|
|
node.CPU = math.Max(0.05, math.Min(0.85, node.CPU))
|
|
|
|
// Update memory with very small changes and mean reversion toward 50%
|
|
memChange := (rand.Float64() - 0.5) * 0.02
|
|
memMeanReversion := (50.0 - node.Memory.Usage) * 0.03
|
|
node.Memory.Usage += (memChange * 100) + memMeanReversion
|
|
node.Memory.Usage = math.Max(10, math.Min(85, node.Memory.Usage))
|
|
node.Memory.Used = int64(float64(node.Memory.Total) * (node.Memory.Usage / 100))
|
|
node.Memory.Free = node.Memory.Total - node.Memory.Used
|
|
}
|
|
|
|
// Update VM metrics
|
|
for i := range data.VMs {
|
|
vm := &data.VMs[i]
|
|
if vm.Status != "running" {
|
|
continue
|
|
}
|
|
|
|
// Random walk for CPU with mean reversion toward 15%
|
|
cpuChange := (rand.Float64() - 0.5) * 0.04
|
|
cpuMeanReversion := (0.15 - vm.CPU) * 0.08
|
|
vm.CPU += cpuChange + cpuMeanReversion
|
|
vm.CPU = math.Max(0.01, math.Min(0.85, vm.CPU))
|
|
|
|
// Update memory with smaller fluctuations and mean reversion toward 50%
|
|
memChange := (rand.Float64() - 0.5) * 0.03 // 3% swing
|
|
memMeanReversion := (50.0 - vm.Memory.Usage) * 0.04
|
|
vm.Memory.Usage += (memChange * 100) + memMeanReversion
|
|
vm.Memory.Usage = math.Max(10, math.Min(85, vm.Memory.Usage))
|
|
vm.Memory.Used = int64(float64(vm.Memory.Total) * (vm.Memory.Usage / 100))
|
|
vm.Memory.Free = vm.Memory.Total - vm.Memory.Used
|
|
|
|
// Update disk usage very slowly (disks fill up gradually)
|
|
if rand.Float64() < 0.05 { // 5% chance to change disk usage
|
|
diskChange := (rand.Float64() - 0.48) * 0.3 // Very slight bias toward filling
|
|
vm.Disk.Usage += diskChange
|
|
vm.Disk.Usage = math.Max(10, math.Min(90, vm.Disk.Usage))
|
|
vm.Disk.Used = int64(float64(vm.Disk.Total) * (vm.Disk.Usage / 100))
|
|
vm.Disk.Free = vm.Disk.Total - vm.Disk.Used
|
|
}
|
|
|
|
// Update network/disk I/O with small chance of changing
|
|
if rand.Float64() < 0.15 { // 15% chance of I/O change
|
|
vm.NetworkIn = generateRealisticIO("network-in")
|
|
vm.NetworkOut = generateRealisticIO("network-out")
|
|
vm.DiskRead = generateRealisticIO("disk-read")
|
|
vm.DiskWrite = generateRealisticIO("disk-write")
|
|
}
|
|
|
|
// Update uptime
|
|
vm.Uptime += 2 // Add 2 seconds per update
|
|
}
|
|
|
|
// Update container metrics
|
|
for i := range data.Containers {
|
|
ct := &data.Containers[i]
|
|
if ct.Status != "running" {
|
|
continue
|
|
}
|
|
|
|
// Random walk for CPU with mean reversion toward 8% (containers typically lower)
|
|
cpuChange := (rand.Float64() - 0.5) * 0.02
|
|
cpuMeanReversion := (0.08 - ct.CPU) * 0.10
|
|
ct.CPU += cpuChange + cpuMeanReversion
|
|
ct.CPU = math.Max(0.01, math.Min(0.75, ct.CPU))
|
|
|
|
// Update memory with smaller fluctuations and mean reversion toward 55%
|
|
memChange := (rand.Float64() - 0.5) * 0.02 // 2% swing
|
|
memMeanReversion := (55.0 - ct.Memory.Usage) * 0.04
|
|
ct.Memory.Usage += (memChange * 100) + memMeanReversion
|
|
ct.Memory.Usage = math.Max(5, math.Min(85, ct.Memory.Usage))
|
|
ct.Memory.Used = int64(float64(ct.Memory.Total) * (ct.Memory.Usage / 100))
|
|
ct.Memory.Free = ct.Memory.Total - ct.Memory.Used
|
|
|
|
// Update disk usage very slowly
|
|
if rand.Float64() < 0.03 { // 3% chance (containers change disk less)
|
|
diskChange := (rand.Float64() - 0.48) * 0.2 // Very slight bias toward filling
|
|
ct.Disk.Usage += diskChange
|
|
ct.Disk.Usage = math.Max(5, math.Min(85, ct.Disk.Usage))
|
|
ct.Disk.Used = int64(float64(ct.Disk.Total) * (ct.Disk.Usage / 100))
|
|
ct.Disk.Free = ct.Disk.Total - ct.Disk.Used
|
|
}
|
|
|
|
// Update network/disk I/O with small chance of changing
|
|
if rand.Float64() < 0.10 { // 10% chance of I/O change (containers change less often)
|
|
ct.NetworkIn = generateRealisticIO("network-in-ct")
|
|
ct.NetworkOut = generateRealisticIO("network-out-ct")
|
|
ct.DiskRead = generateRealisticIO("disk-read-ct")
|
|
ct.DiskWrite = generateRealisticIO("disk-write-ct")
|
|
}
|
|
|
|
// Update uptime
|
|
ct.Uptime += 2
|
|
}
|
|
|
|
// Update disk metrics occasionally
|
|
for i := range data.PhysicalDisks {
|
|
disk := &data.PhysicalDisks[i]
|
|
|
|
// Occasionally change temperature
|
|
if rand.Float64() < 0.1 {
|
|
disk.Temperature += rand.Intn(5) - 2
|
|
if disk.Temperature < 30 {
|
|
disk.Temperature = 30
|
|
}
|
|
if disk.Temperature > 85 {
|
|
disk.Temperature = 85
|
|
}
|
|
}
|
|
|
|
// Occasionally degrade SSD life
|
|
if disk.Wearout > 0 && rand.Float64() < 0.01 {
|
|
disk.Wearout = disk.Wearout - 1
|
|
if disk.Wearout < 0 {
|
|
disk.Wearout = 0
|
|
}
|
|
}
|
|
|
|
disk.LastChecked = time.Now()
|
|
}
|
|
|
|
// Update PMG metrics with gentle fluctuations
|
|
for i := range data.PMGInstances {
|
|
inst := &data.PMGInstances[i]
|
|
now := time.Now()
|
|
inst.LastSeen = now
|
|
inst.LastUpdated = now
|
|
|
|
if inst.MailStats != nil {
|
|
inst.MailStats.CountTotal = fluctuateFloat(inst.MailStats.CountTotal, 0.05, 0, math.MaxFloat64)
|
|
inst.MailStats.CountIn = fluctuateFloat(inst.MailStats.CountIn, 0.05, 0, math.MaxFloat64)
|
|
inst.MailStats.CountOut = fluctuateFloat(inst.MailStats.CountOut, 0.05, 0, math.MaxFloat64)
|
|
inst.MailStats.SpamIn = fluctuateFloat(inst.MailStats.SpamIn, 0.08, 0, math.MaxFloat64)
|
|
inst.MailStats.SpamOut = fluctuateFloat(inst.MailStats.SpamOut, 0.08, 0, math.MaxFloat64)
|
|
inst.MailStats.VirusIn = fluctuateFloat(inst.MailStats.VirusIn, 0.1, 0, math.MaxFloat64)
|
|
inst.MailStats.VirusOut = fluctuateFloat(inst.MailStats.VirusOut, 0.1, 0, math.MaxFloat64)
|
|
inst.MailStats.RBLRejects = fluctuateFloat(inst.MailStats.RBLRejects, 0.07, 0, math.MaxFloat64)
|
|
inst.MailStats.PregreetRejects = fluctuateFloat(inst.MailStats.PregreetRejects, 0.07, 0, math.MaxFloat64)
|
|
inst.MailStats.GreylistCount = fluctuateFloat(inst.MailStats.GreylistCount, 0.05, 0, math.MaxFloat64)
|
|
inst.MailStats.AverageProcessTimeMs = fluctuateFloat(inst.MailStats.AverageProcessTimeMs, 0.05, 200, 2000)
|
|
inst.MailStats.UpdatedAt = now
|
|
}
|
|
|
|
if len(inst.MailCount) > 0 {
|
|
// Drop oldest point if we already have 24
|
|
if len(inst.MailCount) >= 24 {
|
|
inst.MailCount = inst.MailCount[1:]
|
|
}
|
|
|
|
base := 60 + rand.Float64()*30
|
|
newPoint := models.PMGMailCountPoint{
|
|
Timestamp: now,
|
|
Count: base + rand.Float64()*15,
|
|
CountIn: base*0.6 + rand.Float64()*10,
|
|
CountOut: base*0.4 + rand.Float64()*8,
|
|
SpamIn: base*0.1 + rand.Float64()*5,
|
|
SpamOut: base*0.02 + rand.Float64()*1,
|
|
VirusIn: rand.Float64() * 2,
|
|
VirusOut: rand.Float64(),
|
|
RBLRejects: rand.Float64() * 4,
|
|
Pregreet: rand.Float64() * 3,
|
|
BouncesIn: rand.Float64() * 3,
|
|
BouncesOut: rand.Float64() * 2,
|
|
Greylist: rand.Float64() * 5,
|
|
Index: len(inst.MailCount),
|
|
Timeframe: "hour",
|
|
}
|
|
inst.MailCount = append(inst.MailCount, newPoint)
|
|
}
|
|
|
|
if len(inst.SpamDistribution) > 0 {
|
|
for j := range inst.SpamDistribution {
|
|
inst.SpamDistribution[j].Count = fluctuateFloat(inst.SpamDistribution[j].Count, 0.05, 0, math.MaxFloat64)
|
|
}
|
|
}
|
|
|
|
if inst.Quarantine != nil {
|
|
inst.Quarantine.Spam = fluctuateInt(inst.Quarantine.Spam, 5, 0, 500)
|
|
inst.Quarantine.Virus = fluctuateInt(inst.Quarantine.Virus, 2, 0, 200)
|
|
inst.Quarantine.Attachment = fluctuateInt(inst.Quarantine.Attachment, 2, 0, 200)
|
|
inst.Quarantine.Blacklisted = fluctuateInt(inst.Quarantine.Blacklisted, 1, 0, 100)
|
|
}
|
|
|
|
if len(inst.Nodes) > 0 {
|
|
for j := range inst.Nodes {
|
|
if inst.Nodes[j].Status == "online" {
|
|
inst.Nodes[j].Uptime += int64(updateInterval.Seconds())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
data.LastUpdate = time.Now()
|
|
}
|
|
|
|
func updateDockerHosts(data *models.StateSnapshot, config MockConfig) {
|
|
if len(data.DockerHosts) == 0 {
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
step := int64(updateInterval.Seconds())
|
|
if step <= 0 {
|
|
step = 2
|
|
}
|
|
|
|
for i := range data.DockerHosts {
|
|
host := &data.DockerHosts[i]
|
|
|
|
if host.Status != "offline" {
|
|
host.LastSeen = now.Add(-time.Duration(rand.Intn(6)) * time.Second)
|
|
host.UptimeSeconds += step
|
|
} else if config.RandomMetrics && rand.Float64() < 0.01 {
|
|
// Occasionally bring an offline host back online
|
|
host.Status = "online"
|
|
host.LastSeen = now
|
|
if len(host.Containers) == 0 {
|
|
host.Containers = generateDockerContainers(host.Hostname, i, config)
|
|
}
|
|
}
|
|
|
|
running := 0
|
|
flagged := 0
|
|
|
|
for j := range host.Containers {
|
|
container := &host.Containers[j]
|
|
state := strings.ToLower(container.State)
|
|
health := strings.ToLower(container.Health)
|
|
|
|
if state != "running" {
|
|
if health == "unhealthy" || health == "starting" {
|
|
flagged++
|
|
}
|
|
|
|
if config.RandomMetrics && (state == "exited" || state == "paused") && rand.Float64() < 0.02 {
|
|
container.State = "running"
|
|
container.Status = "Up a few seconds"
|
|
container.Health = "starting"
|
|
container.CPUPercent = clampFloat(rand.Float64()*35, 2, 90)
|
|
container.MemoryPercent = clampFloat(25+rand.Float64()*40, 5, 85)
|
|
if container.MemoryLimit > 0 {
|
|
container.MemoryUsage = int64(float64(container.MemoryLimit) * (container.MemoryPercent / 100.0))
|
|
}
|
|
container.UptimeSeconds = step
|
|
start := now.Add(-time.Duration(container.UptimeSeconds) * time.Second)
|
|
container.StartedAt = &start
|
|
container.FinishedAt = nil
|
|
state = "running"
|
|
health = "starting"
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if state == "running" {
|
|
running++
|
|
|
|
if config.RandomMetrics {
|
|
cpuChange := (rand.Float64() - 0.5) * 6
|
|
container.CPUPercent = clampFloat(container.CPUPercent+cpuChange, 0, 190)
|
|
|
|
memChange := (rand.Float64() - 0.5) * 4
|
|
container.MemoryPercent = clampFloat(container.MemoryPercent+memChange, 1, 97)
|
|
if container.MemoryLimit > 0 {
|
|
container.MemoryUsage = int64(float64(container.MemoryLimit) * (container.MemoryPercent / 100.0))
|
|
}
|
|
|
|
container.UptimeSeconds += step
|
|
|
|
switch health {
|
|
case "unhealthy":
|
|
if rand.Float64() < 0.3 {
|
|
container.Health = "healthy"
|
|
health = "healthy"
|
|
}
|
|
case "starting":
|
|
if rand.Float64() < 0.5 {
|
|
container.Health = "healthy"
|
|
health = "healthy"
|
|
}
|
|
default:
|
|
if rand.Float64() < 0.03 {
|
|
container.Health = "unhealthy"
|
|
health = "unhealthy"
|
|
} else if rand.Float64() < 0.04 {
|
|
container.Health = "starting"
|
|
health = "starting"
|
|
}
|
|
}
|
|
|
|
if rand.Float64() < 0.01 {
|
|
container.RestartCount++
|
|
}
|
|
}
|
|
|
|
if health == "unhealthy" || health == "starting" {
|
|
flagged++
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.ConnectionHealth != nil {
|
|
data.ConnectionHealth[dockerConnectionPrefix+host.ID] = host.Status != "offline"
|
|
}
|
|
|
|
if host.Status == "offline" {
|
|
continue
|
|
}
|
|
|
|
total := len(host.Containers)
|
|
if total == 0 {
|
|
host.Status = "offline"
|
|
if data.ConnectionHealth != nil {
|
|
data.ConnectionHealth[dockerConnectionPrefix+host.ID] = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
if running == 0 {
|
|
host.Status = "offline"
|
|
host.LastSeen = now.Add(-90 * time.Second)
|
|
if data.ConnectionHealth != nil {
|
|
data.ConnectionHealth[dockerConnectionPrefix+host.ID] = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
stopped := total - running
|
|
if flagged > 0 || float64(stopped)/float64(total) > 0.35 {
|
|
host.Status = "degraded"
|
|
} else {
|
|
host.Status = "online"
|
|
}
|
|
}
|
|
}
|
|
|
|
func fluctuateFloat(value, variance, min, max float64) float64 {
|
|
change := (rand.Float64()*2 - 1) * variance
|
|
newValue := value * (1 + change)
|
|
if newValue < min {
|
|
newValue = min
|
|
}
|
|
if newValue > max {
|
|
newValue = max
|
|
}
|
|
return newValue
|
|
}
|
|
|
|
func fluctuateInt(value, delta, min, max int) int {
|
|
if delta <= 0 {
|
|
return value
|
|
}
|
|
change := rand.Intn(delta*2+1) - delta
|
|
newValue := value + change
|
|
if newValue < min {
|
|
newValue = min
|
|
}
|
|
if newValue > max {
|
|
newValue = max
|
|
}
|
|
return newValue
|
|
}
|
|
|
|
func generateDisksForNode(node models.Node) []models.PhysicalDisk {
|
|
disks := []models.PhysicalDisk{}
|
|
|
|
// Generate 1-3 disks per node
|
|
diskCount := rand.Intn(3) + 1
|
|
|
|
diskModels := []struct {
|
|
model string
|
|
diskType string
|
|
size int64
|
|
}{
|
|
{"Samsung SSD 970 EVO Plus 1TB", "nvme", 1000204886016},
|
|
{"WD Blue SN570 500GB", "nvme", 500107862016},
|
|
{"Crucial MX500 2TB", "sata", 2000398934016},
|
|
{"Seagate BarraCuda 4TB", "sata", 4000787030016},
|
|
{"Kingston NV2 250GB", "nvme", 250059350016},
|
|
{"WD Red Pro 8TB", "sata", 8001563222016},
|
|
{"Samsung 980 PRO 2TB", "nvme", 2000398934016},
|
|
{"Intel SSD 660p 1TB", "nvme", 1000204886016},
|
|
{"Toshiba X300 6TB", "sata", 6001175126016},
|
|
}
|
|
|
|
for i := 0; i < diskCount; i++ {
|
|
diskModel := diskModels[rand.Intn(len(diskModels))]
|
|
|
|
// Generate health status - most are healthy
|
|
health := "PASSED"
|
|
if rand.Float64() < 0.05 { // 5% chance of failure
|
|
health = "FAILED"
|
|
} else if rand.Float64() < 0.1 { // 10% chance of unknown
|
|
health = "UNKNOWN"
|
|
}
|
|
|
|
// Generate wearout for SSDs (percentage life remaining; lower numbers mean heavy wear)
|
|
wearout := 0
|
|
if diskModel.diskType == "nvme" || diskModel.diskType == "sata" {
|
|
if rand.Float64() < 0.7 { // 70% chance it's an SSD with wearout data
|
|
wearout = rand.Intn(50) + 50 // 50-100% life remaining
|
|
if rand.Float64() < 0.1 { // 10% chance of low life
|
|
wearout = rand.Intn(15) + 5 // 5-20% life remaining
|
|
}
|
|
}
|
|
}
|
|
if wearout < 0 {
|
|
wearout = 0
|
|
}
|
|
if wearout > 100 {
|
|
wearout = 100
|
|
}
|
|
|
|
// Generate temperature
|
|
temp := rand.Intn(20) + 35 // 35-55°C normal range
|
|
if rand.Float64() < 0.1 { // 10% chance of high temp
|
|
temp = rand.Intn(15) + 65 // 65-80°C hot
|
|
}
|
|
|
|
disk := models.PhysicalDisk{
|
|
ID: fmt.Sprintf("%s-%s-/dev/%s%d", node.Instance, node.Name, []string{"nvme", "sd"}[i%2], i),
|
|
Node: node.Name,
|
|
Instance: node.Instance,
|
|
DevPath: fmt.Sprintf("/dev/%s%d", []string{"nvme", "sd"}[i%2], i),
|
|
Model: diskModel.model,
|
|
Serial: fmt.Sprintf("SERIAL%d%d%d", rand.Intn(9999), rand.Intn(9999), rand.Intn(9999)),
|
|
Type: diskModel.diskType,
|
|
Size: diskModel.size,
|
|
Health: health,
|
|
Wearout: wearout,
|
|
Temperature: temp,
|
|
Used: []string{"ext4", "zfs", "btrfs", "xfs"}[rand.Intn(4)],
|
|
LastChecked: time.Now(),
|
|
}
|
|
|
|
disks = append(disks, disk)
|
|
}
|
|
|
|
return disks
|
|
}
|