go.d smartctl add "extra_devices" option (#18140)
Ilya Mashchenko committed
Jul 13, 2024 at 23:19 UTC
92b00666e19594d77b17fce91ef849ef9cbb3baf
10 files changed
+252
-19
src/go/plugin/go.d/modules/smartctl/collect.go
+3
-2
@@ -42,7 +42,8 @@ func (s *Smartctl) collect() (map[string]int64, error) {
42
// TODO: make it concurrent
43
for _, d := range s.scannedDevices {
44
if err := s.collectScannedDevice(mx, d); err != nil {
45
- return nil, err
45
+ s.Warning(err)
46
+ continue
47
}
48
}
49
@@ -57,7 +58,7 @@ func (s *Smartctl) collect() (map[string]int64, error) {
58
func (s *Smartctl) collectScannedDevice(mx map[string]int64, scanDev *scanDevice) error {
59
resp, err := s.exec.deviceInfo(scanDev.name, scanDev.typ, s.NoCheckPowerMode)
60
if err != nil {
60
- if resp != nil && isDeviceOpenFailedNoSuchDevice(resp) {
61
+ if resp != nil && isDeviceOpenFailedNoSuchDevice(resp) && !scanDev.extra {
62
s.Infof("smartctl reported that device '%s' type '%s' no longer exists", scanDev.name, scanDev.typ)
63
s.forceScan = true
64
return nil
src/go/plugin/go.d/modules/smartctl/config_schema.json
+43
-2
@@ -50,6 +50,36 @@
50
"type": "string",
51
"minimum": 1,
52
"default": "*"
53
+ },
54
+ "extra_devices": {
55
+ "title": "Extra devices",
56
+ "description": "Allows manual specification of devices not automatically detected by `smartctl --scan`. Each device entry must include both a name and a type.",
57
+ "type": [
58
+ "array",
59
+ "null"
60
+ ],
61
+ "uniqueItems": true,
62
+ "items": {
63
+ "title": "Device",
64
+ "type": [
65
+ "object",
66
+ "null"
67
+ ],
68
+ "required": [
69
+ "name",
70
+ "type"
71
+ ],
72
+ "properties": {
73
+ "name": {
74
+ "title": "Name",
75
+ "type": "string"
76
+ },
77
+ "type": {
78
+ "title": "Type",
79
+ "type": "string"
80
+ }
81
+ }
82
+ }
83
}
84
},
85
"additionalProperties": false,
@@ -75,9 +105,10 @@
105
]
106
},
107
{
78
- "title": "Filtering",
108
+ "title": "Devices",
109
"fields": [
80
- "device_selector"
110
+ "device_selector",
111
+ "extra_devices"
112
]
113
}
114
]
@@ -94,6 +125,16 @@
125
},
126
"device_selector": {
127
"ui:help": "Leave blank or use `*` to collect data for all devices."
128
+ },
129
+ "extra_devices": {
130
+ "items": {
131
+ "name": {
132
+ "ui:placeholder": "/dev/sda"
133
+ },
134
+ "type": {
135
+ "ui:placeholder": "jmb39x-q,3"
136
+ }
137
+ }
138
}
139
}
140
}
src/go/plugin/go.d/modules/smartctl/init.go
+7
@@ -17,6 +17,13 @@ func (s *Smartctl) validateConfig() error {
17
default:
18
return fmt.Errorf("invalid power mode '%s'", s.NoCheckPowerMode)
19
}
20
+
21
+ for _, v := range s.ExtraDevices {
22
+ if v.Name == "" || v.Type == "" {
23
+ return fmt.Errorf("invalid extra device: name and type must both be provided, got name='%s' type='%s'", v.Name, v.Type)
24
+ }
25
+ }
26
+
27
return nil
28
}
29
src/go/plugin/go.d/modules/smartctl/metadata.yaml
+13
@@ -108,6 +108,10 @@ modules:
108
description: "Specifies a pattern to match the 'info name' of devices as reported by `smartctl --scan --json`."
109
default_value: "*"
110
required: false
111
+ - name: extra_devices
112
+ description: "Allows manual specification of devices not automatically detected by `smartctl --scan`. Each device entry must include both a name and a type. See \"Configuration Examples\" for details."
113
+ default_value: "[]"
114
+ required: false
115
- name: no_check_power_mode
116
description: "Skip data collection when the device is in a low-power mode. Prevents unnecessary disk spin-up."
117
default_value: standby
@@ -132,6 +136,15 @@ modules:
136
jobs:
137
- name: smartctl
138
devices_poll_interval: 60 # Collect S.M.A.R.T statistics every 60 seconds
139
+ - name: Extra devices
140
+ description: |
141
+ This example demonstrates using `extra_devices` to manually add a storage device (`/dev/sdc`) not automatically detected by `smartctl --scan`.
142
+ config: |
143
+ jobs:
144
+ - name: smartctl
145
+ extra_devices:
146
+ - name: /dev/sdc
147
+ type: jmb39x-q,3
148
troubleshooting:
149
problems:
150
list: []
src/go/plugin/go.d/modules/smartctl/scan.go
+13
-2
@@ -12,6 +12,7 @@ type scanDevice struct {
12
name string
13
infoName string
14
typ string
15
+ extra bool // added via config "extra_devices"
16
}
17
18
func (s *scanDevice) key() string {
@@ -65,11 +66,21 @@ func (s *Smartctl) scanDevices() (map[string]*scanDevice, error) {
66
devices[dev.key()] = dev
67
}
68
69
+ s.Debugf("smartctl scan found %d devices", len(devices))
70
+
71
+ for _, v := range s.ExtraDevices {
72
+ if v.Name == "" || v.Type == "" {
73
+ continue
74
+ }
75
+ dev := &scanDevice{name: v.Name, typ: v.Type, extra: true}
76
+ if _, ok := devices[dev.key()]; !ok {
77
+ devices[dev.key()] = dev
78
+ }
79
+ }
80
+
81
if len(devices) == 0 {
82
return nil, errors.New("no devices found during scan")
83
}
84
72
- s.Debugf("smartctl scan found %d devices", len(devices))
73
-
85
return devices, nil
86
}
src/go/plugin/go.d/modules/smartctl/smartctl.go
+15
-8
@@ -43,14 +43,21 @@ func New() *Smartctl {
43
}
44
}
45
46
-type Config struct {
47
- UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
48
- Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
49
- ScanEvery web.Duration `yaml:"scan_every,omitempty" json:"scan_every"`
50
- PollDevicesEvery web.Duration `yaml:"poll_devices_every,omitempty" json:"poll_devices_every"`
51
- NoCheckPowerMode string `yaml:"no_check_power_mode,omitempty" json:"no_check_power_mode"`
52
- DeviceSelector string `yaml:"device_selector,omitempty" json:"device_selector"`
53
-}
46
+type (
47
+ Config struct {
48
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
49
+ Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
50
+ ScanEvery web.Duration `yaml:"scan_every,omitempty" json:"scan_every"`
51
+ PollDevicesEvery web.Duration `yaml:"poll_devices_every,omitempty" json:"poll_devices_every"`
52
+ NoCheckPowerMode string `yaml:"no_check_power_mode,omitempty" json:"no_check_power_mode"`
53
+ DeviceSelector string `yaml:"device_selector,omitempty" json:"device_selector"`
54
+ ExtraDevices []ConfigExtraDevice `yaml:"extra_devices,omitempty" json:"extra_devices"`
55
+ }
56
+ ConfigExtraDevice struct {
57
+ Name string `yaml:"name" json:"name"`
58
+ Type string `yaml:"type" json:"type"`
59
+ }
60
+)
61
62
type (
63
Smartctl struct {
src/go/plugin/go.d/modules/smartctl/smartctl_test.go
+34
-3
@@ -26,6 +26,7 @@ var (
26
27
dataTypeNvmeScan, _ = os.ReadFile("testdata/type-nvme/scan.json")
28
dataTypeNvmeDeviceNvme0, _ = os.ReadFile("testdata/type-nvme/device-nvme0.json")
29
+ dataTypeNvmeDeviceNvme1, _ = os.ReadFile("testdata/type-nvme/device-nvme1.json")
30
31
dataTypeScsiScan, _ = os.ReadFile("testdata/type-scsi/scan.json")
32
dataTypeScsiDeviceSda, _ = os.ReadFile("testdata/type-scsi/device-sda.json")
@@ -42,6 +43,7 @@ func Test_testDataIsValid(t *testing.T) {
43
44
"dataTypeNvmeScan": dataTypeNvmeScan,
45
"dataTypeNvmeDeviceNvme0": dataTypeNvmeDeviceNvme0,
46
+ "dataTypeNvmeDeviceNvme1": dataTypeNvmeDeviceNvme1,
47
48
"dataTypeScsiScan": dataTypeScsiScan,
49
"dataTypeScsiDeviceSda": dataTypeScsiDeviceSda,
@@ -166,9 +168,10 @@ func TestSmartctl_Check(t *testing.T) {
168
169
func TestSmartctl_Collect(t *testing.T) {
170
tests := map[string]struct {
169
- prepareMock func() *mockSmartctlCliExec
170
- wantMetrics map[string]int64
171
- wantCharts int
171
+ prepareMock func() *mockSmartctlCliExec
172
+ prepareConfig func() Config
173
+ wantMetrics map[string]int64
174
+ wantCharts int
175
}{
176
"success type sata devices": {
177
prepareMock: prepareMockOkTypeSata,
@@ -295,6 +298,29 @@ func TestSmartctl_Collect(t *testing.T) {
298
"device_nvme0_type_nvme_temperature": 39,
299
},
300
},
301
+ "success type nvme devices with extra": {
302
+ prepareMock: prepareMockOkTypeNvme,
303
+ prepareConfig: func() Config {
304
+ cfg := New().Config
305
+ cfg.ExtraDevices = []ConfigExtraDevice{
306
+ {Name: "/dev/nvme1", Type: "nvme"},
307
+ }
308
+ return cfg
309
+ },
310
+ wantCharts: 8,
311
+ wantMetrics: map[string]int64{
312
+ "device_nvme0_type_nvme_power_cycle_count": 2,
313
+ "device_nvme0_type_nvme_power_on_time": 11206800,
314
+ "device_nvme0_type_nvme_smart_status_failed": 0,
315
+ "device_nvme0_type_nvme_smart_status_passed": 1,
316
+ "device_nvme0_type_nvme_temperature": 39,
317
+ "device_nvme1_type_nvme_power_cycle_count": 5,
318
+ "device_nvme1_type_nvme_power_on_time": 17038800,
319
+ "device_nvme1_type_nvme_smart_status_failed": 0,
320
+ "device_nvme1_type_nvme_smart_status_passed": 1,
321
+ "device_nvme1_type_nvme_temperature": 36,
322
+ },
323
+ },
324
"success type scsi devices": {
325
prepareMock: prepareMockOkTypeScsi,
326
wantCharts: 7,
@@ -326,6 +352,9 @@ func TestSmartctl_Collect(t *testing.T) {
352
for name, test := range tests {
353
t.Run(name, func(t *testing.T) {
354
smart := New()
355
+ if test.prepareConfig != nil {
356
+ smart.Config = test.prepareConfig()
357
+ }
358
mock := test.prepareMock()
359
smart.exec = mock
360
smart.ScanEvery = web.Duration(time.Microsecond * 1)
@@ -390,6 +419,8 @@ func prepareMockOkTypeNvme() *mockSmartctlCliExec {
419
switch deviceName {
420
case "/dev/nvme0":
421
return dataTypeNvmeDeviceNvme0, nil
422
+ case "/dev/nvme1":
423
+ return dataTypeNvmeDeviceNvme1, nil
424
default:
425
return nil, fmt.Errorf("unexpected device name %s", deviceName)
426
}
src/go/plugin/go.d/modules/smartctl/testdata/config.json
+7
-1
@@ -4,5 +4,11 @@
4
"scan_every": 123.123,
5
"poll_devices_every": 123.123,
6
"no_check_power_mode": "ok",
7
- "device_selector": "ok"
7
+ "device_selector": "ok",
8
+ "extra_devices": [
9
+ {
10
+ "name": "ok",
11
+ "type": "ok"
12
+ }
13
+ ]
14
}
src/go/plugin/go.d/modules/smartctl/testdata/config.yaml
+4
-1
@@ -3,4 +3,7 @@ timeout: 123.123
3
scan_every: 123.123
4
poll_devices_every: 123.123
5
no_check_power_mode: "ok"
6
-"device_selector": "ok"
6
+device_selector: "ok"
7
+extra_devices:
8
+ - name: "ok"
9
+ type: "ok"
src/go/plugin/go.d/modules/smartctl/testdata/type-nvme/device-nvme1.json
new
+113
@@ -0,0 +1,113 @@
1
+{
2
+ "json_format_version": [
3
+ 1,
4
+ 0
5
+ ],
6
+ "smartctl": {
7
+ "version": [
8
+ 7,
9
+ 3
10
+ ],
11
+ "svn_revision": "5338",
12
+ "platform_info": "REDACTED",
13
+ "build_info": "(local build)",
14
+ "argv": [
15
+ "smartctl",
16
+ "--json",
17
+ "--all",
18
+ "/dev/nvme1",
19
+ "--device",
20
+ "nvme"
21
+ ],
22
+ "exit_status": 0
23
+ },
24
+ "local_time": {
25
+ "time_t": 1720897758,
26
+ "asctime": "Sat Jul 13 22:09:18 2024 EEST"
27
+ },
28
+ "device": {
29
+ "name": "/dev/nvme1",
30
+ "info_name": "/dev/nvme1",
31
+ "type": "nvme",
32
+ "protocol": "NVMe"
33
+ },
34
+ "model_name": "Seagate FireCuda 530 ZP4000GM30023",
35
+ "serial_number": "REDACTED",
36
+ "firmware_version": "REDACTED",
37
+ "nvme_pci_vendor": {
38
+ "id": 7089,
39
+ "subsystem_id": 7089
40
+ },
41
+ "nvme_ieee_oui_identifier": 6584743,
42
+ "nvme_total_capacity": 4000787030016,
43
+ "nvme_unallocated_capacity": 0,
44
+ "nvme_controller_id": 1,
45
+ "nvme_version": {
46
+ "string": "1.4",
47
+ "value": 66560
48
+ },
49
+ "nvme_number_of_namespaces": 1,
50
+ "nvme_namespaces": [
51
+ {
52
+ "id": 1,
53
+ "size": {
54
+ "blocks": 7814037168,
55
+ "bytes": 4000787030016
56
+ },
57
+ "capacity": {
58
+ "blocks": 7814037168,
59
+ "bytes": 4000787030016
60
+ },
61
+ "utilization": {
62
+ "blocks": 7814037168,
63
+ "bytes": 4000787030016
64
+ },
65
+ "formatted_lba_size": 512,
66
+ "eui64": {
67
+ "oui": 6584743,
68
+ "ext_id": 553497146765
69
+ }
70
+ }
71
+ ],
72
+ "user_capacity": {
73
+ "blocks": 7814037168,
74
+ "bytes": 4000787030016
75
+ },
76
+ "logical_block_size": 512,
77
+ "smart_support": {
78
+ "available": true,
79
+ "enabled": true
80
+ },
81
+ "smart_status": {
82
+ "passed": true,
83
+ "nvme": {
84
+ "value": 0
85
+ }
86
+ },
87
+ "nvme_smart_health_information_log": {
88
+ "critical_warning": 0,
89
+ "temperature": 36,
90
+ "available_spare": 100,
91
+ "available_spare_threshold": 5,
92
+ "percentage_used": 0,
93
+ "data_units_read": 202,
94
+ "data_units_written": 0,
95
+ "host_reads": 2509,
96
+ "host_writes": 0,
97
+ "controller_busy_time": 0,
98
+ "power_cycles": 5,
99
+ "power_on_hours": 4733,
100
+ "unsafe_shutdowns": 2,
101
+ "media_errors": 0,
102
+ "num_err_log_entries": 20,
103
+ "warning_temp_time": 0,
104
+ "critical_comp_time": 0
105
+ },
106
+ "temperature": {
107
+ "current": 36
108
+ },
109
+ "power_cycle_count": 5,
110
+ "power_on_time": {
111
+ "hours": 4733
112
+ }
113
+}