Improve LXC guest metrics visibility (#596)
This commit is contained in:
parent
e1fe8354e9
commit
aac3dacd63
6 changed files with 747 additions and 66 deletions
|
|
@ -181,6 +181,15 @@ func (c Container) ToFrontend() ContainerFrontend {
|
||||||
ct.Disks = c.Disks
|
ct.Disks = c.Disks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(c.IPAddresses) > 0 {
|
||||||
|
ct.IPAddresses = append([]string(nil), c.IPAddresses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.NetworkInterfaces) > 0 {
|
||||||
|
ct.NetworkInterfaces = make([]GuestNetworkInterface, len(c.NetworkInterfaces))
|
||||||
|
copy(ct.NetworkInterfaces, c.NetworkInterfaces)
|
||||||
|
}
|
||||||
|
|
||||||
return ct
|
return ct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,6 +242,68 @@ func (d DockerHost) ToFrontend() DockerHostFrontend {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToFrontend converts a Host to HostFrontend.
|
||||||
|
func (h Host) ToFrontend() HostFrontend {
|
||||||
|
host := HostFrontend{
|
||||||
|
ID: h.ID,
|
||||||
|
Hostname: h.Hostname,
|
||||||
|
DisplayName: h.DisplayName,
|
||||||
|
Platform: h.Platform,
|
||||||
|
OSName: h.OSName,
|
||||||
|
OSVersion: h.OSVersion,
|
||||||
|
KernelVersion: h.KernelVersion,
|
||||||
|
Architecture: h.Architecture,
|
||||||
|
CPUCount: h.CPUCount,
|
||||||
|
CPUUsage: h.CPUUsage,
|
||||||
|
Status: h.Status,
|
||||||
|
UptimeSeconds: h.UptimeSeconds,
|
||||||
|
IntervalSeconds: h.IntervalSeconds,
|
||||||
|
AgentVersion: h.AgentVersion,
|
||||||
|
TokenID: h.TokenID,
|
||||||
|
TokenName: h.TokenName,
|
||||||
|
TokenHint: h.TokenHint,
|
||||||
|
Tags: append([]string(nil), h.Tags...),
|
||||||
|
LastSeen: h.LastSeen.Unix() * 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
if host.DisplayName == "" {
|
||||||
|
if h.DisplayName != "" {
|
||||||
|
host.DisplayName = h.DisplayName
|
||||||
|
} else if h.Hostname != "" {
|
||||||
|
host.DisplayName = h.Hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(h.LoadAverage) > 0 {
|
||||||
|
host.LoadAverage = append([]float64(nil), h.LoadAverage...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h.Memory != Memory{}) {
|
||||||
|
mem := h.Memory
|
||||||
|
host.Memory = &mem
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(h.Disks) > 0 {
|
||||||
|
host.Disks = append([]Disk(nil), h.Disks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(h.NetworkInterfaces) > 0 {
|
||||||
|
host.NetworkInterfaces = make([]HostNetworkInterface, len(h.NetworkInterfaces))
|
||||||
|
copy(host.NetworkInterfaces, h.NetworkInterfaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := hostSensorSummaryToFrontend(h.Sensors); s != nil {
|
||||||
|
host.Sensors = s
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.TokenLastUsedAt != nil && !h.TokenLastUsedAt.IsZero() {
|
||||||
|
ts := h.TokenLastUsedAt.Unix() * 1000
|
||||||
|
host.TokenLastUsedAt = &ts
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
// ToFrontend converts a DockerContainer to DockerContainerFrontend
|
// ToFrontend converts a DockerContainer to DockerContainerFrontend
|
||||||
func (c DockerContainer) ToFrontend() DockerContainerFrontend {
|
func (c DockerContainer) ToFrontend() DockerContainerFrontend {
|
||||||
container := DockerContainerFrontend{
|
container := DockerContainerFrontend{
|
||||||
|
|
@ -291,6 +362,35 @@ func (c DockerContainer) ToFrontend() DockerContainerFrontend {
|
||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hostSensorSummaryToFrontend(src HostSensorSummary) *HostSensorSummaryFrontend {
|
||||||
|
if len(src.TemperatureCelsius) == 0 && len(src.FanRPM) == 0 && len(src.Additional) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := &HostSensorSummaryFrontend{}
|
||||||
|
if len(src.TemperatureCelsius) > 0 {
|
||||||
|
dest.TemperatureCelsius = copyStringFloatMap(src.TemperatureCelsius)
|
||||||
|
}
|
||||||
|
if len(src.FanRPM) > 0 {
|
||||||
|
dest.FanRPM = copyStringFloatMap(src.FanRPM)
|
||||||
|
}
|
||||||
|
if len(src.Additional) > 0 {
|
||||||
|
dest.Additional = copyStringFloatMap(src.Additional)
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyStringFloatMap(src map[string]float64) map[string]float64 {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dest := make(map[string]float64, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
dest[k] = v
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
func toDockerHostCommandFrontend(cmd DockerHostCommandStatus) *DockerHostCommandFrontend {
|
func toDockerHostCommandFrontend(cmd DockerHostCommandStatus) *DockerHostCommandFrontend {
|
||||||
result := &DockerHostCommandFrontend{
|
result := &DockerHostCommandFrontend{
|
||||||
ID: cmd.ID,
|
ID: cmd.ID,
|
||||||
|
|
|
||||||
|
|
@ -116,28 +116,30 @@ type VM struct {
|
||||||
|
|
||||||
// Container represents an LXC container
|
// Container represents an LXC container
|
||||||
type Container struct {
|
type Container struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
VMID int `json:"vmid"`
|
VMID int `json:"vmid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Node string `json:"node"`
|
Node string `json:"node"`
|
||||||
Instance string `json:"instance"`
|
Instance string `json:"instance"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
CPU float64 `json:"cpu"`
|
CPU float64 `json:"cpu"`
|
||||||
CPUs int `json:"cpus"`
|
CPUs int `json:"cpus"`
|
||||||
Memory Memory `json:"memory"`
|
Memory Memory `json:"memory"`
|
||||||
Disk Disk `json:"disk"`
|
Disk Disk `json:"disk"`
|
||||||
Disks []Disk `json:"disks,omitempty"`
|
Disks []Disk `json:"disks,omitempty"`
|
||||||
NetworkIn int64 `json:"networkIn"`
|
NetworkIn int64 `json:"networkIn"`
|
||||||
NetworkOut int64 `json:"networkOut"`
|
NetworkOut int64 `json:"networkOut"`
|
||||||
DiskRead int64 `json:"diskRead"`
|
DiskRead int64 `json:"diskRead"`
|
||||||
DiskWrite int64 `json:"diskWrite"`
|
DiskWrite int64 `json:"diskWrite"`
|
||||||
Uptime int64 `json:"uptime"`
|
Uptime int64 `json:"uptime"`
|
||||||
Template bool `json:"template"`
|
Template bool `json:"template"`
|
||||||
LastBackup time.Time `json:"lastBackup,omitempty"`
|
LastBackup time.Time `json:"lastBackup,omitempty"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
Lock string `json:"lock,omitempty"`
|
Lock string `json:"lock,omitempty"`
|
||||||
LastSeen time.Time `json:"lastSeen"`
|
LastSeen time.Time `json:"lastSeen"`
|
||||||
|
IPAddresses []string `json:"ipAddresses,omitempty"`
|
||||||
|
NetworkInterfaces []GuestNetworkInterface `json:"networkInterfaces,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host represents a generic infrastructure host reporting via external agents.
|
// Host represents a generic infrastructure host reporting via external agents.
|
||||||
|
|
|
||||||
|
|
@ -67,30 +67,32 @@ type VMFrontend struct {
|
||||||
|
|
||||||
// ContainerFrontend represents a Container with frontend-friendly field names
|
// ContainerFrontend represents a Container with frontend-friendly field names
|
||||||
type ContainerFrontend struct {
|
type ContainerFrontend struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
VMID int `json:"vmid"`
|
VMID int `json:"vmid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Node string `json:"node"`
|
Node string `json:"node"`
|
||||||
Instance string `json:"instance"`
|
Instance string `json:"instance"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
CPU float64 `json:"cpu"`
|
CPU float64 `json:"cpu"`
|
||||||
CPUs int `json:"cpus"`
|
CPUs int `json:"cpus"`
|
||||||
Memory *Memory `json:"memory,omitempty"` // Full memory object
|
Memory *Memory `json:"memory,omitempty"` // Full memory object
|
||||||
Mem int64 `json:"mem"` // Maps to Memory.Used
|
Mem int64 `json:"mem"` // Maps to Memory.Used
|
||||||
MaxMem int64 `json:"maxmem"` // Maps to Memory.Total
|
MaxMem int64 `json:"maxmem"` // Maps to Memory.Total
|
||||||
DiskObj *Disk `json:"disk,omitempty"` // Full disk object
|
DiskObj *Disk `json:"disk,omitempty"` // Full disk object
|
||||||
Disks []Disk `json:"disks,omitempty"` // Individual filesystem/disk usage
|
Disks []Disk `json:"disks,omitempty"` // Individual filesystem/disk usage
|
||||||
NetIn int64 `json:"networkIn"` // Maps to NetworkIn (camelCase for frontend)
|
NetworkInterfaces []GuestNetworkInterface `json:"networkInterfaces,omitempty"`
|
||||||
NetOut int64 `json:"networkOut"` // Maps to NetworkOut (camelCase for frontend)
|
IPAddresses []string `json:"ipAddresses,omitempty"`
|
||||||
DiskRead int64 `json:"diskRead"` // Maps to DiskRead (camelCase for frontend)
|
NetIn int64 `json:"networkIn"` // Maps to NetworkIn (camelCase for frontend)
|
||||||
DiskWrite int64 `json:"diskWrite"` // Maps to DiskWrite (camelCase for frontend)
|
NetOut int64 `json:"networkOut"` // Maps to NetworkOut (camelCase for frontend)
|
||||||
Uptime int64 `json:"uptime"`
|
DiskRead int64 `json:"diskRead"` // Maps to DiskRead (camelCase for frontend)
|
||||||
Template bool `json:"template"`
|
DiskWrite int64 `json:"diskWrite"` // Maps to DiskWrite (camelCase for frontend)
|
||||||
LastBackup int64 `json:"lastBackup,omitempty"` // Unix timestamp
|
Uptime int64 `json:"uptime"`
|
||||||
Tags string `json:"tags,omitempty"` // Joined string
|
Template bool `json:"template"`
|
||||||
Lock string `json:"lock,omitempty"`
|
LastBackup int64 `json:"lastBackup,omitempty"` // Unix timestamp
|
||||||
LastSeen int64 `json:"lastSeen"` // Unix timestamp
|
Tags string `json:"tags,omitempty"` // Joined string
|
||||||
|
Lock string `json:"lock,omitempty"`
|
||||||
|
LastSeen int64 `json:"lastSeen"` // Unix timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerHostFrontend represents a Docker host with frontend-friendly fields
|
// DockerHostFrontend represents a Docker host with frontend-friendly fields
|
||||||
|
|
@ -174,6 +176,42 @@ type DockerHostCommandFrontend struct {
|
||||||
ExpiresAt *int64 `json:"expiresAt,omitempty"`
|
ExpiresAt *int64 `json:"expiresAt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostFrontend represents a generic infrastructure host exposed to the UI.
|
||||||
|
type HostFrontend struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
OSName string `json:"osName,omitempty"`
|
||||||
|
OSVersion string `json:"osVersion,omitempty"`
|
||||||
|
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||||
|
Architecture string `json:"architecture,omitempty"`
|
||||||
|
CPUCount int `json:"cpuCount,omitempty"`
|
||||||
|
CPUUsage float64 `json:"cpuUsage,omitempty"`
|
||||||
|
LoadAverage []float64 `json:"loadAverage,omitempty"`
|
||||||
|
Memory *Memory `json:"memory,omitempty"`
|
||||||
|
Disks []Disk `json:"disks,omitempty"`
|
||||||
|
NetworkInterfaces []HostNetworkInterface `json:"networkInterfaces,omitempty"`
|
||||||
|
Sensors *HostSensorSummaryFrontend `json:"sensors,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
UptimeSeconds int64 `json:"uptimeSeconds,omitempty"`
|
||||||
|
LastSeen int64 `json:"lastSeen"`
|
||||||
|
IntervalSeconds int `json:"intervalSeconds,omitempty"`
|
||||||
|
AgentVersion string `json:"agentVersion,omitempty"`
|
||||||
|
TokenID string `json:"tokenId,omitempty"`
|
||||||
|
TokenName string `json:"tokenName,omitempty"`
|
||||||
|
TokenHint string `json:"tokenHint,omitempty"`
|
||||||
|
TokenLastUsedAt *int64 `json:"tokenLastUsedAt,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostSensorSummaryFrontend mirrors HostSensorSummary with primitives for the frontend.
|
||||||
|
type HostSensorSummaryFrontend struct {
|
||||||
|
TemperatureCelsius map[string]float64 `json:"temperatureCelsius,omitempty"`
|
||||||
|
FanRPM map[string]float64 `json:"fanRpm,omitempty"`
|
||||||
|
Additional map[string]float64 `json:"additional,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// StorageFrontend represents Storage with frontend-friendly field names
|
// StorageFrontend represents Storage with frontend-friendly field names
|
||||||
type StorageFrontend struct {
|
type StorageFrontend struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
@ -263,6 +301,7 @@ type StateFrontend struct {
|
||||||
VMs []VMFrontend `json:"vms"`
|
VMs []VMFrontend `json:"vms"`
|
||||||
Containers []ContainerFrontend `json:"containers"`
|
Containers []ContainerFrontend `json:"containers"`
|
||||||
DockerHosts []DockerHostFrontend `json:"dockerHosts"`
|
DockerHosts []DockerHostFrontend `json:"dockerHosts"`
|
||||||
|
Hosts []HostFrontend `json:"hosts"`
|
||||||
Storage []StorageFrontend `json:"storage"`
|
Storage []StorageFrontend `json:"storage"`
|
||||||
CephClusters []CephClusterFrontend `json:"cephClusters"`
|
CephClusters []CephClusterFrontend `json:"cephClusters"`
|
||||||
PhysicalDisks []PhysicalDisk `json:"physicalDisks"`
|
PhysicalDisks []PhysicalDisk `json:"physicalDisks"`
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
"github.com/rcourtman/pulse-go-rewrite/internal/notifications"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||||
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
|
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
|
||||||
|
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
||||||
|
|
@ -509,6 +510,17 @@ func safeFloat(val float64) float64 {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cloneStringFloatMap(src map[string]float64) map[string]float64 {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]float64, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// shouldRunBackupPoll determines whether a backup polling cycle should execute.
|
// shouldRunBackupPoll determines whether a backup polling cycle should execute.
|
||||||
// Returns whether polling should run, a human-readable skip reason, and the timestamp to record.
|
// Returns whether polling should run, a human-readable skip reason, and the timestamp to record.
|
||||||
func (m *Monitor) shouldRunBackupPoll(last time.Time, now time.Time) (bool, string, time.Time) {
|
func (m *Monitor) shouldRunBackupPoll(last time.Time, now time.Time) (bool, string, time.Time) {
|
||||||
|
|
@ -547,6 +559,7 @@ func (m *Monitor) shouldRunBackupPoll(last time.Time, now time.Time) (bool, stri
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dockerConnectionPrefix = "docker-"
|
dockerConnectionPrefix = "docker-"
|
||||||
|
hostConnectionPrefix = "host-"
|
||||||
dockerOfflineGraceMultiplier = 4
|
dockerOfflineGraceMultiplier = 4
|
||||||
dockerMinimumHealthWindow = 30 * time.Second
|
dockerMinimumHealthWindow = 30 * time.Second
|
||||||
dockerMaximumHealthWindow = 10 * time.Minute
|
dockerMaximumHealthWindow = 10 * time.Minute
|
||||||
|
|
@ -1266,6 +1279,170 @@ func (m *Monitor) ApplyDockerReport(report agentsdocker.Report, tokenRecord *con
|
||||||
return host, nil
|
return host, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyHostReport ingests a host agent report into the shared state.
|
||||||
|
func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config.APITokenRecord) (models.Host, error) {
|
||||||
|
hostname := strings.TrimSpace(report.Host.Hostname)
|
||||||
|
if hostname == "" {
|
||||||
|
return models.Host{}, fmt.Errorf("host report missing hostname")
|
||||||
|
}
|
||||||
|
|
||||||
|
identifier := strings.TrimSpace(report.Host.ID)
|
||||||
|
if identifier != "" {
|
||||||
|
identifier = sanitizeDockerHostSuffix(identifier)
|
||||||
|
}
|
||||||
|
if identifier == "" {
|
||||||
|
if machine := sanitizeDockerHostSuffix(report.Host.MachineID); machine != "" {
|
||||||
|
identifier = machine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if identifier == "" {
|
||||||
|
if agentID := sanitizeDockerHostSuffix(report.Agent.ID); agentID != "" {
|
||||||
|
identifier = agentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if identifier == "" {
|
||||||
|
if hostName := sanitizeDockerHostSuffix(hostname); hostName != "" {
|
||||||
|
identifier = hostName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if identifier == "" {
|
||||||
|
seedParts := uniqueNonEmptyStrings(
|
||||||
|
report.Host.MachineID,
|
||||||
|
report.Agent.ID,
|
||||||
|
report.Host.Hostname,
|
||||||
|
)
|
||||||
|
if len(seedParts) == 0 {
|
||||||
|
seedParts = []string{hostname}
|
||||||
|
}
|
||||||
|
seed := strings.Join(seedParts, "|")
|
||||||
|
sum := sha1.Sum([]byte(seed))
|
||||||
|
identifier = fmt.Sprintf("host-%s", hex.EncodeToString(sum[:6]))
|
||||||
|
}
|
||||||
|
|
||||||
|
existingHosts := m.state.GetHosts()
|
||||||
|
var previous models.Host
|
||||||
|
var hasPrevious bool
|
||||||
|
for _, candidate := range existingHosts {
|
||||||
|
if candidate.ID == identifier {
|
||||||
|
previous = candidate
|
||||||
|
hasPrevious = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName := strings.TrimSpace(report.Host.DisplayName)
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := report.Timestamp
|
||||||
|
if timestamp.IsZero() {
|
||||||
|
timestamp = time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
memory := models.Memory{
|
||||||
|
Total: report.Metrics.Memory.TotalBytes,
|
||||||
|
Used: report.Metrics.Memory.UsedBytes,
|
||||||
|
Free: report.Metrics.Memory.FreeBytes,
|
||||||
|
Usage: safeFloat(report.Metrics.Memory.Usage),
|
||||||
|
SwapTotal: report.Metrics.Memory.SwapTotal,
|
||||||
|
SwapUsed: report.Metrics.Memory.SwapUsed,
|
||||||
|
}
|
||||||
|
if memory.Usage <= 0 && memory.Total > 0 {
|
||||||
|
memory.Usage = safePercentage(float64(memory.Used), float64(memory.Total))
|
||||||
|
}
|
||||||
|
|
||||||
|
disks := make([]models.Disk, 0, len(report.Disks))
|
||||||
|
for _, disk := range report.Disks {
|
||||||
|
usage := safeFloat(disk.Usage)
|
||||||
|
if usage <= 0 && disk.TotalBytes > 0 {
|
||||||
|
usage = safePercentage(float64(disk.UsedBytes), float64(disk.TotalBytes))
|
||||||
|
}
|
||||||
|
disks = append(disks, models.Disk{
|
||||||
|
Total: disk.TotalBytes,
|
||||||
|
Used: disk.UsedBytes,
|
||||||
|
Free: disk.FreeBytes,
|
||||||
|
Usage: usage,
|
||||||
|
Mountpoint: disk.Mountpoint,
|
||||||
|
Type: disk.Type,
|
||||||
|
Device: disk.Device,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
network := make([]models.HostNetworkInterface, 0, len(report.Network))
|
||||||
|
for _, nic := range report.Network {
|
||||||
|
network = append(network, models.HostNetworkInterface{
|
||||||
|
Name: nic.Name,
|
||||||
|
MAC: nic.MAC,
|
||||||
|
Addresses: append([]string(nil), nic.Addresses...),
|
||||||
|
RXBytes: nic.RXBytes,
|
||||||
|
TXBytes: nic.TXBytes,
|
||||||
|
SpeedMbps: nic.SpeedMbps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
host := models.Host{
|
||||||
|
ID: identifier,
|
||||||
|
Hostname: hostname,
|
||||||
|
DisplayName: displayName,
|
||||||
|
Platform: strings.TrimSpace(strings.ToLower(report.Host.Platform)),
|
||||||
|
OSName: strings.TrimSpace(report.Host.OSName),
|
||||||
|
OSVersion: strings.TrimSpace(report.Host.OSVersion),
|
||||||
|
KernelVersion: strings.TrimSpace(report.Host.KernelVersion),
|
||||||
|
Architecture: strings.TrimSpace(report.Host.Architecture),
|
||||||
|
CPUCount: report.Host.CPUCount,
|
||||||
|
CPUUsage: safeFloat(report.Metrics.CPUUsagePercent),
|
||||||
|
LoadAverage: append([]float64(nil), report.Host.LoadAverage...),
|
||||||
|
Memory: memory,
|
||||||
|
Disks: disks,
|
||||||
|
NetworkInterfaces: network,
|
||||||
|
Sensors: models.HostSensorSummary{
|
||||||
|
TemperatureCelsius: cloneStringFloatMap(report.Sensors.TemperatureCelsius),
|
||||||
|
FanRPM: cloneStringFloatMap(report.Sensors.FanRPM),
|
||||||
|
Additional: cloneStringFloatMap(report.Sensors.Additional),
|
||||||
|
},
|
||||||
|
Status: "online",
|
||||||
|
UptimeSeconds: report.Host.UptimeSeconds,
|
||||||
|
IntervalSeconds: report.Agent.IntervalSeconds,
|
||||||
|
LastSeen: timestamp,
|
||||||
|
AgentVersion: strings.TrimSpace(report.Agent.Version),
|
||||||
|
Tags: append([]string(nil), report.Tags...),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(host.LoadAverage) == 0 {
|
||||||
|
host.LoadAverage = nil
|
||||||
|
}
|
||||||
|
if len(host.Disks) == 0 {
|
||||||
|
host.Disks = nil
|
||||||
|
}
|
||||||
|
if len(host.NetworkInterfaces) == 0 {
|
||||||
|
host.NetworkInterfaces = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenRecord != nil {
|
||||||
|
host.TokenID = tokenRecord.ID
|
||||||
|
host.TokenName = tokenRecord.Name
|
||||||
|
host.TokenHint = tokenHintFromRecord(tokenRecord)
|
||||||
|
if tokenRecord.LastUsedAt != nil {
|
||||||
|
t := tokenRecord.LastUsedAt.UTC()
|
||||||
|
host.TokenLastUsedAt = &t
|
||||||
|
} else {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
host.TokenLastUsedAt = &now
|
||||||
|
}
|
||||||
|
} else if hasPrevious {
|
||||||
|
host.TokenID = previous.TokenID
|
||||||
|
host.TokenName = previous.TokenName
|
||||||
|
host.TokenHint = previous.TokenHint
|
||||||
|
host.TokenLastUsedAt = previous.TokenLastUsedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
m.state.UpsertHost(host)
|
||||||
|
m.state.SetConnectionHealth(hostConnectionPrefix+host.ID, true)
|
||||||
|
|
||||||
|
return host, nil
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
removedDockerHostsTTL = 24 * time.Hour // Clean up removed hosts tracking after 24 hours
|
removedDockerHostsTTL = 24 * time.Hour // Clean up removed hosts tracking after 24 hours
|
||||||
)
|
)
|
||||||
|
|
@ -1659,6 +1836,336 @@ func anyToInt64(val interface{}) int64 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) enrichContainerMetadata(ctx context.Context, client PVEClientInterface, instanceName, nodeName string, container *models.Container) {
|
||||||
|
if container == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureContainerRootDiskEntry(container)
|
||||||
|
|
||||||
|
if client == nil || container.Status != "running" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
status, err := client.GetContainerStatus(statusCtx, nodeName, container.VMID)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().
|
||||||
|
Err(err).
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Str("node", nodeName).
|
||||||
|
Str("container", container.Name).
|
||||||
|
Int("vmid", container.VMID).
|
||||||
|
Msg("Container status metadata unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addressSet := make(map[string]struct{})
|
||||||
|
addressOrder := make([]string, 0, 4)
|
||||||
|
|
||||||
|
addAddress := func(addr string) {
|
||||||
|
addr = strings.TrimSpace(addr)
|
||||||
|
if addr == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, exists := addressSet[addr]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addressSet[addr] = struct{}{}
|
||||||
|
addressOrder = append(addressOrder, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range sanitizeGuestAddressStrings(status.IP) {
|
||||||
|
addAddress(addr)
|
||||||
|
}
|
||||||
|
for _, addr := range sanitizeGuestAddressStrings(status.IP6) {
|
||||||
|
addAddress(addr)
|
||||||
|
}
|
||||||
|
for _, addr := range parseContainerRawIPs(status.IPv4) {
|
||||||
|
addAddress(addr)
|
||||||
|
}
|
||||||
|
for _, addr := range parseContainerRawIPs(status.IPv6) {
|
||||||
|
addAddress(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
networkIfaces := make([]models.GuestNetworkInterface, 0, len(status.Network))
|
||||||
|
for rawName, cfg := range status.Network {
|
||||||
|
if cfg == (proxmox.ContainerNetworkConfig{}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
iface := models.GuestNetworkInterface{}
|
||||||
|
name := strings.TrimSpace(cfg.Name)
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(rawName)
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
iface.Name = name
|
||||||
|
}
|
||||||
|
if mac := strings.TrimSpace(cfg.HWAddr); mac != "" {
|
||||||
|
iface.MAC = mac
|
||||||
|
}
|
||||||
|
|
||||||
|
addrCandidates := make([]string, 0, 4)
|
||||||
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IP)...)
|
||||||
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IP6)...)
|
||||||
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IPv4)...)
|
||||||
|
addrCandidates = append(addrCandidates, collectIPsFromInterface(cfg.IPv6)...)
|
||||||
|
|
||||||
|
if len(addrCandidates) > 0 {
|
||||||
|
deduped := dedupeStringsPreserveOrder(addrCandidates)
|
||||||
|
if len(deduped) > 0 {
|
||||||
|
iface.Addresses = deduped
|
||||||
|
for _, addr := range deduped {
|
||||||
|
addAddress(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.Name != "" || iface.MAC != "" || len(iface.Addresses) > 0 {
|
||||||
|
networkIfaces = append(networkIfaces, iface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(networkIfaces) > 1 {
|
||||||
|
sort.SliceStable(networkIfaces, func(i, j int) bool {
|
||||||
|
left := strings.TrimSpace(networkIfaces[i].Name)
|
||||||
|
right := strings.TrimSpace(networkIfaces[j].Name)
|
||||||
|
return left < right
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(addressOrder) > 1 {
|
||||||
|
sort.Strings(addressOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(addressOrder) > 0 {
|
||||||
|
container.IPAddresses = addressOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(networkIfaces) > 0 {
|
||||||
|
container.NetworkInterfaces = networkIfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
if disks := convertContainerDiskInfo(status); len(disks) > 0 {
|
||||||
|
container.Disks = disks
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureContainerRootDiskEntry(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureContainerRootDiskEntry(container *models.Container) {
|
||||||
|
if container == nil || len(container.Disks) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total := container.Disk.Total
|
||||||
|
used := container.Disk.Used
|
||||||
|
if total <= 0 && used <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if total > 0 && used > total {
|
||||||
|
used = total
|
||||||
|
}
|
||||||
|
|
||||||
|
free := total - used
|
||||||
|
if free < 0 {
|
||||||
|
free = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
usage := container.Disk.Usage
|
||||||
|
if total > 0 && usage <= 0 {
|
||||||
|
usage = safePercentage(float64(used), float64(total))
|
||||||
|
}
|
||||||
|
|
||||||
|
container.Disks = []models.Disk{
|
||||||
|
{
|
||||||
|
Total: total,
|
||||||
|
Used: used,
|
||||||
|
Free: free,
|
||||||
|
Usage: usage,
|
||||||
|
Mountpoint: "/",
|
||||||
|
Type: "rootfs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertContainerDiskInfo(status *proxmox.Container) []models.Disk {
|
||||||
|
if status == nil || len(status.DiskInfo) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
disks := make([]models.Disk, 0, len(status.DiskInfo))
|
||||||
|
for name, info := range status.DiskInfo {
|
||||||
|
total := clampToInt64(info.Total)
|
||||||
|
used := clampToInt64(info.Used)
|
||||||
|
if total <= 0 && used <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if total > 0 && used > total {
|
||||||
|
used = total
|
||||||
|
}
|
||||||
|
free := total - used
|
||||||
|
if free < 0 {
|
||||||
|
free = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
disk := models.Disk{
|
||||||
|
Total: total,
|
||||||
|
Used: used,
|
||||||
|
Free: free,
|
||||||
|
}
|
||||||
|
|
||||||
|
if total > 0 {
|
||||||
|
disk.Usage = safePercentage(float64(used), float64(total))
|
||||||
|
}
|
||||||
|
|
||||||
|
label := strings.TrimSpace(name)
|
||||||
|
if strings.EqualFold(label, "rootfs") || label == "" {
|
||||||
|
disk.Mountpoint = "/"
|
||||||
|
disk.Type = "rootfs"
|
||||||
|
if device := sanitizeRootFSDevice(status.RootFS); device != "" {
|
||||||
|
disk.Device = device
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disk.Mountpoint = label
|
||||||
|
disk.Type = strings.ToLower(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
disks = append(disks, disk)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(disks) > 1 {
|
||||||
|
sort.SliceStable(disks, func(i, j int) bool {
|
||||||
|
return disks[i].Mountpoint < disks[j].Mountpoint
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return disks
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeRootFSDevice(root string) string {
|
||||||
|
root = strings.TrimSpace(root)
|
||||||
|
if root == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if idx := strings.Index(root, ","); idx != -1 {
|
||||||
|
root = root[:idx]
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseContainerRawIPs(raw json.RawMessage) []string {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var data interface{}
|
||||||
|
if err := json.Unmarshal(raw, &data); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return collectIPsFromInterface(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectIPsFromInterface(value interface{}) []string {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
return sanitizeGuestAddressStrings(v)
|
||||||
|
case []interface{}:
|
||||||
|
results := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
results = append(results, collectIPsFromInterface(item)...)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
case []string:
|
||||||
|
results := make([]string, 0, len(v))
|
||||||
|
for _, item := range v {
|
||||||
|
results = append(results, sanitizeGuestAddressStrings(item)...)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
case map[string]interface{}:
|
||||||
|
results := make([]string, 0)
|
||||||
|
for _, key := range []string{"ip", "ip6", "ipv4", "ipv6", "address", "value"} {
|
||||||
|
if val, ok := v[key]; ok {
|
||||||
|
results = append(results, collectIPsFromInterface(val)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
case json.Number:
|
||||||
|
return sanitizeGuestAddressStrings(v.String())
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeGuestAddressStrings(value string) []string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lower := strings.ToLower(value)
|
||||||
|
switch lower {
|
||||||
|
case "dhcp", "manual", "static", "auto", "none", "n/a", "unknown", "0.0.0.0", "::", "::1":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.FieldsFunc(value, func(r rune) bool {
|
||||||
|
return unicode.IsSpace(r) || r == ',' || r == ';'
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(parts) > 1 {
|
||||||
|
results := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
results = append(results, sanitizeGuestAddressStrings(part)...)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "127.") || strings.HasPrefix(strings.ToLower(value), "0.0.0.0") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lower = strings.ToLower(value)
|
||||||
|
if strings.HasPrefix(lower, "fe80") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(lower, "::1") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeStringsPreserveOrder(values []string) []string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(values))
|
||||||
|
result := make([]string, 0, len(values))
|
||||||
|
for _, v := range values {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// GetConnectionStatuses returns the current connection status for all nodes
|
// GetConnectionStatuses returns the current connection status for all nodes
|
||||||
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
func (m *Monitor) GetConnectionStatuses() map[string]bool {
|
||||||
if mock.IsMockEnabled() {
|
if mock.IsMockEnabled() {
|
||||||
|
|
@ -5185,6 +5692,8 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.enrichContainerMetadata(ctx, client, instanceName, res.Node, &container)
|
||||||
|
|
||||||
allContainers = append(allContainers, container)
|
allContainers = append(allContainers, container)
|
||||||
|
|
||||||
// For non-running containers, zero out resource usage metrics to prevent false alerts
|
// For non-running containers, zero out resource usage metrics to prevent false alerts
|
||||||
|
|
|
||||||
|
|
@ -975,6 +975,8 @@ func (m *Monitor) pollContainersWithNodes(ctx context.Context, instanceName stri
|
||||||
modelContainer.Disk.Usage = safePercentage(float64(modelContainer.Disk.Used), float64(modelContainer.Disk.Total))
|
modelContainer.Disk.Usage = safePercentage(float64(modelContainer.Disk.Used), float64(modelContainer.Disk.Total))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.enrichContainerMetadata(ctx, client, instanceName, n.Node, &modelContainer)
|
||||||
|
|
||||||
// Zero out metrics for non-running containers
|
// Zero out metrics for non-running containers
|
||||||
if container.Status != "running" {
|
if container.Status != "running" {
|
||||||
modelContainer.CPU = 0
|
modelContainer.CPU = 0
|
||||||
|
|
|
||||||
|
|
@ -734,26 +734,55 @@ type VM struct {
|
||||||
|
|
||||||
// Container represents a Proxmox VE LXC container
|
// Container represents a Proxmox VE LXC container
|
||||||
type Container struct {
|
type Container struct {
|
||||||
VMID FlexInt `json:"vmid"` // Changed to FlexInt to handle string VMIDs from some Proxmox versions
|
VMID FlexInt `json:"vmid"` // Changed to FlexInt to handle string VMIDs from some Proxmox versions
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Node string `json:"node"`
|
Node string `json:"node"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CPU float64 `json:"cpu"`
|
CPU float64 `json:"cpu"`
|
||||||
CPUs FlexInt `json:"cpus"`
|
CPUs FlexInt `json:"cpus"`
|
||||||
Mem uint64 `json:"mem"`
|
Mem uint64 `json:"mem"`
|
||||||
MaxMem uint64 `json:"maxmem"`
|
MaxMem uint64 `json:"maxmem"`
|
||||||
Swap uint64 `json:"swap"`
|
Swap uint64 `json:"swap"`
|
||||||
MaxSwap uint64 `json:"maxswap"`
|
MaxSwap uint64 `json:"maxswap"`
|
||||||
Disk uint64 `json:"disk"`
|
Disk uint64 `json:"disk"`
|
||||||
MaxDisk uint64 `json:"maxdisk"`
|
MaxDisk uint64 `json:"maxdisk"`
|
||||||
NetIn uint64 `json:"netin"`
|
NetIn uint64 `json:"netin"`
|
||||||
NetOut uint64 `json:"netout"`
|
NetOut uint64 `json:"netout"`
|
||||||
DiskRead uint64 `json:"diskread"`
|
DiskRead uint64 `json:"diskread"`
|
||||||
DiskWrite uint64 `json:"diskwrite"`
|
DiskWrite uint64 `json:"diskwrite"`
|
||||||
Uptime uint64 `json:"uptime"`
|
Uptime uint64 `json:"uptime"`
|
||||||
Template int `json:"template"`
|
Template int `json:"template"`
|
||||||
Tags string `json:"tags"`
|
Tags string `json:"tags"`
|
||||||
Lock string `json:"lock"`
|
Lock string `json:"lock"`
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
IP string `json:"ip,omitempty"`
|
||||||
|
IP6 string `json:"ip6,omitempty"`
|
||||||
|
IPv4 json.RawMessage `json:"ipv4,omitempty"`
|
||||||
|
IPv6 json.RawMessage `json:"ipv6,omitempty"`
|
||||||
|
Network map[string]ContainerNetworkConfig `json:"network,omitempty"`
|
||||||
|
DiskInfo map[string]ContainerDiskUsage `json:"diskinfo,omitempty"`
|
||||||
|
RootFS string `json:"rootfs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerNetworkConfig captures basic container network status information.
|
||||||
|
type ContainerNetworkConfig struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
HWAddr string `json:"hwaddr,omitempty"`
|
||||||
|
Bridge string `json:"bridge,omitempty"`
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
IP interface{} `json:"ip,omitempty"`
|
||||||
|
IP6 interface{} `json:"ip6,omitempty"`
|
||||||
|
IPv4 interface{} `json:"ipv4,omitempty"`
|
||||||
|
IPv6 interface{} `json:"ipv6,omitempty"`
|
||||||
|
Firewall interface{} `json:"firewall,omitempty"`
|
||||||
|
Tag interface{} `json:"tag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerDiskUsage captures disk usage details returned by the LXC status API.
|
||||||
|
type ContainerDiskUsage struct {
|
||||||
|
Total uint64 `json:"total,omitempty"`
|
||||||
|
Used uint64 `json:"used,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage represents a Proxmox VE storage
|
// Storage represents a Proxmox VE storage
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue