This commit implements the Unified Resource Architecture for AI-first
infrastructure management. Key features:
Phase 1 - Backend Unification:
- New unified Resource type with 9 resource types, 7 platforms, 7 statuses
- Resource store with identity-based deduplication (hostname, machineID, IP)
- 8 converter functions (FromNode, FromVM, FromContainer, etc.)
- REST API endpoints: /api/resources, /api/resources/stats, /api/resources/{id}
- 28 comprehensive unit tests
Phase 2 - AI Context Enhancement:
- Unified context builder for AI system prompts
- Cross-platform query methods: GetTopByCPU, GetTopByMemory, GetTopByDisk
- Resource correlation: GetRelated (parent, children, siblings, cluster)
- Infrastructure summary: GetResourceSummary with health status counts
- AI context now includes top consumers and infrastructure overview
Phase 3 - Agent Preference & Hybrid Mode:
- Polling optimization methods in resource store
- ResourceStoreInterface added to Monitor
- SetResourceStore() and shouldSkipNodeMetrics() helper methods
- Store automatically wired into Monitor via Router.SetMonitor()
- Foundation ready for reduced API polling when agents are active
Files added:
- internal/resources/resource.go - Core Resource type
- internal/resources/store.go - Store with deduplication
- internal/resources/converters.go - Type converters
- internal/resources/platform_data.go - Platform-specific data
- internal/resources/store_test.go - 28 tests
- internal/resources/converters_test.go - Converter tests
- internal/api/resource_handlers.go - REST API handlers
- internal/ai/resource_context.go - AI context builder
- .gemini/docs/unified-resource-architecture.md - Architecture docs
All tests pass.
847 lines
20 KiB
Go
847 lines
20 KiB
Go
package resources
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
// FromNode converts a Proxmox Node to a unified Resource.
|
|
func FromNode(n models.Node) Resource {
|
|
// Calculate primary temperature if available
|
|
var temp *float64
|
|
if n.Temperature != nil && n.Temperature.Available && n.Temperature.HasCPU {
|
|
// Use CPU package temperature if available, otherwise average core temps
|
|
if n.Temperature.CPUPackage > 0 {
|
|
temp = &n.Temperature.CPUPackage
|
|
} else if len(n.Temperature.Cores) > 0 {
|
|
avg := 0.0
|
|
for _, c := range n.Temperature.Cores {
|
|
avg += c.Temp
|
|
}
|
|
avg /= float64(len(n.Temperature.Cores))
|
|
temp = &avg
|
|
}
|
|
}
|
|
|
|
// Build memory metric
|
|
var memory *MetricValue
|
|
if n.Memory.Total > 0 {
|
|
memory = &MetricValue{
|
|
Current: n.Memory.Usage,
|
|
Total: &n.Memory.Total,
|
|
Used: &n.Memory.Used,
|
|
Free: &n.Memory.Free,
|
|
}
|
|
}
|
|
|
|
// Build disk metric
|
|
var disk *MetricValue
|
|
if n.Disk.Total > 0 {
|
|
disk = &MetricValue{
|
|
Current: n.Disk.Usage,
|
|
Total: &n.Disk.Total,
|
|
Used: &n.Disk.Used,
|
|
Free: &n.Disk.Free,
|
|
}
|
|
}
|
|
|
|
// Build platform data
|
|
platformData := NodePlatformData{
|
|
Instance: n.Instance,
|
|
Host: n.Host,
|
|
GuestURL: n.GuestURL,
|
|
PVEVersion: n.PVEVersion,
|
|
KernelVersion: n.KernelVersion,
|
|
LoadAverage: n.LoadAverage,
|
|
IsClusterMember: n.IsClusterMember,
|
|
ClusterName: n.ClusterName,
|
|
ConnectionHealth: n.ConnectionHealth,
|
|
CPUInfo: CPUInfo{
|
|
Model: n.CPUInfo.Model,
|
|
Cores: n.CPUInfo.Cores,
|
|
Sockets: n.CPUInfo.Sockets,
|
|
},
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
// Determine cluster ID
|
|
clusterID := ""
|
|
if n.IsClusterMember && n.ClusterName != "" {
|
|
clusterID = fmt.Sprintf("pve-cluster/%s", n.ClusterName)
|
|
}
|
|
|
|
return Resource{
|
|
ID: n.ID,
|
|
Type: ResourceTypeNode,
|
|
Name: n.Name,
|
|
DisplayName: n.DisplayName,
|
|
PlatformID: n.Instance,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
ClusterID: clusterID,
|
|
Status: mapNodeStatus(n.Status),
|
|
CPU: &MetricValue{
|
|
Current: n.CPU * 100, // Node CPU is 0-1, convert to percentage
|
|
},
|
|
Memory: memory,
|
|
Disk: disk,
|
|
Temperature: temp,
|
|
Uptime: &n.Uptime,
|
|
LastSeen: n.LastSeen,
|
|
PlatformData: platformDataJSON,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: n.Name,
|
|
},
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// FromVM converts a Proxmox VM to a unified Resource.
|
|
func FromVM(vm models.VM) Resource {
|
|
// Build memory metric
|
|
var memory *MetricValue
|
|
if vm.Memory.Total > 0 {
|
|
memory = &MetricValue{
|
|
Current: vm.Memory.Usage,
|
|
Total: &vm.Memory.Total,
|
|
Used: &vm.Memory.Used,
|
|
Free: &vm.Memory.Free,
|
|
}
|
|
}
|
|
|
|
// Build disk metric
|
|
var disk *MetricValue
|
|
if vm.Disk.Total > 0 {
|
|
disk = &MetricValue{
|
|
Current: vm.Disk.Usage,
|
|
Total: &vm.Disk.Total,
|
|
Used: &vm.Disk.Used,
|
|
Free: &vm.Disk.Free,
|
|
}
|
|
}
|
|
|
|
// Build network metric
|
|
network := &NetworkMetric{
|
|
RXBytes: vm.NetworkIn,
|
|
TXBytes: vm.NetworkOut,
|
|
}
|
|
|
|
// Build platform data
|
|
var lastBackup *time.Time
|
|
if !vm.LastBackup.IsZero() {
|
|
lastBackup = &vm.LastBackup
|
|
}
|
|
platformData := VMPlatformData{
|
|
VMID: vm.VMID,
|
|
Node: vm.Node,
|
|
Instance: vm.Instance,
|
|
CPUs: vm.CPUs,
|
|
Template: vm.Template,
|
|
Lock: vm.Lock,
|
|
AgentVersion: vm.AgentVersion,
|
|
OSName: vm.OSName,
|
|
OSVersion: vm.OSVersion,
|
|
IPAddresses: vm.IPAddresses,
|
|
NetworkIn: vm.NetworkIn,
|
|
NetworkOut: vm.NetworkOut,
|
|
DiskRead: vm.DiskRead,
|
|
DiskWrite: vm.DiskWrite,
|
|
LastBackup: lastBackup,
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
// Parent is the node
|
|
parentID := fmt.Sprintf("%s/node/%s", vm.Instance, vm.Node)
|
|
|
|
return Resource{
|
|
ID: vm.ID,
|
|
Type: ResourceTypeVM,
|
|
Name: vm.Name,
|
|
PlatformID: vm.Instance,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
ParentID: parentID,
|
|
Status: mapGuestStatus(vm.Status),
|
|
CPU: &MetricValue{
|
|
Current: vm.CPU * 100, // VM CPU is 0-1, convert to percentage
|
|
},
|
|
Memory: memory,
|
|
Disk: disk,
|
|
Network: network,
|
|
Uptime: &vm.Uptime,
|
|
Tags: vm.Tags,
|
|
LastSeen: vm.LastSeen,
|
|
PlatformData: platformDataJSON,
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// FromContainer converts a Proxmox LXC Container to a unified Resource.
|
|
func FromContainer(ct models.Container) Resource {
|
|
// Build memory metric
|
|
var memory *MetricValue
|
|
if ct.Memory.Total > 0 {
|
|
memory = &MetricValue{
|
|
Current: ct.Memory.Usage,
|
|
Total: &ct.Memory.Total,
|
|
Used: &ct.Memory.Used,
|
|
Free: &ct.Memory.Free,
|
|
}
|
|
}
|
|
|
|
// Build disk metric
|
|
var disk *MetricValue
|
|
if ct.Disk.Total > 0 {
|
|
disk = &MetricValue{
|
|
Current: ct.Disk.Usage,
|
|
Total: &ct.Disk.Total,
|
|
Used: &ct.Disk.Used,
|
|
Free: &ct.Disk.Free,
|
|
}
|
|
}
|
|
|
|
// Build network metric
|
|
network := &NetworkMetric{
|
|
RXBytes: ct.NetworkIn,
|
|
TXBytes: ct.NetworkOut,
|
|
}
|
|
|
|
// Build platform data
|
|
var lastBackup *time.Time
|
|
if !ct.LastBackup.IsZero() {
|
|
lastBackup = &ct.LastBackup
|
|
}
|
|
platformData := ContainerPlatformData{
|
|
VMID: ct.VMID,
|
|
Node: ct.Node,
|
|
Instance: ct.Instance,
|
|
CPUs: ct.CPUs,
|
|
Template: ct.Template,
|
|
Lock: ct.Lock,
|
|
OSName: ct.OSName,
|
|
IPAddresses: ct.IPAddresses,
|
|
NetworkIn: ct.NetworkIn,
|
|
NetworkOut: ct.NetworkOut,
|
|
DiskRead: ct.DiskRead,
|
|
DiskWrite: ct.DiskWrite,
|
|
LastBackup: lastBackup,
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
// Parent is the node
|
|
parentID := fmt.Sprintf("%s/node/%s", ct.Instance, ct.Node)
|
|
|
|
return Resource{
|
|
ID: ct.ID,
|
|
Type: ResourceTypeContainer,
|
|
Name: ct.Name,
|
|
PlatformID: ct.Instance,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
ParentID: parentID,
|
|
Status: mapGuestStatus(ct.Status),
|
|
CPU: &MetricValue{
|
|
Current: ct.CPU * 100, // Container CPU is 0-1, convert to percentage
|
|
},
|
|
Memory: memory,
|
|
Disk: disk,
|
|
Network: network,
|
|
Uptime: &ct.Uptime,
|
|
Tags: ct.Tags,
|
|
LastSeen: ct.LastSeen,
|
|
PlatformData: platformDataJSON,
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// FromHost converts a Host agent to a unified Resource.
|
|
func FromHost(h models.Host) Resource {
|
|
// Build memory metric
|
|
var memory *MetricValue
|
|
if h.Memory.Total > 0 {
|
|
memory = &MetricValue{
|
|
Current: h.Memory.Usage,
|
|
Total: &h.Memory.Total,
|
|
Used: &h.Memory.Used,
|
|
Free: &h.Memory.Free,
|
|
}
|
|
}
|
|
|
|
// Combine disk metrics from multiple disks
|
|
var disk *MetricValue
|
|
var totalDisk, usedDisk, freeDisk int64
|
|
for _, d := range h.Disks {
|
|
totalDisk += d.Total
|
|
usedDisk += d.Used
|
|
freeDisk += d.Free
|
|
}
|
|
if totalDisk > 0 {
|
|
usage := float64(usedDisk) / float64(totalDisk) * 100
|
|
disk = &MetricValue{
|
|
Current: usage,
|
|
Total: &totalDisk,
|
|
Used: &usedDisk,
|
|
Free: &freeDisk,
|
|
}
|
|
}
|
|
|
|
// Calculate network totals
|
|
var rxTotal, txTotal int64
|
|
for _, iface := range h.NetworkInterfaces {
|
|
rxTotal += int64(iface.RXBytes)
|
|
txTotal += int64(iface.TXBytes)
|
|
}
|
|
network := &NetworkMetric{
|
|
RXBytes: rxTotal,
|
|
TXBytes: txTotal,
|
|
}
|
|
|
|
// Get primary temperature
|
|
var temp *float64
|
|
if len(h.Sensors.TemperatureCelsius) > 0 {
|
|
// Pick the first available temperature
|
|
for _, t := range h.Sensors.TemperatureCelsius {
|
|
temp = &t
|
|
break
|
|
}
|
|
}
|
|
|
|
// Build platform data
|
|
disks := make([]DiskInfo, len(h.Disks))
|
|
for i, d := range h.Disks {
|
|
disks[i] = DiskInfo{
|
|
Mountpoint: d.Mountpoint,
|
|
Device: d.Device,
|
|
Type: d.Type,
|
|
Total: d.Total,
|
|
Used: d.Used,
|
|
Free: d.Free,
|
|
Usage: d.Usage,
|
|
}
|
|
}
|
|
|
|
interfaces := make([]NetworkInterface, len(h.NetworkInterfaces))
|
|
for i, iface := range h.NetworkInterfaces {
|
|
interfaces[i] = NetworkInterface{
|
|
Name: iface.Name,
|
|
MAC: iface.MAC,
|
|
Addresses: iface.Addresses,
|
|
RXBytes: iface.RXBytes,
|
|
TXBytes: iface.TXBytes,
|
|
SpeedMbps: iface.SpeedMbps,
|
|
}
|
|
}
|
|
|
|
diskIO := make([]DiskIOStats, len(h.DiskIO))
|
|
for i, d := range h.DiskIO {
|
|
diskIO[i] = DiskIOStats{
|
|
Device: d.Device,
|
|
ReadBytes: d.ReadBytes,
|
|
WriteBytes: d.WriteBytes,
|
|
ReadOps: d.ReadOps,
|
|
WriteOps: d.WriteOps,
|
|
ReadTimeMs: d.ReadTime,
|
|
WriteTimeMs: d.WriteTime,
|
|
IOTimeMs: d.IOTime,
|
|
}
|
|
}
|
|
|
|
raid := make([]HostRAIDArray, len(h.RAID))
|
|
for i, r := range h.RAID {
|
|
devices := make([]HostRAIDDevice, len(r.Devices))
|
|
for j, d := range r.Devices {
|
|
devices[j] = HostRAIDDevice{
|
|
Device: d.Device,
|
|
State: d.State,
|
|
Slot: d.Slot,
|
|
}
|
|
}
|
|
raid[i] = HostRAIDArray{
|
|
Device: r.Device,
|
|
Name: r.Name,
|
|
Level: r.Level,
|
|
State: r.State,
|
|
TotalDevices: r.TotalDevices,
|
|
ActiveDevices: r.ActiveDevices,
|
|
WorkingDevices: r.WorkingDevices,
|
|
FailedDevices: r.FailedDevices,
|
|
SpareDevices: r.SpareDevices,
|
|
UUID: r.UUID,
|
|
Devices: devices,
|
|
RebuildPercent: r.RebuildPercent,
|
|
RebuildSpeed: r.RebuildSpeed,
|
|
}
|
|
}
|
|
|
|
platformData := HostPlatformData{
|
|
Platform: h.Platform,
|
|
OSName: h.OSName,
|
|
OSVersion: h.OSVersion,
|
|
KernelVersion: h.KernelVersion,
|
|
Architecture: h.Architecture,
|
|
CPUCount: h.CPUCount,
|
|
LoadAverage: h.LoadAverage,
|
|
AgentVersion: h.AgentVersion,
|
|
IsLegacy: h.IsLegacy,
|
|
Disks: disks,
|
|
Interfaces: interfaces,
|
|
DiskIO: diskIO,
|
|
RAID: raid,
|
|
TokenID: h.TokenID,
|
|
TokenName: h.TokenName,
|
|
TokenHint: h.TokenHint,
|
|
TokenLastUsedAt: h.TokenLastUsedAt,
|
|
Sensors: HostSensorSummary{
|
|
TemperatureCelsius: h.Sensors.TemperatureCelsius,
|
|
FanRPM: h.Sensors.FanRPM,
|
|
Additional: h.Sensors.Additional,
|
|
},
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
// Collect IPs for identity
|
|
var ips []string
|
|
for _, iface := range h.NetworkInterfaces {
|
|
ips = append(ips, iface.Addresses...)
|
|
}
|
|
|
|
return Resource{
|
|
ID: h.ID,
|
|
Type: ResourceTypeHost,
|
|
Name: h.Hostname,
|
|
DisplayName: h.DisplayName,
|
|
PlatformID: "host-agent", // No specific platform ID for host agents
|
|
PlatformType: PlatformHostAgent,
|
|
SourceType: SourceAgent,
|
|
Status: mapHostStatus(h.Status),
|
|
CPU: &MetricValue{
|
|
Current: h.CPUUsage,
|
|
},
|
|
Memory: memory,
|
|
Disk: disk,
|
|
Network: network,
|
|
Temperature: temp,
|
|
Uptime: &h.UptimeSeconds,
|
|
Tags: h.Tags,
|
|
LastSeen: h.LastSeen,
|
|
PlatformData: platformDataJSON,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: h.Hostname,
|
|
IPs: ips,
|
|
},
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// FromDockerHost converts a DockerHost to a unified Resource.
|
|
func FromDockerHost(dh models.DockerHost) Resource {
|
|
// Build memory metric
|
|
var memory *MetricValue
|
|
if dh.Memory.Total > 0 {
|
|
memory = &MetricValue{
|
|
Current: dh.Memory.Usage,
|
|
Total: &dh.Memory.Total,
|
|
Used: &dh.Memory.Used,
|
|
Free: &dh.Memory.Free,
|
|
}
|
|
}
|
|
|
|
// Combine disk metrics
|
|
var disk *MetricValue
|
|
var totalDisk, usedDisk, freeDisk int64
|
|
for _, d := range dh.Disks {
|
|
totalDisk += d.Total
|
|
usedDisk += d.Used
|
|
freeDisk += d.Free
|
|
}
|
|
if totalDisk > 0 {
|
|
usage := float64(usedDisk) / float64(totalDisk) * 100
|
|
disk = &MetricValue{
|
|
Current: usage,
|
|
Total: &totalDisk,
|
|
Used: &usedDisk,
|
|
Free: &freeDisk,
|
|
}
|
|
}
|
|
|
|
// Calculate network totals
|
|
var rxTotal, txTotal int64
|
|
for _, iface := range dh.NetworkInterfaces {
|
|
rxTotal += int64(iface.RXBytes)
|
|
txTotal += int64(iface.TXBytes)
|
|
}
|
|
network := &NetworkMetric{
|
|
RXBytes: rxTotal,
|
|
TXBytes: txTotal,
|
|
}
|
|
|
|
// Build platform data
|
|
disks := make([]DiskInfo, len(dh.Disks))
|
|
for i, d := range dh.Disks {
|
|
disks[i] = DiskInfo{
|
|
Mountpoint: d.Mountpoint,
|
|
Device: d.Device,
|
|
Type: d.Type,
|
|
Total: d.Total,
|
|
Used: d.Used,
|
|
Free: d.Free,
|
|
Usage: d.Usage,
|
|
}
|
|
}
|
|
|
|
interfaces := make([]NetworkInterface, len(dh.NetworkInterfaces))
|
|
for i, iface := range dh.NetworkInterfaces {
|
|
interfaces[i] = NetworkInterface{
|
|
Name: iface.Name,
|
|
MAC: iface.MAC,
|
|
Addresses: iface.Addresses,
|
|
RXBytes: iface.RXBytes,
|
|
TXBytes: iface.TXBytes,
|
|
SpeedMbps: iface.SpeedMbps,
|
|
}
|
|
}
|
|
|
|
var swarm *DockerSwarmInfo
|
|
if dh.Swarm != nil {
|
|
swarm = &DockerSwarmInfo{
|
|
NodeID: dh.Swarm.NodeID,
|
|
NodeRole: dh.Swarm.NodeRole,
|
|
LocalState: dh.Swarm.LocalState,
|
|
ControlAvailable: dh.Swarm.ControlAvailable,
|
|
ClusterID: dh.Swarm.ClusterID,
|
|
ClusterName: dh.Swarm.ClusterName,
|
|
Scope: dh.Swarm.Scope,
|
|
Error: dh.Swarm.Error,
|
|
}
|
|
}
|
|
|
|
platformData := DockerHostPlatformData{
|
|
AgentID: dh.AgentID,
|
|
MachineID: dh.MachineID,
|
|
OS: dh.OS,
|
|
KernelVersion: dh.KernelVersion,
|
|
Architecture: dh.Architecture,
|
|
Runtime: dh.Runtime,
|
|
RuntimeVersion: dh.RuntimeVersion,
|
|
DockerVersion: dh.DockerVersion,
|
|
LoadAverage: dh.LoadAverage,
|
|
AgentVersion: dh.AgentVersion,
|
|
CPUs: dh.CPUs,
|
|
IsLegacy: dh.IsLegacy,
|
|
Disks: disks,
|
|
Interfaces: interfaces,
|
|
CustomDisplayName: dh.CustomDisplayName,
|
|
Hidden: dh.Hidden,
|
|
PendingUninstall: dh.PendingUninstall,
|
|
Swarm: swarm,
|
|
TokenID: dh.TokenID,
|
|
TokenName: dh.TokenName,
|
|
TokenHint: dh.TokenHint,
|
|
TokenLastUsedAt: dh.TokenLastUsedAt,
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
// Collect IPs for identity
|
|
var ips []string
|
|
for _, iface := range dh.NetworkInterfaces {
|
|
ips = append(ips, iface.Addresses...)
|
|
}
|
|
|
|
// Determine display name
|
|
displayName := dh.DisplayName
|
|
if dh.CustomDisplayName != "" {
|
|
displayName = dh.CustomDisplayName
|
|
}
|
|
|
|
// Determine cluster ID from swarm
|
|
clusterID := ""
|
|
if dh.Swarm != nil && dh.Swarm.ClusterID != "" {
|
|
clusterID = fmt.Sprintf("docker-swarm/%s", dh.Swarm.ClusterID)
|
|
}
|
|
|
|
return Resource{
|
|
ID: dh.ID,
|
|
Type: ResourceTypeDockerHost,
|
|
Name: dh.Hostname,
|
|
DisplayName: displayName,
|
|
PlatformID: dh.AgentID,
|
|
PlatformType: PlatformDocker,
|
|
SourceType: SourceAgent,
|
|
ClusterID: clusterID,
|
|
Status: mapDockerHostStatus(dh.Status),
|
|
CPU: &MetricValue{
|
|
Current: dh.CPUUsage,
|
|
},
|
|
Memory: memory,
|
|
Disk: disk,
|
|
Network: network,
|
|
Uptime: &dh.UptimeSeconds,
|
|
LastSeen: dh.LastSeen,
|
|
PlatformData: platformDataJSON,
|
|
Identity: &ResourceIdentity{
|
|
Hostname: dh.Hostname,
|
|
MachineID: dh.MachineID,
|
|
IPs: ips,
|
|
},
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// FromDockerContainer converts a DockerContainer to a unified Resource.
|
|
func FromDockerContainer(dc models.DockerContainer, hostID, hostName string) Resource {
|
|
// Build memory metric
|
|
memTotal := dc.MemoryLimit
|
|
memUsed := dc.MemoryUsage
|
|
var memory *MetricValue
|
|
if memTotal > 0 {
|
|
memory = &MetricValue{
|
|
Current: dc.MemoryPercent,
|
|
Total: &memTotal,
|
|
Used: &memUsed,
|
|
}
|
|
}
|
|
|
|
// Build platform data
|
|
ports := make([]ContainerPort, len(dc.Ports))
|
|
for i, p := range dc.Ports {
|
|
ports[i] = ContainerPort{
|
|
PrivatePort: p.PrivatePort,
|
|
PublicPort: p.PublicPort,
|
|
Protocol: p.Protocol,
|
|
IP: p.IP,
|
|
}
|
|
}
|
|
|
|
networks := make([]ContainerNetwork, len(dc.Networks))
|
|
for i, n := range dc.Networks {
|
|
networks[i] = ContainerNetwork{
|
|
Name: n.Name,
|
|
IPv4: n.IPv4,
|
|
IPv6: n.IPv6,
|
|
}
|
|
}
|
|
|
|
var podman *PodmanContainerInfo
|
|
if dc.Podman != nil {
|
|
podman = &PodmanContainerInfo{
|
|
PodName: dc.Podman.PodName,
|
|
PodID: dc.Podman.PodID,
|
|
Infra: dc.Podman.Infra,
|
|
ComposeProject: dc.Podman.ComposeProject,
|
|
ComposeService: dc.Podman.ComposeService,
|
|
}
|
|
}
|
|
|
|
platformData := DockerContainerPlatformData{
|
|
HostID: hostID,
|
|
HostName: hostName,
|
|
Image: dc.Image,
|
|
State: dc.State,
|
|
Status: dc.Status,
|
|
Health: dc.Health,
|
|
RestartCount: dc.RestartCount,
|
|
ExitCode: dc.ExitCode,
|
|
CreatedAt: dc.CreatedAt,
|
|
StartedAt: dc.StartedAt,
|
|
FinishedAt: dc.FinishedAt,
|
|
Labels: dc.Labels,
|
|
Ports: ports,
|
|
Networks: networks,
|
|
Podman: podman,
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
// Create unique ID combining host and container
|
|
resourceID := fmt.Sprintf("%s/%s", hostID, dc.ID)
|
|
|
|
return Resource{
|
|
ID: resourceID,
|
|
Type: ResourceTypeDockerContainer,
|
|
Name: dc.Name,
|
|
PlatformID: hostID,
|
|
PlatformType: PlatformDocker,
|
|
SourceType: SourceAgent,
|
|
ParentID: hostID,
|
|
Status: mapDockerContainerStatus(dc.State),
|
|
CPU: &MetricValue{
|
|
Current: dc.CPUPercent,
|
|
},
|
|
Memory: memory,
|
|
Uptime: &dc.UptimeSeconds,
|
|
Labels: dc.Labels,
|
|
LastSeen: time.Now(), // Containers don't have their own LastSeen
|
|
PlatformData: platformDataJSON,
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// FromPBSInstance converts a PBS instance to a unified Resource.
|
|
func FromPBSInstance(pbs models.PBSInstance) Resource {
|
|
// Build memory metric
|
|
var memory *MetricValue
|
|
if pbs.MemoryTotal > 0 {
|
|
memory = &MetricValue{
|
|
Current: pbs.Memory,
|
|
Total: &pbs.MemoryTotal,
|
|
Used: &pbs.MemoryUsed,
|
|
}
|
|
}
|
|
|
|
platformData := PBSPlatformData{
|
|
Host: pbs.Host,
|
|
Version: pbs.Version,
|
|
ConnectionHealth: pbs.ConnectionHealth,
|
|
MemoryUsed: pbs.MemoryUsed,
|
|
MemoryTotal: pbs.MemoryTotal,
|
|
NumDatastores: len(pbs.Datastores),
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
return Resource{
|
|
ID: pbs.ID,
|
|
Type: ResourceTypePBS,
|
|
Name: pbs.Name,
|
|
PlatformID: pbs.Host,
|
|
PlatformType: PlatformProxmoxPBS,
|
|
SourceType: SourceAPI,
|
|
Status: mapPBSStatus(pbs.Status, pbs.ConnectionHealth),
|
|
CPU: &MetricValue{
|
|
Current: pbs.CPU,
|
|
},
|
|
Memory: memory,
|
|
Uptime: &pbs.Uptime,
|
|
LastSeen: pbs.LastSeen,
|
|
PlatformData: platformDataJSON,
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// FromStorage converts a Proxmox Storage to a unified Resource.
|
|
func FromStorage(s models.Storage) Resource {
|
|
var disk *MetricValue
|
|
if s.Total > 0 {
|
|
disk = &MetricValue{
|
|
Current: s.Usage,
|
|
Total: &s.Total,
|
|
Used: &s.Used,
|
|
Free: &s.Free,
|
|
}
|
|
}
|
|
|
|
platformData := StoragePlatformData{
|
|
Instance: s.Instance,
|
|
Node: s.Node,
|
|
Nodes: s.Nodes,
|
|
Type: s.Type,
|
|
Content: s.Content,
|
|
Shared: s.Shared,
|
|
Enabled: s.Enabled,
|
|
Active: s.Active,
|
|
}
|
|
platformDataJSON, _ := json.Marshal(platformData)
|
|
|
|
status := StatusOnline
|
|
if !s.Active {
|
|
status = StatusOffline
|
|
} else if !s.Enabled {
|
|
status = StatusStopped
|
|
}
|
|
|
|
return Resource{
|
|
ID: s.ID,
|
|
Type: ResourceTypeStorage,
|
|
Name: s.Name,
|
|
PlatformID: s.Instance,
|
|
PlatformType: PlatformProxmoxPVE,
|
|
SourceType: SourceAPI,
|
|
ParentID: fmt.Sprintf("%s/node/%s", s.Instance, s.Node),
|
|
Status: status,
|
|
Disk: disk,
|
|
LastSeen: time.Now(), // Storage doesn't have LastSeen
|
|
PlatformData: platformDataJSON,
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
}
|
|
}
|
|
|
|
// Status mapping helpers
|
|
|
|
func mapNodeStatus(status string) ResourceStatus {
|
|
switch strings.ToLower(status) {
|
|
case "online":
|
|
return StatusOnline
|
|
case "offline":
|
|
return StatusOffline
|
|
default:
|
|
return StatusUnknown
|
|
}
|
|
}
|
|
|
|
func mapGuestStatus(status string) ResourceStatus {
|
|
switch strings.ToLower(status) {
|
|
case "running":
|
|
return StatusRunning
|
|
case "stopped":
|
|
return StatusStopped
|
|
case "paused":
|
|
return StatusPaused
|
|
default:
|
|
return StatusUnknown
|
|
}
|
|
}
|
|
|
|
func mapHostStatus(status string) ResourceStatus {
|
|
switch strings.ToLower(status) {
|
|
case "online":
|
|
return StatusOnline
|
|
case "offline":
|
|
return StatusOffline
|
|
case "degraded":
|
|
return StatusDegraded
|
|
default:
|
|
return StatusUnknown
|
|
}
|
|
}
|
|
|
|
func mapDockerHostStatus(status string) ResourceStatus {
|
|
switch strings.ToLower(status) {
|
|
case "online":
|
|
return StatusOnline
|
|
case "offline":
|
|
return StatusOffline
|
|
default:
|
|
return StatusUnknown
|
|
}
|
|
}
|
|
|
|
func mapDockerContainerStatus(state string) ResourceStatus {
|
|
switch strings.ToLower(state) {
|
|
case "running":
|
|
return StatusRunning
|
|
case "exited", "dead":
|
|
return StatusStopped
|
|
case "paused":
|
|
return StatusPaused
|
|
case "restarting", "created":
|
|
return StatusUnknown
|
|
default:
|
|
return StatusUnknown
|
|
}
|
|
}
|
|
|
|
func mapPBSStatus(status, connectionHealth string) ResourceStatus {
|
|
if connectionHealth != "healthy" {
|
|
return StatusDegraded
|
|
}
|
|
switch strings.ToLower(status) {
|
|
case "online":
|
|
return StatusOnline
|
|
case "offline":
|
|
return StatusOffline
|
|
default:
|
|
return StatusUnknown
|
|
}
|
|
}
|