add EXCLUDE_SMART env var (#1392)

This commit is contained in:
henrygd
2025-11-11 16:05:00 -05:00
parent aaa788bc2f
commit f64a361c60
2 changed files with 247 additions and 7 deletions

View File

@@ -24,11 +24,12 @@ import (
// SmartManager manages data collection for SMART devices
type SmartManager struct {
sync.Mutex
SmartDataMap map[string]*smart.SmartData
SmartDevices []*DeviceInfo
refreshMutex sync.Mutex
lastScanTime time.Time
binPath string
SmartDataMap map[string]*smart.SmartData
SmartDevices []*DeviceInfo
refreshMutex sync.Mutex
lastScanTime time.Time
binPath string
excludedDevices map[string]struct{}
}
type scanOutput struct {
@@ -185,6 +186,7 @@ func (sm *SmartManager) ScanDevices(force bool) error {
}
finalDevices := mergeDeviceLists(currentDevices, scannedDevices, configuredDevices)
finalDevices = sm.filterExcludedDevices(finalDevices)
sm.updateSmartDevices(finalDevices)
if len(finalDevices) == 0 {
@@ -232,6 +234,47 @@ func (sm *SmartManager) parseConfiguredDevices(config string) ([]*DeviceInfo, er
return devices, nil
}
func (sm *SmartManager) refreshExcludedDevices() {
rawValue, _ := GetEnv("EXCLUDE_SMART")
sm.excludedDevices = make(map[string]struct{})
for entry := range strings.SplitSeq(rawValue, ",") {
device := strings.TrimSpace(entry)
if device == "" {
continue
}
sm.excludedDevices[device] = struct{}{}
}
}
func (sm *SmartManager) isExcludedDevice(deviceName string) bool {
_, exists := sm.excludedDevices[deviceName]
return exists
}
func (sm *SmartManager) filterExcludedDevices(devices []*DeviceInfo) []*DeviceInfo {
if devices == nil {
return []*DeviceInfo{}
}
excluded := sm.excludedDevices
if len(excluded) == 0 {
return devices
}
filtered := make([]*DeviceInfo, 0, len(devices))
for _, device := range devices {
if device == nil || device.Name == "" {
continue
}
if _, skip := excluded[device.Name]; skip {
continue
}
filtered = append(filtered, device)
}
return filtered
}
// detectSmartOutputType inspects sections that are unique to each smartctl
// JSON schema (NVMe, ATA/SATA, SCSI) to determine which parser should be used
// when the reported device type is ambiguous or missing.
@@ -378,6 +421,10 @@ func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte)
// Uses -n standby to avoid waking up sleeping disks, but bypasses standby mode
// for initial data collection when no cached data exists
func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
if deviceInfo != nil && sm.isExcludedDevice(deviceInfo.Name) {
return errNoValidSmartData
}
// slog.Info("collecting SMART data", "device", deviceInfo.Name, "type", deviceInfo.Type, "has_existing_data", sm.hasDataForDevice(deviceInfo.Name))
// Check if we have any existing data for this device
@@ -409,10 +456,10 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
if !hasValidData {
if err != nil {
slog.Debug("smartctl failed", "device", deviceInfo.Name, "err", err)
slog.Info("smartctl failed", "device", deviceInfo.Name, "err", err)
return err
}
slog.Debug("no valid SMART data found", "device", deviceInfo.Name)
slog.Info("no valid SMART data found", "device", deviceInfo.Name)
return errNoValidSmartData
}
@@ -915,6 +962,7 @@ func NewSmartManager() (*SmartManager, error) {
sm := &SmartManager{
SmartDataMap: make(map[string]*smart.SmartData),
}
sm.refreshExcludedDevices()
path, err := sm.detectSmartctl()
if err != nil {
slog.Debug(err.Error())

View File

@@ -588,3 +588,195 @@ func TestIsVirtualDeviceScsi(t *testing.T) {
})
}
}
func TestRefreshExcludedDevices(t *testing.T) {
tests := []struct {
name string
envValue string
expectedDevs map[string]struct{}
}{
{
name: "empty env",
envValue: "",
expectedDevs: map[string]struct{}{},
},
{
name: "single device",
envValue: "/dev/sda",
expectedDevs: map[string]struct{}{
"/dev/sda": {},
},
},
{
name: "multiple devices",
envValue: "/dev/sda,/dev/sdb,/dev/nvme0",
expectedDevs: map[string]struct{}{
"/dev/sda": {},
"/dev/sdb": {},
"/dev/nvme0": {},
},
},
{
name: "devices with whitespace",
envValue: " /dev/sda , /dev/sdb , /dev/nvme0 ",
expectedDevs: map[string]struct{}{
"/dev/sda": {},
"/dev/sdb": {},
"/dev/nvme0": {},
},
},
{
name: "duplicate devices",
envValue: "/dev/sda,/dev/sdb,/dev/sda",
expectedDevs: map[string]struct{}{
"/dev/sda": {},
"/dev/sdb": {},
},
},
{
name: "empty entries and whitespace",
envValue: "/dev/sda,, /dev/sdb , , ",
expectedDevs: map[string]struct{}{
"/dev/sda": {},
"/dev/sdb": {},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envValue != "" {
t.Setenv("EXCLUDE_SMART", tt.envValue)
} else {
// Ensure env var is not set for empty test
os.Unsetenv("EXCLUDE_SMART")
}
sm := &SmartManager{}
sm.refreshExcludedDevices()
assert.Equal(t, tt.expectedDevs, sm.excludedDevices)
})
}
}
func TestIsExcludedDevice(t *testing.T) {
sm := &SmartManager{
excludedDevices: map[string]struct{}{
"/dev/sda": {},
"/dev/nvme0": {},
},
}
tests := []struct {
name string
deviceName string
expectedBool bool
}{
{"excluded device sda", "/dev/sda", true},
{"excluded device nvme0", "/dev/nvme0", true},
{"non-excluded device sdb", "/dev/sdb", false},
{"non-excluded device nvme1", "/dev/nvme1", false},
{"empty device name", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sm.isExcludedDevice(tt.deviceName)
assert.Equal(t, tt.expectedBool, result)
})
}
}
func TestFilterExcludedDevices(t *testing.T) {
tests := []struct {
name string
excludedDevs map[string]struct{}
inputDevices []*DeviceInfo
expectedDevs []*DeviceInfo
expectedLength int
}{
{
name: "no exclusions",
excludedDevs: map[string]struct{}{},
inputDevices: []*DeviceInfo{
{Name: "/dev/sda"},
{Name: "/dev/sdb"},
{Name: "/dev/nvme0"},
},
expectedDevs: []*DeviceInfo{
{Name: "/dev/sda"},
{Name: "/dev/sdb"},
{Name: "/dev/nvme0"},
},
expectedLength: 3,
},
{
name: "some devices excluded",
excludedDevs: map[string]struct{}{
"/dev/sda": {},
"/dev/nvme0": {},
},
inputDevices: []*DeviceInfo{
{Name: "/dev/sda"},
{Name: "/dev/sdb"},
{Name: "/dev/nvme0"},
{Name: "/dev/nvme1"},
},
expectedDevs: []*DeviceInfo{
{Name: "/dev/sdb"},
{Name: "/dev/nvme1"},
},
expectedLength: 2,
},
{
name: "all devices excluded",
excludedDevs: map[string]struct{}{
"/dev/sda": {},
"/dev/sdb": {},
},
inputDevices: []*DeviceInfo{
{Name: "/dev/sda"},
{Name: "/dev/sdb"},
},
expectedDevs: []*DeviceInfo{},
expectedLength: 0,
},
{
name: "nil devices",
excludedDevs: map[string]struct{}{},
inputDevices: nil,
expectedDevs: []*DeviceInfo{},
expectedLength: 0,
},
{
name: "filter nil and empty name devices",
excludedDevs: map[string]struct{}{
"/dev/sda": {},
},
inputDevices: []*DeviceInfo{
{Name: "/dev/sda"},
nil,
{Name: ""},
{Name: "/dev/sdb"},
},
expectedDevs: []*DeviceInfo{
{Name: "/dev/sdb"},
},
expectedLength: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := &SmartManager{
excludedDevices: tt.excludedDevs,
}
result := sm.filterExcludedDevices(tt.inputDevices)
assert.Len(t, result, tt.expectedLength)
assert.Equal(t, tt.expectedDevs, result)
})
}
}