mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
230 lines
6 KiB
Go
230 lines
6 KiB
Go
package proxmox
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// GetZFSPoolsWithDetails gets both the list and detailed info for all ZFS pools on a node
|
|
// This combines the list and detail endpoints to get complete information
|
|
func (c *Client) GetZFSPoolsWithDetails(ctx context.Context, node string) ([]ZFSPoolInfo, error) {
|
|
// First get the list of pools
|
|
pools, err := c.GetZFSPoolStatus(ctx, node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list ZFS pools: %w", err)
|
|
}
|
|
|
|
// Now get details for each pool
|
|
var poolInfos []ZFSPoolInfo
|
|
for _, pool := range pools {
|
|
info := ZFSPoolInfo{
|
|
Name: pool.Name,
|
|
Health: pool.Health,
|
|
Size: pool.Size,
|
|
Alloc: pool.Alloc,
|
|
Free: pool.Free,
|
|
Frag: pool.Frag,
|
|
Dedup: pool.Dedup,
|
|
}
|
|
|
|
// Try to get detailed info, but don't fail if it's not available
|
|
detail, err := c.GetZFSPoolDetail(ctx, node, pool.Name)
|
|
if err != nil {
|
|
log.Debug().
|
|
Err(err).
|
|
Str("node", node).
|
|
Str("pool", pool.Name).
|
|
Msg("Could not get ZFS pool details, using basic info only")
|
|
// Continue with basic info
|
|
} else {
|
|
info.State = detail.State
|
|
info.Status = detail.Status
|
|
info.Scan = detail.Scan
|
|
info.Errors = detail.Errors
|
|
info.Devices = detail.Children
|
|
}
|
|
|
|
poolInfos = append(poolInfos, info)
|
|
}
|
|
|
|
return poolInfos, nil
|
|
}
|
|
|
|
// ZFSPoolInfo combines list and detail info for a complete picture
|
|
type ZFSPoolInfo struct {
|
|
// From list endpoint
|
|
Name string `json:"name"`
|
|
Health string `json:"health"`
|
|
Size uint64 `json:"size"`
|
|
Alloc uint64 `json:"alloc"`
|
|
Free uint64 `json:"free"`
|
|
Frag int `json:"frag"`
|
|
Dedup float64 `json:"dedup"`
|
|
|
|
// From detail endpoint (may be empty if not available)
|
|
State string `json:"state,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Scan string `json:"scan,omitempty"`
|
|
Errors string `json:"errors,omitempty"`
|
|
Devices []ZFSPoolDevice `json:"devices,omitempty"`
|
|
}
|
|
|
|
// ConvertToModelZFSPool converts the combined pool info to our model
|
|
func (p *ZFSPoolInfo) ConvertToModelZFSPool() *ZFSPool {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
|
|
// Use State if available, otherwise fall back to Health
|
|
state := p.State
|
|
if state == "" {
|
|
state = p.Health
|
|
}
|
|
|
|
pool := &ZFSPool{
|
|
Name: p.Name,
|
|
State: state,
|
|
Health: p.Health,
|
|
Status: p.Status,
|
|
Scan: p.Scan,
|
|
Errors: p.Errors,
|
|
}
|
|
|
|
// Extract error counts from devices if available
|
|
pool.Devices = make([]ZFSDevice, 0)
|
|
for _, dev := range p.Devices {
|
|
pool.Devices = append(pool.Devices, convertDeviceRecursive(dev, "")...)
|
|
}
|
|
|
|
// Calculate total errors from all devices
|
|
for _, dev := range pool.Devices {
|
|
pool.ReadErrors += dev.ReadErrors
|
|
pool.WriteErrors += dev.WriteErrors
|
|
pool.ChecksumErrors += dev.ChecksumErrors
|
|
}
|
|
|
|
return pool
|
|
}
|
|
|
|
// ZFSPool represents complete ZFS pool information for monitoring
|
|
type ZFSPool struct {
|
|
Name string `json:"name"`
|
|
State string `json:"state"`
|
|
Health string `json:"health"`
|
|
Status string `json:"status"`
|
|
Scan string `json:"scan"`
|
|
Errors string `json:"errors"`
|
|
ReadErrors int64 `json:"readErrors"`
|
|
WriteErrors int64 `json:"writeErrors"`
|
|
ChecksumErrors int64 `json:"checksumErrors"`
|
|
Devices []ZFSDevice `json:"devices"`
|
|
}
|
|
|
|
// ZFSDevice represents a device in the pool (flattened from tree structure)
|
|
type ZFSDevice struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
State string `json:"state"`
|
|
ReadErrors int64 `json:"readErrors"`
|
|
WriteErrors int64 `json:"writeErrors"`
|
|
ChecksumErrors int64 `json:"checksumErrors"`
|
|
IsLeaf bool `json:"isLeaf"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// convertDeviceRecursive flattens the device tree into a list
|
|
func convertDeviceRecursive(dev ZFSPoolDevice, parentRole string) []ZFSDevice {
|
|
var devices []ZFSDevice
|
|
|
|
name := strings.TrimSpace(dev.Name)
|
|
lowerName := strings.ToLower(name)
|
|
|
|
role := parentRole
|
|
if role == "" {
|
|
switch {
|
|
case lowerName == "logs" || lowerName == "log" || strings.HasPrefix(lowerName, "slog"):
|
|
role = "log"
|
|
case lowerName == "cache" || strings.HasPrefix(lowerName, "l2arc"):
|
|
role = "cache"
|
|
case lowerName == "spares" || strings.HasPrefix(lowerName, "spare"):
|
|
role = "spare"
|
|
}
|
|
}
|
|
|
|
state := strings.ToUpper(strings.TrimSpace(dev.State))
|
|
if state == "" {
|
|
state = "UNKNOWN"
|
|
}
|
|
|
|
isVdev := dev.Leaf == 0 && len(dev.Children) > 0
|
|
|
|
deviceType := "disk"
|
|
if isVdev {
|
|
deviceType = "vdev"
|
|
}
|
|
|
|
switch {
|
|
case isVdev && (lowerName == "mirror" || strings.HasPrefix(lowerName, "mirror")):
|
|
deviceType = "mirror"
|
|
case isVdev && strings.HasPrefix(lowerName, "raidz"):
|
|
deviceType = lowerName // raidz, raidz2, raidz3
|
|
case isVdev && role == "log":
|
|
deviceType = "log"
|
|
case isVdev && role == "cache":
|
|
deviceType = "cache"
|
|
case isVdev && role == "spare":
|
|
deviceType = "spare"
|
|
case role == "log" && dev.Leaf == 1:
|
|
deviceType = "log"
|
|
case role == "cache" && dev.Leaf == 1:
|
|
deviceType = "cache"
|
|
}
|
|
|
|
isSpare := lowerName == "spares" || strings.HasPrefix(lowerName, "spare")
|
|
if isSpare {
|
|
if dev.Leaf == 1 {
|
|
deviceType = "spare"
|
|
} else {
|
|
deviceType = "spare-group"
|
|
}
|
|
}
|
|
|
|
healthyStates := map[string]bool{
|
|
"ONLINE": true,
|
|
"SPARE": true,
|
|
"AVAIL": true,
|
|
"INUSE": true,
|
|
}
|
|
|
|
if state == "UNKNOWN" {
|
|
if role == "log" || role == "cache" || role == "spare" || isSpare {
|
|
healthyStates[state] = true
|
|
}
|
|
}
|
|
|
|
// Add this device if it has errors or is not healthy (but skip healthy spares)
|
|
if !healthyStates[state] || dev.Read > 0 || dev.Write > 0 || dev.Cksum > 0 {
|
|
message := strings.TrimSpace(dev.Msg)
|
|
|
|
devices = append(devices, ZFSDevice{
|
|
Name: name,
|
|
Type: deviceType,
|
|
State: state,
|
|
ReadErrors: dev.Read,
|
|
WriteErrors: dev.Write,
|
|
ChecksumErrors: dev.Cksum,
|
|
IsLeaf: dev.Leaf == 1,
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
// Process children
|
|
for _, child := range dev.Children {
|
|
devices = append(devices, convertDeviceRecursive(child, role)...)
|
|
}
|
|
|
|
return devices
|
|
}
|