mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 02:42:11 +00:00
223 lines
6.1 KiB
Go
223 lines
6.1 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/proxmox"
|
|
)
|
|
|
|
// diskPoolAssignment maps a disk's canonical key (lowercase, prefix-stripped) to
|
|
// its owning pool. Keys are derived from leaf-device names in ZFS pool trees and
|
|
// are consulted with several candidate forms of a physical disk's identity.
|
|
type diskPoolAssignment struct {
|
|
keyToPool map[string]string // normalised leaf name / by-id path → pool name
|
|
serialPool map[string]string // lowercase serial fragment → pool name (for by-id matches)
|
|
}
|
|
|
|
// buildDiskPoolAssignment flattens the ZFS pool trees returned by Proxmox and
|
|
// indexes every leaf device under several normalised keys so a physical disk
|
|
// can be matched regardless of whether zpool references it by /dev path,
|
|
// /dev/disk/by-id path, or partition name.
|
|
func buildDiskPoolAssignment(pools []proxmox.ZFSPoolInfo) *diskPoolAssignment {
|
|
assignment := &diskPoolAssignment{
|
|
keyToPool: make(map[string]string),
|
|
serialPool: make(map[string]string),
|
|
}
|
|
for _, pool := range pools {
|
|
if pool.Name == "" {
|
|
continue
|
|
}
|
|
for _, dev := range pool.Devices {
|
|
assignment.indexLeaves(dev, pool.Name)
|
|
}
|
|
}
|
|
return assignment
|
|
}
|
|
|
|
func (a *diskPoolAssignment) indexLeaves(dev proxmox.ZFSPoolDevice, poolName string) {
|
|
if dev.Leaf == 1 {
|
|
name := strings.TrimSpace(dev.Name)
|
|
if name != "" {
|
|
for _, key := range normaliseLeafKeys(name) {
|
|
if _, exists := a.keyToPool[key]; !exists {
|
|
a.keyToPool[key] = poolName
|
|
}
|
|
}
|
|
if serial := serialFromByID(name); serial != "" {
|
|
if _, exists := a.serialPool[serial]; !exists {
|
|
a.serialPool[serial] = poolName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, child := range dev.Children {
|
|
a.indexLeaves(child, poolName)
|
|
}
|
|
}
|
|
|
|
// lookup returns the pool name a disk belongs to, or "" if no match.
|
|
func (a *diskPoolAssignment) lookup(disk models.PhysicalDisk) string {
|
|
if a == nil {
|
|
return ""
|
|
}
|
|
for _, key := range diskLookupKeys(disk) {
|
|
if pool, ok := a.keyToPool[key]; ok {
|
|
return pool
|
|
}
|
|
}
|
|
serial := strings.ToLower(strings.TrimSpace(disk.Serial))
|
|
if serial != "" {
|
|
if pool, ok := a.serialPool[serial]; ok {
|
|
return pool
|
|
}
|
|
}
|
|
wwn := strings.ToLower(strings.TrimSpace(disk.WWN))
|
|
wwn = strings.TrimPrefix(wwn, "0x")
|
|
if wwn != "" {
|
|
if pool, ok := a.serialPool[wwn]; ok {
|
|
return pool
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// normaliseLeafKeys derives candidate lookup keys from a zpool leaf-device name.
|
|
// Keys are lowercased and stripped of common prefixes so they can match against
|
|
// physical-disk DevPath variants.
|
|
func normaliseLeafKeys(raw string) []string {
|
|
name := strings.ToLower(strings.TrimSpace(raw))
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
keys := map[string]struct{}{name: {}}
|
|
|
|
trimmed := strings.TrimPrefix(name, "/dev/")
|
|
trimmed = strings.TrimPrefix(trimmed, "disk/by-id/")
|
|
trimmed = strings.TrimPrefix(trimmed, "disk/by-path/")
|
|
trimmed = strings.TrimPrefix(trimmed, "disk/by-uuid/")
|
|
if trimmed != "" {
|
|
keys[trimmed] = struct{}{}
|
|
}
|
|
|
|
if base := stripPartitionSuffix(trimmed); base != "" {
|
|
keys[base] = struct{}{}
|
|
}
|
|
|
|
out := make([]string, 0, len(keys))
|
|
for k := range keys {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// diskLookupKeys derives candidate lookup keys from a physical disk's
|
|
// identifying fields. Order matters: the most specific matches come first.
|
|
func diskLookupKeys(disk models.PhysicalDisk) []string {
|
|
devPath := strings.ToLower(strings.TrimSpace(disk.DevPath))
|
|
if devPath == "" {
|
|
return nil
|
|
}
|
|
seen := map[string]struct{}{}
|
|
add := func(k string) {
|
|
if k == "" {
|
|
return
|
|
}
|
|
seen[k] = struct{}{}
|
|
}
|
|
|
|
add(devPath)
|
|
add(strings.TrimPrefix(devPath, "/dev/"))
|
|
base := strings.TrimPrefix(devPath, "/dev/")
|
|
add(stripPartitionSuffix(base))
|
|
|
|
out := make([]string, 0, len(seen))
|
|
for k := range seen {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// stripPartitionSuffix removes a trailing partition identifier. Handles both
|
|
// sdX style (trailing digits after an all-alphabetic prefix) and the
|
|
// p-separator style used by devices whose name already contains digits
|
|
// (nvme0n1p3, mmcblk0p1).
|
|
func stripPartitionSuffix(name string) string {
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
// p-separator form: the base name already contains a digit, and the
|
|
// partition is appended after a literal "p". nvme0n1p3 → nvme0n1.
|
|
if idx := strings.LastIndex(name, "p"); idx > 0 {
|
|
suffix := name[idx+1:]
|
|
if suffix != "" && allDigits(suffix) {
|
|
prev := name[:idx]
|
|
if len(prev) > 0 && isDigit(prev[len(prev)-1]) {
|
|
return prev
|
|
}
|
|
}
|
|
}
|
|
// sdX / hdX form: strip trailing digits only when the remaining prefix
|
|
// is all alphabetic. This avoids over-stripping device names that
|
|
// carry digits as part of their identifier (nvme0n1, mmcblk0).
|
|
i := len(name)
|
|
for i > 0 && isDigit(name[i-1]) {
|
|
i--
|
|
}
|
|
if i == len(name) || i == 0 {
|
|
return name
|
|
}
|
|
prefix := name[:i]
|
|
for j := 0; j < len(prefix); j++ {
|
|
if !isAlpha(prefix[j]) {
|
|
return name
|
|
}
|
|
}
|
|
return prefix
|
|
}
|
|
|
|
func isAlpha(b byte) bool {
|
|
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
|
}
|
|
|
|
// serialFromByID extracts a lowercase serial-like fragment from a by-id path.
|
|
// Proxmox commonly references ZFS members via /dev/disk/by-id/ata-MODEL_SERIAL
|
|
// or nvme-MODEL_SERIAL; the trailing token is the disk serial.
|
|
func serialFromByID(raw string) string {
|
|
name := strings.ToLower(strings.TrimSpace(raw))
|
|
name = strings.TrimPrefix(name, "/dev/")
|
|
name = strings.TrimPrefix(name, "disk/by-id/")
|
|
if !(strings.HasPrefix(name, "ata-") ||
|
|
strings.HasPrefix(name, "scsi-") ||
|
|
strings.HasPrefix(name, "nvme-") ||
|
|
strings.HasPrefix(name, "wwn-")) {
|
|
return ""
|
|
}
|
|
// Strip optional partition suffix like "-part1"
|
|
if idx := strings.LastIndex(name, "-part"); idx > 0 {
|
|
name = name[:idx]
|
|
}
|
|
// The last underscore-separated token is typically the serial.
|
|
if idx := strings.LastIndex(name, "_"); idx > 0 {
|
|
return name[idx+1:]
|
|
}
|
|
if strings.HasPrefix(name, "wwn-0x") {
|
|
return strings.TrimPrefix(name, "wwn-0x")
|
|
}
|
|
if strings.HasPrefix(name, "wwn-") {
|
|
return strings.TrimPrefix(name, "wwn-")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func allDigits(s string) bool {
|
|
for i := 0; i < len(s); i++ {
|
|
if !isDigit(s[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return len(s) > 0
|
|
}
|
|
|
|
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
|