mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
674ca18968
The PVE disks/list poll and the agent SMART report both observe a RAID controller's exported volume, but PVE surfaces its NAA identifier as a bare-hex serial while smartctl reports the same value as a naa.-prefixed WWN with no serial. The linked-disk join compared serial to serial and WWN to WWN verbatim, so the volume rendered twice (/dev/sda and sda) with independent metric histories. Hardware identity comparison now normalizes reporter framing (naa., eui., wwn-, 0x prefixes, case) and folds serial and WWN together before comparing. Values are never truncated: sibling volumes on one controller share their leading WWN bytes, and a truncated udev ID_WWN must stay unequal to a full identifier. Placeholder serials no longer count as an identity match. Refs #1720
129 lines
4.1 KiB
Go
129 lines
4.1 KiB
Go
package diskinventory
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// DeviceToken returns the kernel block-device token from either a canonical
|
|
// /dev path or a legacy smartctl display label such as "sda [scsi]".
|
|
func DeviceToken(device string) string {
|
|
device = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(device), "/dev/"))
|
|
if fields := strings.Fields(device); len(fields) > 0 {
|
|
device = fields[0]
|
|
}
|
|
return strings.TrimSpace(device)
|
|
}
|
|
|
|
// PreferredID selects stable hardware identity first and scopes topology
|
|
// fallbacks to the reporting host. Controller and target are included only
|
|
// when present, preserving legacy direct-device IDs for SATA/NVMe disks.
|
|
func PreferredID(serial, wwn, scope, device, controller, target string) string {
|
|
if serial = strings.TrimSpace(serial); IsUsableHardwareID(serial) {
|
|
return serial
|
|
}
|
|
if wwn = strings.TrimSpace(wwn); IsUsableHardwareID(wwn) {
|
|
return wwn
|
|
}
|
|
|
|
scope = strings.TrimSpace(scope)
|
|
device = DeviceToken(device)
|
|
controller = strings.TrimSpace(controller)
|
|
target = strings.TrimSpace(target)
|
|
if scope == "" || device == "" {
|
|
return ""
|
|
}
|
|
if !IsControllerMemberTarget(target) {
|
|
return fmt.Sprintf("%s:%s", scope, device)
|
|
}
|
|
return fmt.Sprintf("%s:%s@%s/%s", scope, device, controller, target)
|
|
}
|
|
|
|
// IsUsableHardwareID rejects controller placeholders that are not unique disk
|
|
// identities. Treating these as real serials collapses different disks and
|
|
// sends their SMART history to the same metric key.
|
|
func IsUsableHardwareID(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return false
|
|
}
|
|
upper := strings.ToUpper(value)
|
|
switch upper {
|
|
case "UNKNOWN", "N/A", "NA", "NONE", "NULL", "DEFAULT", "DEFAULT-SERIAL",
|
|
"TO BE FILLED BY O.E.M.", "0123456789":
|
|
return false
|
|
}
|
|
compact := strings.NewReplacer("-", "", ":", "", ".", "", " ", "").Replace(upper)
|
|
if compact == "" {
|
|
return false
|
|
}
|
|
allZero := true
|
|
allF := true
|
|
for _, char := range compact {
|
|
allZero = allZero && char == '0'
|
|
allF = allF && char == 'F'
|
|
}
|
|
return !allZero && !allF
|
|
}
|
|
|
|
// normalizeHardwareID canonicalizes a serial or WWN for cross-source
|
|
// comparison. Reporters disagree on framing, not identity: smartctl emits
|
|
// naa./eui.-prefixed WWNs, udev emits wwn-0x tokens, and PVE surfaces bare
|
|
// hex. Values are only ever prefix-stripped and case-folded, never truncated:
|
|
// sibling volumes on one RAID controller share their leading WWN bytes, so a
|
|
// truncated form must stay unequal to the full identifier.
|
|
func normalizeHardwareID(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if !IsUsableHardwareID(value) {
|
|
return ""
|
|
}
|
|
for {
|
|
trimmed := value
|
|
for _, prefix := range []string{"naa.", "eui.", "wwn-", "0x"} {
|
|
trimmed = strings.TrimPrefix(trimmed, prefix)
|
|
}
|
|
if trimmed == value {
|
|
break
|
|
}
|
|
value = trimmed
|
|
}
|
|
if !IsUsableHardwareID(value) {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
// HardwareIdentityMatch reports whether two disk observations carry the same
|
|
// stable hardware identity. Serial and WWN are folded together because
|
|
// sources disagree on which field holds the durable identifier: PVE reports a
|
|
// RAID array volume's NAA identifier as its serial while smartctl reports the
|
|
// same value as a naa.-prefixed WWN with no serial at all.
|
|
func HardwareIdentityMatch(leftSerial, leftWWN, rightSerial, rightWWN string) bool {
|
|
left := [2]string{normalizeHardwareID(leftSerial), normalizeHardwareID(leftWWN)}
|
|
right := [2]string{normalizeHardwareID(rightSerial), normalizeHardwareID(rightWWN)}
|
|
for _, l := range left {
|
|
if l == "" {
|
|
continue
|
|
}
|
|
for _, r := range right {
|
|
if r != "" && l == r {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsControllerMemberTarget reports whether target addresses one member behind
|
|
// a shared controller block path. Controller grammars vary after the numeric
|
|
// member prefix (for example megaraid,7, areca,1/1, and sssraid,0,1).
|
|
func IsControllerMemberTarget(target string) bool {
|
|
target = strings.TrimSpace(target)
|
|
index := strings.IndexByte(target, ',')
|
|
if index < 0 || index+1 >= len(target) {
|
|
return false
|
|
}
|
|
next := target[index+1]
|
|
return next >= '0' && next <= '9'
|
|
}
|