Pulse/internal/resources/converters.go
rcourtman 5f000b7974 feat: Complete Unified Resource Architecture (Phases 1-3)
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.
2025-12-07 13:49:00 +00:00

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