go.d/powervault: add Dell PowerVault ME4/ME5 storage array collector (#21936)
Co-authored-by: ilyam8 <ilya@netdata.cloud>
Costa Tsaousis committed
Mar 19, 2026 at 00:28 UTC
94f65cead6d9df92845145ada7e58afb803bf6ab
34 files changed
+3611
src/go/plugin/go.d/collector/init.go
+1
@@ -91,6 +91,7 @@ import (
91
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/powerdns"
92
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/powerdns_recursor"
93
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/powerstore"
94
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/powervault"
95
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/prometheus"
96
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/proxysql"
97
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/pulsar"
src/go/plugin/go.d/collector/powervault/charts.yaml
new
+526
@@ -0,0 +1,526 @@
1
+version: v1
2
+context_namespace: powervault
3
+groups:
4
+ - family: System Health
5
+ metrics:
6
+ - system_health
7
+ charts:
8
+ - id: system_health
9
+ title: System Health Status
10
+ context: system_health
11
+ units: status
12
+ algorithm: absolute
13
+ dimensions:
14
+ - selector: system_health
15
+ name: health
16
+
17
+ - family: Hardware Health
18
+ metrics:
19
+ - hw_controller_ok
20
+ - hw_controller_degraded
21
+ - hw_controller_fault
22
+ - hw_controller_unknown
23
+ - hw_drive_ok
24
+ - hw_drive_degraded
25
+ - hw_drive_fault
26
+ - hw_drive_unknown
27
+ - hw_fan_ok
28
+ - hw_fan_degraded
29
+ - hw_fan_fault
30
+ - hw_fan_unknown
31
+ - hw_psu_ok
32
+ - hw_psu_degraded
33
+ - hw_psu_fault
34
+ - hw_psu_unknown
35
+ - hw_fru_ok
36
+ - hw_fru_degraded
37
+ - hw_fru_fault
38
+ - hw_fru_unknown
39
+ - hw_port_ok
40
+ - hw_port_degraded
41
+ - hw_port_fault
42
+ - hw_port_unknown
43
+ charts:
44
+ - id: hw_health_controller
45
+ title: Controller Health Status
46
+ context: hw_health_controller
47
+ units: controllers
48
+ type: stacked
49
+ algorithm: absolute
50
+ dimensions:
51
+ - selector: hw_controller_ok
52
+ name: ok
53
+ - selector: hw_controller_degraded
54
+ name: degraded
55
+ - selector: hw_controller_fault
56
+ name: fault
57
+ - selector: hw_controller_unknown
58
+ name: unknown
59
+ - id: hw_health_drive
60
+ title: Drive Health Status
61
+ context: hw_health_drive
62
+ units: drives
63
+ type: stacked
64
+ algorithm: absolute
65
+ dimensions:
66
+ - selector: hw_drive_ok
67
+ name: ok
68
+ - selector: hw_drive_degraded
69
+ name: degraded
70
+ - selector: hw_drive_fault
71
+ name: fault
72
+ - selector: hw_drive_unknown
73
+ name: unknown
74
+ - id: hw_health_fan
75
+ title: Fan Health Status
76
+ context: hw_health_fan
77
+ units: fans
78
+ type: stacked
79
+ algorithm: absolute
80
+ dimensions:
81
+ - selector: hw_fan_ok
82
+ name: ok
83
+ - selector: hw_fan_degraded
84
+ name: degraded
85
+ - selector: hw_fan_fault
86
+ name: fault
87
+ - selector: hw_fan_unknown
88
+ name: unknown
89
+ - id: hw_health_psu
90
+ title: Power Supply Health Status
91
+ context: hw_health_psu
92
+ units: PSUs
93
+ type: stacked
94
+ algorithm: absolute
95
+ dimensions:
96
+ - selector: hw_psu_ok
97
+ name: ok
98
+ - selector: hw_psu_degraded
99
+ name: degraded
100
+ - selector: hw_psu_fault
101
+ name: fault
102
+ - selector: hw_psu_unknown
103
+ name: unknown
104
+ - id: hw_health_fru
105
+ title: FRU Health Status
106
+ context: hw_health_fru
107
+ units: FRUs
108
+ type: stacked
109
+ algorithm: absolute
110
+ dimensions:
111
+ - selector: hw_fru_ok
112
+ name: ok
113
+ - selector: hw_fru_degraded
114
+ name: degraded
115
+ - selector: hw_fru_fault
116
+ name: fault
117
+ - selector: hw_fru_unknown
118
+ name: unknown
119
+ - id: hw_health_port
120
+ title: Port Health Status
121
+ context: hw_health_port
122
+ units: ports
123
+ type: stacked
124
+ algorithm: absolute
125
+ dimensions:
126
+ - selector: hw_port_ok
127
+ name: ok
128
+ - selector: hw_port_degraded
129
+ name: degraded
130
+ - selector: hw_port_fault
131
+ name: fault
132
+ - selector: hw_port_unknown
133
+ name: unknown
134
+
135
+ - family: Controller
136
+ metrics:
137
+ - controller_iops
138
+ - controller_throughput
139
+ - controller_cpu_load
140
+ - controller_write_cache_used
141
+ - controller_forwarded_cmds
142
+ - controller_data_read
143
+ - controller_data_written
144
+ - controller_read_ops
145
+ - controller_write_ops
146
+ - controller_write_cache_hits
147
+ - controller_write_cache_misses
148
+ - controller_read_cache_hits
149
+ - controller_read_cache_misses
150
+ charts:
151
+ - id: controller_iops
152
+ title: Controller IOPS
153
+ context: controller_iops
154
+ units: ops/s
155
+ algorithm: absolute
156
+ instances:
157
+ by_labels: [controller]
158
+ dimensions:
159
+ - selector: controller_iops
160
+ name: iops
161
+ - id: controller_throughput
162
+ title: Controller Throughput
163
+ context: controller_throughput
164
+ units: bytes/s
165
+ algorithm: absolute
166
+ instances:
167
+ by_labels: [controller]
168
+ dimensions:
169
+ - selector: controller_throughput
170
+ name: throughput
171
+ - id: controller_cpu_load
172
+ title: Controller CPU Load
173
+ context: controller_cpu_load
174
+ units: percentage
175
+ algorithm: absolute
176
+ instances:
177
+ by_labels: [controller]
178
+ dimensions:
179
+ - selector: controller_cpu_load
180
+ name: cpu_load
181
+ options:
182
+ float: true
183
+ - id: controller_write_cache_used
184
+ title: Controller Write Cache Usage
185
+ context: controller_write_cache_used
186
+ units: percentage
187
+ algorithm: absolute
188
+ instances:
189
+ by_labels: [controller]
190
+ dimensions:
191
+ - selector: controller_write_cache_used
192
+ name: write_cache_used
193
+ - id: controller_forwarded_cmds
194
+ title: Controller Forwarded Commands
195
+ context: controller_forwarded_cmds
196
+ units: commands
197
+ algorithm: incremental
198
+ instances:
199
+ by_labels: [controller]
200
+ dimensions:
201
+ - selector: controller_forwarded_cmds
202
+ name: forwarded
203
+ - id: controller_io
204
+ title: Controller I/O
205
+ context: controller_io
206
+ units: bytes
207
+ type: area
208
+ algorithm: incremental
209
+ instances:
210
+ by_labels: [controller]
211
+ dimensions:
212
+ - selector: controller_data_read
213
+ name: read
214
+ - selector: controller_data_written
215
+ name: written
216
+ options:
217
+ multiplier: -1
218
+ - id: controller_ops
219
+ title: Controller Operations
220
+ context: controller_ops
221
+ units: ops
222
+ type: area
223
+ algorithm: incremental
224
+ instances:
225
+ by_labels: [controller]
226
+ dimensions:
227
+ - selector: controller_read_ops
228
+ name: read
229
+ - selector: controller_write_ops
230
+ name: write
231
+ options:
232
+ multiplier: -1
233
+ - id: controller_cache_hits
234
+ title: Controller Cache Hits and Misses
235
+ context: controller_cache_hits
236
+ units: hits
237
+ type: stacked
238
+ algorithm: incremental
239
+ instances:
240
+ by_labels: [controller]
241
+ dimensions:
242
+ - selector: controller_read_cache_hits
243
+ name: read_hits
244
+ - selector: controller_read_cache_misses
245
+ name: read_misses
246
+ - selector: controller_write_cache_hits
247
+ name: write_hits
248
+ - selector: controller_write_cache_misses
249
+ name: write_misses
250
+
251
+ - family: Volume
252
+ metrics:
253
+ - volume_iops
254
+ - volume_throughput
255
+ - volume_write_cache_percent
256
+ - volume_data_read
257
+ - volume_data_written
258
+ - volume_read_ops
259
+ - volume_write_ops
260
+ - volume_write_cache_hits
261
+ - volume_write_cache_misses
262
+ - volume_read_cache_hits
263
+ - volume_read_cache_misses
264
+ - volume_tier_ssd
265
+ - volume_tier_sas
266
+ - volume_tier_sata
267
+ charts:
268
+ - id: volume_iops
269
+ title: Volume IOPS
270
+ context: volume_iops
271
+ units: ops/s
272
+ algorithm: absolute
273
+ instances:
274
+ by_labels: [volume]
275
+ dimensions:
276
+ - selector: volume_iops
277
+ name: iops
278
+ - id: volume_throughput
279
+ title: Volume Throughput
280
+ context: volume_throughput
281
+ units: bytes/s
282
+ algorithm: absolute
283
+ instances:
284
+ by_labels: [volume]
285
+ dimensions:
286
+ - selector: volume_throughput
287
+ name: throughput
288
+ - id: volume_write_cache_percent
289
+ title: Volume Write Cache Usage
290
+ context: volume_write_cache_percent
291
+ units: percentage
292
+ algorithm: absolute
293
+ instances:
294
+ by_labels: [volume]
295
+ dimensions:
296
+ - selector: volume_write_cache_percent
297
+ name: write_cache
298
+ - id: volume_io
299
+ title: Volume I/O
300
+ context: volume_io
301
+ units: bytes
302
+ type: area
303
+ algorithm: incremental
304
+ instances:
305
+ by_labels: [volume]
306
+ dimensions:
307
+ - selector: volume_data_read
308
+ name: read
309
+ - selector: volume_data_written
310
+ name: written
311
+ options:
312
+ multiplier: -1
313
+ - id: volume_ops
314
+ title: Volume Operations
315
+ context: volume_ops
316
+ units: ops
317
+ type: area
318
+ algorithm: incremental
319
+ instances:
320
+ by_labels: [volume]
321
+ dimensions:
322
+ - selector: volume_read_ops
323
+ name: read
324
+ - selector: volume_write_ops
325
+ name: write
326
+ options:
327
+ multiplier: -1
328
+ - id: volume_cache_hits
329
+ title: Volume Cache Hits and Misses
330
+ context: volume_cache_hits
331
+ units: hits
332
+ type: stacked
333
+ algorithm: incremental
334
+ instances:
335
+ by_labels: [volume]
336
+ dimensions:
337
+ - selector: volume_read_cache_hits
338
+ name: read_hits
339
+ - selector: volume_read_cache_misses
340
+ name: read_misses
341
+ - selector: volume_write_cache_hits
342
+ name: write_hits
343
+ - selector: volume_write_cache_misses
344
+ name: write_misses
345
+ - id: volume_tier_distribution
346
+ title: Volume Storage Tier Distribution
347
+ context: volume_tier_distribution
348
+ units: percentage
349
+ type: stacked
350
+ algorithm: absolute
351
+ instances:
352
+ by_labels: [volume]
353
+ dimensions:
354
+ - selector: volume_tier_ssd
355
+ name: ssd
356
+ - selector: volume_tier_sas
357
+ name: sas
358
+ - selector: volume_tier_sata
359
+ name: sata
360
+
361
+ - family: Port
362
+ metrics:
363
+ - port_read_ops
364
+ - port_write_ops
365
+ - port_data_read
366
+ - port_data_written
367
+ charts:
368
+ - id: port_io
369
+ title: Port I/O
370
+ context: port_io
371
+ units: bytes
372
+ type: area
373
+ algorithm: incremental
374
+ instances:
375
+ by_labels: [port]
376
+ dimensions:
377
+ - selector: port_data_read
378
+ name: read
379
+ - selector: port_data_written
380
+ name: written
381
+ options:
382
+ multiplier: -1
383
+ - id: port_ops
384
+ title: Port Operations
385
+ context: port_ops
386
+ units: ops
387
+ type: area
388
+ algorithm: incremental
389
+ instances:
390
+ by_labels: [port]
391
+ dimensions:
392
+ - selector: port_read_ops
393
+ name: read
394
+ - selector: port_write_ops
395
+ name: write
396
+ options:
397
+ multiplier: -1
398
+
399
+ - family: PHY Errors
400
+ metrics:
401
+ - phy_disparity_errors
402
+ - phy_lost_dwords
403
+ - phy_invalid_dwords
404
+ charts:
405
+ - id: phy_errors
406
+ title: SAS PHY Errors
407
+ context: phy_errors
408
+ units: errors
409
+ type: stacked
410
+ algorithm: incremental
411
+ instances:
412
+ by_labels: [port]
413
+ dimensions:
414
+ - selector: phy_disparity_errors
415
+ name: disparity
416
+ - selector: phy_lost_dwords
417
+ name: lost_dwords
418
+ - selector: phy_invalid_dwords
419
+ name: invalid_dwords
420
+
421
+ - family: Pool
422
+ metrics:
423
+ - pool_total_bytes
424
+ - pool_available_bytes
425
+ charts:
426
+ - id: pool_capacity
427
+ title: Pool Capacity
428
+ context: pool_capacity
429
+ units: bytes
430
+ algorithm: absolute
431
+ instances:
432
+ by_labels: [pool]
433
+ dimensions:
434
+ - selector: pool_total_bytes
435
+ name: total
436
+ - selector: pool_available_bytes
437
+ name: available
438
+
439
+ - family: Drive
440
+ metrics:
441
+ - drive_temperature
442
+ - drive_power_on_hours
443
+ - drive_ssd_life_left
444
+ charts:
445
+ - id: drive_temperature
446
+ title: Drive Temperature
447
+ context: drive_temperature
448
+ units: Celsius
449
+ algorithm: absolute
450
+ instances:
451
+ by_labels: [drive]
452
+ dimensions:
453
+ - selector: drive_temperature
454
+ name: temperature
455
+ - id: drive_power_on_hours
456
+ title: Drive Power-On Hours
457
+ context: drive_power_on_hours
458
+ units: hours
459
+ algorithm: absolute
460
+ instances:
461
+ by_labels: [drive]
462
+ dimensions:
463
+ - selector: drive_power_on_hours
464
+ name: power_on_hours
465
+ - id: drive_ssd_life_left
466
+ title: Drive SSD Life Remaining
467
+ context: drive_ssd_life_left
468
+ units: percentage
469
+ algorithm: absolute
470
+ instances:
471
+ by_labels: [drive]
472
+ dimensions:
473
+ - selector: drive_ssd_life_left
474
+ name: life_left
475
+
476
+ - family: Sensor
477
+ metrics:
478
+ - sensor_temperature
479
+ - sensor_voltage
480
+ - sensor_current
481
+ - sensor_charge_capacity
482
+ charts:
483
+ - id: sensor_temperature
484
+ title: Sensor Temperature
485
+ context: sensor_temperature
486
+ units: Celsius
487
+ algorithm: absolute
488
+ instances:
489
+ by_labels: [sensor]
490
+ dimensions:
491
+ - selector: sensor_temperature
492
+ name: temperature
493
+ - id: sensor_voltage
494
+ title: Sensor Voltage
495
+ context: sensor_voltage
496
+ units: millivolts
497
+ algorithm: absolute
498
+ instances:
499
+ by_labels: [sensor]
500
+ dimensions:
501
+ - selector: sensor_voltage
502
+ name: voltage
503
+ options:
504
+ float: true
505
+ - id: sensor_current
506
+ title: Sensor Current
507
+ context: sensor_current
508
+ units: milliamps
509
+ algorithm: absolute
510
+ instances:
511
+ by_labels: [sensor]
512
+ dimensions:
513
+ - selector: sensor_current
514
+ name: current
515
+ options:
516
+ float: true
517
+ - id: sensor_charge_capacity
518
+ title: Sensor Charge Capacity
519
+ context: sensor_charge_capacity
520
+ units: percentage
521
+ algorithm: absolute
522
+ instances:
523
+ by_labels: [sensor]
524
+ dimensions:
525
+ - selector: sensor_charge_capacity
526
+ name: charge_capacity
src/go/plugin/go.d/collector/powervault/client/client.go
new
+293
@@ -0,0 +1,293 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package client
4
+
5
+import (
6
+ "crypto/md5"
7
+ "crypto/sha256"
8
+ "encoding/json"
9
+ "fmt"
10
+ "net/http"
11
+ "net/url"
12
+ "path"
13
+ "sync"
14
+
15
+ "github.com/netdata/netdata/go/plugins/pkg/web"
16
+)
17
+
18
+// New creates a new PowerVault MCI REST API client.
19
+func New(client web.ClientConfig, request web.RequestConfig, digest string) (*Client, error) {
20
+ httpClient, err := web.NewHTTPClient(client)
21
+ if err != nil {
22
+ return nil, err
23
+ }
24
+
25
+ return &Client{
26
+ request: request,
27
+ httpClient: httpClient,
28
+ digest: digest,
29
+ }, nil
30
+}
31
+
32
+// Client represents a Dell PowerVault MCI API client.
33
+type Client struct {
34
+ request web.RequestConfig
35
+ httpClient *http.Client
36
+ digest string // "sha256" or "md5"
37
+
38
+ mu sync.Mutex // protects sessionKey reads/writes
39
+ sessionKey string
40
+ authMu sync.Mutex // serializes re-authentication
41
+}
42
+
43
+// Login authenticates with the PowerVault API.
44
+// Hashes "username_password" with SHA-256 or MD5, then GET /api/login/<hash>.
45
+func (c *Client) Login() error {
46
+ key, err := c.login()
47
+ if err != nil {
48
+ return err
49
+ }
50
+ c.setSessionKey(key)
51
+ return nil
52
+}
53
+
54
+// login authenticates and returns the session key without publishing it.
55
+func (c *Client) login() (string, error) {
56
+ hash := c.authHash()
57
+
58
+ req := c.newRequest("/api/login/" + hash)
59
+
60
+ resp, err := c.doOK(req)
61
+ defer web.CloseBody(resp)
62
+ if err != nil {
63
+ return "", fmt.Errorf("login failed: %v", err)
64
+ }
65
+
66
+ var result struct {
67
+ Status []StatusResponse `json:"status"`
68
+ }
69
+ if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
70
+ return "", fmt.Errorf("login: error decoding response: %v", err)
71
+ }
72
+ if len(result.Status) == 0 || result.Status[0].ResponseType != "Success" {
73
+ if len(result.Status) > 0 {
74
+ return "", fmt.Errorf("login: authentication failed: %s (rc=%d)", result.Status[0].Response, result.Status[0].ReturnCode)
75
+ }
76
+ return "", fmt.Errorf("login: authentication failed: empty status response")
77
+ }
78
+
79
+ return result.Status[0].Response, nil
80
+}
81
+
82
+func (c *Client) setSessionKey(key string) {
83
+ c.mu.Lock()
84
+ c.sessionKey = key
85
+ c.mu.Unlock()
86
+}
87
+
88
+// SetLocale sets the CLI output to English to prevent locale-dependent parsing.
89
+func (c *Client) SetLocale() error {
90
+ req := c.newSessionRequest("/api/set/cli-parameters/locale/English")
91
+ resp, err := c.doOK(req)
92
+ web.CloseBody(resp)
93
+ return err
94
+}
95
+
96
+// setLocale sets the CLI locale to English using the given session key.
97
+func (c *Client) setLocale(sessionKey string) error {
98
+ req := c.newRequest("/api/set/cli-parameters/locale/English")
99
+ if sessionKey != "" {
100
+ req.Headers["sessionKey"] = sessionKey
101
+ }
102
+ resp, err := c.doOK(req)
103
+ web.CloseBody(resp)
104
+ return err
105
+}
106
+
107
+// System returns system information.
108
+func (c *Client) System() ([]SystemInfo, error) {
109
+ return doShow[SystemInfo](c, "/api/show/system", "system")
110
+}
111
+
112
+// Controllers returns all controllers.
113
+func (c *Client) Controllers() ([]Controller, error) {
114
+ return doShow[Controller](c, "/api/show/controllers", "controllers")
115
+}
116
+
117
+// Drives returns all drives.
118
+func (c *Client) Drives() ([]Drive, error) {
119
+ return doShow[Drive](c, "/api/show/disks", "drives")
120
+}
121
+
122
+// Fans returns all fans.
123
+func (c *Client) Fans() ([]Fan, error) {
124
+ return doShow[Fan](c, "/api/show/fans", "fan")
125
+}
126
+
127
+// PowerSupplies returns all power supplies.
128
+func (c *Client) PowerSupplies() ([]PowerSupply, error) {
129
+ return doShow[PowerSupply](c, "/api/show/power-supplies", "power-supplies")
130
+}
131
+
132
+// Sensors returns all sensor readings.
133
+func (c *Client) Sensors() ([]Sensor, error) {
134
+ return doShow[Sensor](c, "/api/show/sensor-status", "sensors")
135
+}
136
+
137
+// FRUs returns all Field Replaceable Units.
138
+func (c *Client) FRUs() ([]FRU, error) {
139
+ return doShow[FRU](c, "/api/show/frus", "enclosure-fru")
140
+}
141
+
142
+// Volumes returns all volumes.
143
+func (c *Client) Volumes() ([]Volume, error) {
144
+ return doShow[Volume](c, "/api/show/volumes", "volumes")
145
+}
146
+
147
+// Pools returns all storage pools.
148
+func (c *Client) Pools() ([]Pool, error) {
149
+ return doShow[Pool](c, "/api/show/pools", "pools")
150
+}
151
+
152
+// Ports returns all host ports.
153
+func (c *Client) Ports() ([]Port, error) {
154
+ return doShow[Port](c, "/api/show/ports", "port")
155
+}
156
+
157
+// ControllerStatistics returns performance stats for all controllers.
158
+func (c *Client) ControllerStatistics() ([]ControllerStats, error) {
159
+ return doShow[ControllerStats](c, "/api/show/controller-statistics", "controller-statistics")
160
+}
161
+
162
+// VolumeStatistics returns performance stats for all volumes.
163
+func (c *Client) VolumeStatistics() ([]VolumeStats, error) {
164
+ return doShow[VolumeStats](c, "/api/show/volume-statistics", "volume-statistics")
165
+}
166
+
167
+// PortStatistics returns I/O stats for all host ports.
168
+func (c *Client) PortStatistics() ([]PortStats, error) {
169
+ return doShow[PortStats](c, "/api/show/host-port-statistics", "host-port-statistics")
170
+}
171
+
172
+// PhyStatistics returns SAS PHY error stats.
173
+func (c *Client) PhyStatistics() ([]PhyStats, error) {
174
+ return doShow[PhyStats](c, "/api/show/host-phy-statistics", "sas-host-phy-statistics")
175
+}
176
+
177
+// doShow fetches a /api/show/<command> endpoint and extracts the named array from the response.
178
+// Re-authenticates once on 401 (session expiry), including locale reset.
179
+func doShow[T any](c *Client, urlPath, key string) ([]T, error) {
180
+ req := c.newSessionRequest(urlPath)
181
+
182
+ resp, err := c.doOK(req)
183
+ if err != nil && resp != nil && resp.StatusCode == http.StatusUnauthorized {
184
+ if loginErr := c.reAuth(); loginErr != nil {
185
+ return nil, fmt.Errorf("%s: session expired and re-auth failed: %v", urlPath, loginErr)
186
+ }
187
+ req = c.newSessionRequest(urlPath)
188
+ resp, err = c.doOK(req)
189
+ }
190
+ defer web.CloseBody(resp)
191
+ if err != nil {
192
+ return nil, err
193
+ }
194
+
195
+ var raw map[string]json.RawMessage
196
+ if err = json.NewDecoder(resp.Body).Decode(&raw); err != nil {
197
+ return nil, fmt.Errorf("%s: error decoding response: %v", urlPath, err)
198
+ }
199
+
200
+ // Check for API-level errors in the status envelope.
201
+ if statusData, ok := raw["status"]; ok {
202
+ var statuses []StatusResponse
203
+ if json.Unmarshal(statusData, &statuses) == nil {
204
+ for _, s := range statuses {
205
+ if s.ResponseType == "Error" {
206
+ return nil, fmt.Errorf("%s: API error: %s (rc=%d)", urlPath, s.Response, s.ReturnCode)
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ data, ok := raw[key]
213
+ if !ok {
214
+ return nil, nil
215
+ }
216
+
217
+ var result []T
218
+ if err = json.Unmarshal(data, &result); err != nil {
219
+ return nil, fmt.Errorf("%s: error decoding %q: %v", urlPath, key, err)
220
+ }
221
+ return result, nil
222
+}
223
+
224
+// reAuth re-authenticates and restores session locale.
225
+// Serialized so concurrent 401 retries don't race on session state.
226
+// The new session key is published only after locale setup, preventing
227
+// concurrent requests from seeing a session with non-English locale.
228
+func (c *Client) reAuth() error {
229
+ c.authMu.Lock()
230
+ defer c.authMu.Unlock()
231
+ key, err := c.login()
232
+ if err != nil {
233
+ return err
234
+ }
235
+ if err := c.setLocale(key); err != nil {
236
+ return err
237
+ }
238
+ c.setSessionKey(key)
239
+ return nil
240
+}
241
+
242
+func (c *Client) authHash() string {
243
+ cred := c.request.Username + "_" + c.request.Password
244
+ if c.digest == "md5" {
245
+ return fmt.Sprintf("%x", md5.Sum([]byte(cred)))
246
+ }
247
+ return fmt.Sprintf("%x", sha256.Sum256([]byte(cred)))
248
+}
249
+
250
+func (c *Client) newRequest(urlPath string) web.RequestConfig {
251
+ req := c.request.Copy()
252
+ u, _ := url.Parse(req.URL)
253
+ u.Path = path.Join(u.Path, urlPath)
254
+ req.URL = u.String()
255
+ // Clear basic auth — MCI API uses hash-based auth, not HTTP Basic.
256
+ req.Username = ""
257
+ req.Password = ""
258
+ if req.Headers == nil {
259
+ req.Headers = make(map[string]string)
260
+ }
261
+ req.Headers["datatype"] = "json"
262
+ return req
263
+}
264
+
265
+func (c *Client) newSessionRequest(urlPath string) web.RequestConfig {
266
+ req := c.newRequest(urlPath)
267
+
268
+ c.mu.Lock()
269
+ key := c.sessionKey
270
+ c.mu.Unlock()
271
+
272
+ if key != "" {
273
+ req.Headers["sessionKey"] = key
274
+ }
275
+ return req
276
+}
277
+
278
+func (c *Client) doOK(req web.RequestConfig) (*http.Response, error) {
279
+ httpReq, err := web.NewHTTPRequest(req)
280
+ if err != nil {
281
+ return nil, fmt.Errorf("error creating request to %s: %v", req.URL, err)
282
+ }
283
+ resp, err := c.httpClient.Do(httpReq)
284
+ if err != nil {
285
+ return resp, err
286
+ }
287
+ if resp.StatusCode < 200 || resp.StatusCode >= 400 {
288
+ web.CloseBody(resp)
289
+ // Return resp (body closed) so callers can inspect StatusCode.
290
+ return resp, fmt.Errorf("%s: HTTP %d", req.URL, resp.StatusCode)
291
+ }
292
+ return resp, nil
293
+}
src/go/plugin/go.d/collector/powervault/client/types.go
new
+197
@@ -0,0 +1,197 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package client
4
+
5
+// StatusResponse is the common status envelope in every MCI API response.
6
+type StatusResponse struct {
7
+ ResponseType string `json:"response-type"`
8
+ Response string `json:"response"`
9
+ ReturnCode int `json:"return-code"`
10
+ ResponseTypeNum int `json:"response-type-numeric"`
11
+}
12
+
13
+// SystemInfo from /api/show/system.
14
+type SystemInfo struct {
15
+ SystemName string `json:"system-name"`
16
+ ProductID string `json:"product-id"`
17
+ HealthNumeric int `json:"health-numeric"`
18
+ Health string `json:"health"`
19
+ MidplaneSerial string `json:"midplane-serial-number"`
20
+ VendorName string `json:"vendor-name"`
21
+ PlatformBrand string `json:"platform-brand"`
22
+ EnclosureCount int `json:"enclosure-count"`
23
+ SystemLocation string `json:"system-location"`
24
+ SystemContact string `json:"system-contact"`
25
+ SCProductID string `json:"scsi-product-id"`
26
+ CurrentNodeWWN string `json:"current-node-wwn"`
27
+}
28
+
29
+// Controller from /api/show/controllers.
30
+type Controller struct {
31
+ DurableID string `json:"durable-id"`
32
+ Description string `json:"description"`
33
+ Status string `json:"status"`
34
+ Health string `json:"health"`
35
+ HealthNumeric int `json:"health-numeric"`
36
+ SerialNumber string `json:"serial-number"`
37
+ RedundancyStatus string `json:"redundancy-status"`
38
+}
39
+
40
+// Drive from /api/show/disks (response key: "drives").
41
+type Drive struct {
42
+ DurableID string `json:"durable-id"`
43
+ Location string `json:"location"`
44
+ SerialNumber string `json:"serial-number"`
45
+ Model string `json:"model"`
46
+ Vendor string `json:"vendor"`
47
+ Description string `json:"description"`
48
+ Architecture string `json:"architecture"` // HDD or SSD
49
+ Interface string `json:"interface"` // SAS, SATA, NVMe
50
+ Size string `json:"size"`
51
+ SizeNumeric int64 `json:"size-numeric"` // in 512-byte blocks
52
+ Health string `json:"health"`
53
+ HealthNumeric int `json:"health-numeric"`
54
+ Status string `json:"status"`
55
+ TempNumeric int `json:"temperature-numeric"`
56
+ SSDLifeLeft int `json:"ssd-life-left-numeric"` // 255 = N/A (HDD)
57
+ PowerOnHours int `json:"power-on-hours"`
58
+ Usage string `json:"usage"`
59
+ UsageNumeric int `json:"usage-numeric"` // 0=AVAIL, 3=GLOBAL SP, 7=FAILED, 8=UNUSABLE, 9=VIRTUAL POOL
60
+ EnclosureID int `json:"enclosure-id"`
61
+ Slot int `json:"slot"`
62
+ StorageTier string `json:"storage-tier"`
63
+ DiskGroup string `json:"disk-group"`
64
+ StoragePoolName string `json:"storage-pool-name"`
65
+}
66
+
67
+// Fan from /api/show/fans (response key: "fan").
68
+type Fan struct {
69
+ DurableID string `json:"durable-id"`
70
+ Name string `json:"name"`
71
+ Location string `json:"location"`
72
+ Health string `json:"health"`
73
+ HealthNumeric int `json:"health-numeric"`
74
+ Status string `json:"status"`
75
+ Speed int `json:"speed"` // RPM
76
+}
77
+
78
+// PowerSupply from /api/show/power-supplies.
79
+type PowerSupply struct {
80
+ DurableID string `json:"durable-id"`
81
+ Name string `json:"name"`
82
+ Location string `json:"location"`
83
+ Health string `json:"health"`
84
+ HealthNumeric int `json:"health-numeric"`
85
+ Status string `json:"status"`
86
+ SerialNumber string `json:"serial-number"`
87
+ Model string `json:"model"`
88
+ Position string `json:"position"`
89
+}
90
+
91
+// Sensor from /api/show/sensor-status (response key: "sensors").
92
+type Sensor struct {
93
+ DurableID string `json:"durable-id"`
94
+ SensorName string `json:"sensor-name"`
95
+ SensorType string `json:"sensor-type"` // Temperature, Voltage, Current, Charge Capacity
96
+ Value string `json:"value"` // e.g. "32 C", "3.3 V", "95%"
97
+ Status string `json:"status"`
98
+ StatusNumeric int `json:"status-numeric"` // DIFFERENT mapping: 0=Unsupported,1=OK,2=Critical,3=Warning,...
99
+}
100
+
101
+// FRU from /api/show/frus (response key: "enclosure-fru").
102
+type FRU struct {
103
+ Name string `json:"name"`
104
+ FRULocation string `json:"fru-location"`
105
+ FRUStatus string `json:"fru-status"`
106
+ FRUStatusNum int `json:"fru-status-numeric"` // 0=OK, 1=Degraded, 2=Fault, 3=Unknown, 5=N/A→OK
107
+ PartNumber string `json:"part-number"`
108
+}
109
+
110
+// Volume from /api/show/volumes (response key: "volumes").
111
+type Volume struct {
112
+ DurableID string `json:"durable-id"`
113
+ VolumeName string `json:"volume-name"`
114
+ Health string `json:"health"`
115
+ HealthNumeric int `json:"health-numeric"`
116
+ TotalSize string `json:"total-size"`
117
+ OwnerNumeric int `json:"owner-numeric"`
118
+}
119
+
120
+// Pool from /api/show/pools.
121
+type Pool struct {
122
+ Name string `json:"name"`
123
+ Health string `json:"health"`
124
+ HealthNumeric int `json:"health-numeric"`
125
+ TotalSize string `json:"total-size"`
126
+ TotalAvail string `json:"total-avail"`
127
+ // Numeric values in 512-byte blocks
128
+ TotalSizeNumeric int64 `json:"total-size-numeric"`
129
+ TotalAvailNumeric int64 `json:"total-avail-numeric"`
130
+}
131
+
132
+// Port from /api/show/ports.
133
+type Port struct {
134
+ DurableID string `json:"durable-id"`
135
+ Port string `json:"port"`
136
+ Health string `json:"health"`
137
+ HealthNumeric int `json:"health-numeric"` // 0-3 + 4=Not Available
138
+ Status string `json:"status"`
139
+ StatusNumeric int `json:"status-numeric"` // 0=up, 1=down, 2=notInstalled
140
+ ActualSpeed string `json:"actual-speed"`
141
+ TargetID string `json:"target-id"`
142
+}
143
+
144
+// ControllerStats from /api/show/controller-statistics.
145
+type ControllerStats struct {
146
+ DurableID string `json:"durable-id"`
147
+ IOPS int64 `json:"iops"`
148
+ BytesPerSecond int64 `json:"bytes-per-second-numeric"`
149
+ CPULoad float64 `json:"cpu-load"`
150
+ DataRead int64 `json:"data-read-numeric"`
151
+ DataWritten int64 `json:"data-written-numeric"`
152
+ NumReads int64 `json:"number-of-reads"`
153
+ NumWrites int64 `json:"number-of-writes"`
154
+ WriteCacheUsed int `json:"write-cache-used"`
155
+ WriteCacheHits int64 `json:"write-cache-hits"`
156
+ WriteCacheMisses int64 `json:"write-cache-misses"`
157
+ ReadCacheHits int64 `json:"read-cache-hits"`
158
+ ReadCacheMisses int64 `json:"read-cache-misses"`
159
+ ForwardedCmds int64 `json:"num-forwarded-cmds"`
160
+}
161
+
162
+// VolumeStats from /api/show/volume-statistics.
163
+type VolumeStats struct {
164
+ VolumeName string `json:"volume-name"`
165
+ IOPS int64 `json:"iops"`
166
+ BytesPerSecond int64 `json:"bytes-per-second-numeric"`
167
+ DataRead int64 `json:"data-read-numeric"`
168
+ DataWritten int64 `json:"data-written-numeric"`
169
+ NumReads int64 `json:"number-of-reads"`
170
+ NumWrites int64 `json:"number-of-writes"`
171
+ WriteCachePercent int `json:"write-cache-percent"`
172
+ WriteCacheHits int64 `json:"write-cache-hits"`
173
+ WriteCacheMisses int64 `json:"write-cache-misses"`
174
+ ReadCacheHits int64 `json:"read-cache-hits"`
175
+ ReadCacheMisses int64 `json:"read-cache-misses"`
176
+ TierSSD int `json:"percent-tier-ssd"`
177
+ TierSAS int `json:"percent-tier-sas"`
178
+ TierSATA int `json:"percent-tier-sata"`
179
+}
180
+
181
+// PortStats from /api/show/host-port-statistics.
182
+type PortStats struct {
183
+ DurableID string `json:"durable-id"`
184
+ NumReads int64 `json:"number-of-reads"`
185
+ NumWrites int64 `json:"number-of-writes"`
186
+ DataRead int64 `json:"data-read-numeric"`
187
+ DataWritten int64 `json:"data-write-numeric"` // note: "data-write" not "data-written"
188
+}
189
+
190
+// PhyStats from /api/show/host-phy-statistics (response key: "sas-host-phy-statistics").
191
+type PhyStats struct {
192
+ Port string `json:"port"`
193
+ Phy string `json:"phy"`
194
+ DisparityErrors int64 `json:"disparity-errors"`
195
+ LostDwords int64 `json:"lost-dwords"`
196
+ InvalidDwords int64 `json:"invalid-dwords"`
197
+}
src/go/plugin/go.d/collector/powervault/collect.go
new
+146
@@ -0,0 +1,146 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+import (
6
+ "sync"
7
+ "time"
8
+
9
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/powervault/client"
10
+)
11
+
12
+const discoveryEvery = 10
13
+
14
+func (c *Collector) collect() error {
15
+ c.runs++
16
+ needsDiscovery := !c.lastDiscoveryOK || c.runs%discoveryEvery == 0
17
+ wasOK := c.lastDiscoveryOK
18
+
19
+ if needsDiscovery {
20
+ if err := c.discovery(); err != nil {
21
+ // Initial discovery must succeed.
22
+ if !wasOK {
23
+ return err
24
+ }
25
+ // Refresh failure — continue with previous data.
26
+ c.Warningf("discovery refresh failed, using previous data: %v", err)
27
+ }
28
+ }
29
+
30
+ var wg sync.WaitGroup
31
+ for _, fn := range []func(){
32
+ c.collectControllerStats,
33
+ c.collectVolumeStats,
34
+ c.collectPortStats,
35
+ c.collectPhyStats,
36
+ } {
37
+ wg.Add(1)
38
+ go func(f func()) {
39
+ defer wg.Done()
40
+ f()
41
+ }(fn)
42
+ }
43
+ wg.Wait()
44
+
45
+ // These use cached discovery data, no API calls.
46
+ c.collectHardwareHealth()
47
+ c.collectDriveMetrics()
48
+ c.collectSensorMetrics()
49
+ c.collectPoolCapacity()
50
+ c.collectSystemHealth()
51
+
52
+ return nil
53
+}
54
+
55
+func (c *Collector) discovery() error {
56
+ start := time.Now()
57
+ c.Debugf("starting discovery")
58
+
59
+ var d discovered
60
+
61
+ sys, err := c.client.System()
62
+ if err != nil {
63
+ c.lastDiscoveryOK = false
64
+ return err
65
+ }
66
+ d.system = sys
67
+
68
+ controllers, err := c.client.Controllers()
69
+ if err != nil {
70
+ c.lastDiscoveryOK = false
71
+ return err
72
+ }
73
+ d.controllers = controllers
74
+
75
+ drives, err := c.client.Drives()
76
+ if err != nil {
77
+ c.lastDiscoveryOK = false
78
+ return err
79
+ }
80
+ d.drives = drives
81
+
82
+ fans, err := c.client.Fans()
83
+ if err != nil {
84
+ c.lastDiscoveryOK = false
85
+ return err
86
+ }
87
+ d.fans = fans
88
+
89
+ psus, err := c.client.PowerSupplies()
90
+ if err != nil {
91
+ c.lastDiscoveryOK = false
92
+ return err
93
+ }
94
+ d.psus = psus
95
+
96
+ sensors, err := c.client.Sensors()
97
+ if err != nil {
98
+ c.lastDiscoveryOK = false
99
+ return err
100
+ }
101
+ d.sensors = sensors
102
+
103
+ frus, err := c.client.FRUs()
104
+ if err != nil {
105
+ c.lastDiscoveryOK = false
106
+ return err
107
+ }
108
+ d.frus = frus
109
+
110
+ volumes, err := c.client.Volumes()
111
+ if err != nil {
112
+ c.lastDiscoveryOK = false
113
+ return err
114
+ }
115
+ d.volumes = make(map[string]client.Volume, len(volumes))
116
+ for _, v := range volumes {
117
+ if c.volumeMatches(v.VolumeName) {
118
+ d.volumes[v.VolumeName] = v
119
+ }
120
+ }
121
+
122
+ pools, err := c.client.Pools()
123
+ if err != nil {
124
+ c.lastDiscoveryOK = false
125
+ return err
126
+ }
127
+ d.pools = pools
128
+
129
+ ports, err := c.client.Ports()
130
+ if err != nil {
131
+ c.lastDiscoveryOK = false
132
+ return err
133
+ }
134
+ d.ports = ports
135
+
136
+ // Atomic swap on full success.
137
+ c.discovered = d
138
+ c.lastDiscoveryOK = true
139
+
140
+ c.Debugf("discovery: %d controllers, %d drives, %d fans, %d PSUs, %d sensors, %d FRUs, %d volumes, %d pools, %d ports (took %s)",
141
+ len(d.controllers), len(d.drives), len(d.fans), len(d.psus),
142
+ len(d.sensors), len(d.frus), len(d.volumes), len(d.pools), len(d.ports),
143
+ time.Since(start))
144
+
145
+ return nil
146
+}
src/go/plugin/go.d/collector/powervault/collect_controllers.go
new
+28
@@ -0,0 +1,28 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+func (c *Collector) collectControllerStats() {
6
+ stats, err := c.client.ControllerStatistics()
7
+ if err != nil {
8
+ c.Warningf("error collecting controller statistics: %v", err)
9
+ return
10
+ }
11
+
12
+ for _, s := range stats {
13
+ id := s.DurableID
14
+ c.mx.controller.iops.WithLabelValues(id).Observe(float64(s.IOPS))
15
+ c.mx.controller.throughput.WithLabelValues(id).Observe(float64(s.BytesPerSecond))
16
+ c.mx.controller.cpuLoad.WithLabelValues(id).Observe(s.CPULoad)
17
+ c.mx.controller.writeCacheUsed.WithLabelValues(id).Observe(float64(s.WriteCacheUsed))
18
+ c.mx.controller.forwardedCmds.WithLabelValues(id).Observe(float64(s.ForwardedCmds))
19
+ c.mx.controller.dataRead.WithLabelValues(id).Observe(float64(s.DataRead))
20
+ c.mx.controller.dataWritten.WithLabelValues(id).Observe(float64(s.DataWritten))
21
+ c.mx.controller.readOps.WithLabelValues(id).Observe(float64(s.NumReads))
22
+ c.mx.controller.writeOps.WithLabelValues(id).Observe(float64(s.NumWrites))
23
+ c.mx.controller.writeCacheHits.WithLabelValues(id).Observe(float64(s.WriteCacheHits))
24
+ c.mx.controller.writeCacheMisses.WithLabelValues(id).Observe(float64(s.WriteCacheMisses))
25
+ c.mx.controller.readCacheHits.WithLabelValues(id).Observe(float64(s.ReadCacheHits))
26
+ c.mx.controller.readCacheMisses.WithLabelValues(id).Observe(float64(s.ReadCacheMisses))
27
+ }
28
+}
src/go/plugin/go.d/collector/powervault/collect_drives.go
new
+18
@@ -0,0 +1,18 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+// collectDriveMetrics reports per-drive temperature, power-on hours, and SSD life.
6
+// Uses cached discovery data — no API calls.
7
+func (c *Collector) collectDriveMetrics() {
8
+ for _, d := range c.discovered.drives {
9
+ id := d.Location
10
+ c.mx.drive.temperature.WithLabelValues(id).Observe(float64(d.TempNumeric))
11
+ c.mx.drive.powerOnHours.WithLabelValues(id).Observe(float64(d.PowerOnHours))
12
+
13
+ // SSD life left: 255 means N/A (HDD). Only report for SSDs.
14
+ if d.SSDLifeLeft != 255 {
15
+ c.mx.drive.ssdLifeLeft.WithLabelValues(id).Observe(float64(d.SSDLifeLeft))
16
+ }
17
+ }
18
+}
src/go/plugin/go.d/collector/powervault/collect_hardware.go
new
+131
@@ -0,0 +1,131 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+// collectHardwareHealth classifies hardware components by health state.
6
+// Uses cached discovery data — no API calls.
7
+//
8
+// health-numeric mapping: 0=OK, 1=Degraded, 2=Fault, 3=Unknown.
9
+// Special cases:
10
+// - Drives with usage-numeric=3 (Global Hot Spare) report health=Unknown but are functionally OK.
11
+// - FRUs use fru-status-numeric: 0=OK, 1=Degraded, 2=Fault, 3=Unknown, 5=N/A (treated as OK).
12
+// - Ports with health-numeric=4 (Not Available) are excluded from counts.
13
+func (c *Collector) collectHardwareHealth() {
14
+ var ctrlOK, ctrlDeg, ctrlFault, ctrlUnk float64
15
+ var drvOK, drvDeg, drvFault, drvUnk float64
16
+ var fanOK, fanDeg, fanFault, fanUnk float64
17
+ var psuOK, psuDeg, psuFault, psuUnk float64
18
+ var fruOK, fruDeg, fruFault, fruUnk float64
19
+ var portOK, portDeg, portFault, portUnk float64
20
+
21
+ for _, ctrl := range c.discovered.controllers {
22
+ ok, deg, fault, unk := classifyHealth(ctrl.HealthNumeric)
23
+ ctrlOK += ok
24
+ ctrlDeg += deg
25
+ ctrlFault += fault
26
+ ctrlUnk += unk
27
+ }
28
+
29
+ for _, d := range c.discovered.drives {
30
+ // Global Hot Spare drives report health=Unknown but are functionally OK.
31
+ if d.UsageNumeric == 3 && d.HealthNumeric == 3 {
32
+ drvOK++
33
+ continue
34
+ }
35
+ ok, deg, fault, unk := classifyHealth(d.HealthNumeric)
36
+ drvOK += ok
37
+ drvDeg += deg
38
+ drvFault += fault
39
+ drvUnk += unk
40
+ }
41
+
42
+ for _, f := range c.discovered.fans {
43
+ ok, deg, fault, unk := classifyHealth(f.HealthNumeric)
44
+ fanOK += ok
45
+ fanDeg += deg
46
+ fanFault += fault
47
+ fanUnk += unk
48
+ }
49
+
50
+ for _, p := range c.discovered.psus {
51
+ ok, deg, fault, unk := classifyHealth(p.HealthNumeric)
52
+ psuOK += ok
53
+ psuDeg += deg
54
+ psuFault += fault
55
+ psuUnk += unk
56
+ }
57
+
58
+ for _, f := range c.discovered.frus {
59
+ ok, deg, fault, unk := classifyFRUHealth(f.FRUStatusNum)
60
+ fruOK += ok
61
+ fruDeg += deg
62
+ fruFault += fault
63
+ fruUnk += unk
64
+ }
65
+
66
+ for _, p := range c.discovered.ports {
67
+ // 4 = Not Available — exclude from health counts.
68
+ if p.HealthNumeric == 4 {
69
+ continue
70
+ }
71
+ ok, deg, fault, unk := classifyHealth(p.HealthNumeric)
72
+ portOK += ok
73
+ portDeg += deg
74
+ portFault += fault
75
+ portUnk += unk
76
+ }
77
+
78
+ hw := c.mx.hardware
79
+ hw.controllerOK.Observe(ctrlOK)
80
+ hw.controllerDegraded.Observe(ctrlDeg)
81
+ hw.controllerFault.Observe(ctrlFault)
82
+ hw.controllerUnknown.Observe(ctrlUnk)
83
+ hw.driveOK.Observe(drvOK)
84
+ hw.driveDegraded.Observe(drvDeg)
85
+ hw.driveFault.Observe(drvFault)
86
+ hw.driveUnknown.Observe(drvUnk)
87
+ hw.fanOK.Observe(fanOK)
88
+ hw.fanDegraded.Observe(fanDeg)
89
+ hw.fanFault.Observe(fanFault)
90
+ hw.fanUnknown.Observe(fanUnk)
91
+ hw.psuOK.Observe(psuOK)
92
+ hw.psuDegraded.Observe(psuDeg)
93
+ hw.psuFault.Observe(psuFault)
94
+ hw.psuUnknown.Observe(psuUnk)
95
+ hw.fruOK.Observe(fruOK)
96
+ hw.fruDegraded.Observe(fruDeg)
97
+ hw.fruFault.Observe(fruFault)
98
+ hw.fruUnknown.Observe(fruUnk)
99
+ hw.portOK.Observe(portOK)
100
+ hw.portDegraded.Observe(portDeg)
101
+ hw.portFault.Observe(portFault)
102
+ hw.portUnknown.Observe(portUnk)
103
+}
104
+
105
+// classifyHealth maps health-numeric (0=OK, 1=Degraded, 2=Fault, 3=Unknown).
106
+func classifyHealth(h int) (ok, degraded, fault, unknown float64) {
107
+ switch h {
108
+ case 0:
109
+ return 1, 0, 0, 0
110
+ case 1:
111
+ return 0, 1, 0, 0
112
+ case 2:
113
+ return 0, 0, 1, 0
114
+ default:
115
+ return 0, 0, 0, 1
116
+ }
117
+}
118
+
119
+// classifyFRUHealth maps fru-status-numeric (0=OK, 1=Degraded, 2=Fault, 3=Unknown, 5=N/A→OK).
120
+func classifyFRUHealth(h int) (ok, degraded, fault, unknown float64) {
121
+ switch h {
122
+ case 0, 5:
123
+ return 1, 0, 0, 0
124
+ case 1:
125
+ return 0, 1, 0, 0
126
+ case 2:
127
+ return 0, 0, 1, 0
128
+ default:
129
+ return 0, 0, 0, 1
130
+ }
131
+}
src/go/plugin/go.d/collector/powervault/collect_pools.go
new
+16
@@ -0,0 +1,16 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+// collectPoolCapacity reports per-pool total and available capacity.
6
+// Uses cached discovery data — no API calls.
7
+// Values from the API are in 512-byte blocks; we convert to bytes.
8
+func (c *Collector) collectPoolCapacity() {
9
+ const blockSize = 512
10
+
11
+ for _, p := range c.discovered.pools {
12
+ name := p.Name
13
+ c.mx.pool.totalBytes.WithLabelValues(name).Observe(float64(p.TotalSizeNumeric * blockSize))
14
+ c.mx.pool.availableBytes.WithLabelValues(name).Observe(float64(p.TotalAvailNumeric * blockSize))
15
+ }
16
+}
src/go/plugin/go.d/collector/powervault/collect_ports.go
new
+52
@@ -0,0 +1,52 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+func (c *Collector) collectPortStats() {
6
+ stats, err := c.client.PortStatistics()
7
+ if err != nil {
8
+ c.Warningf("error collecting port statistics: %v", err)
9
+ return
10
+ }
11
+
12
+ for _, s := range stats {
13
+ id := s.DurableID
14
+ c.mx.port.readOps.WithLabelValues(id).Observe(float64(s.NumReads))
15
+ c.mx.port.writeOps.WithLabelValues(id).Observe(float64(s.NumWrites))
16
+ c.mx.port.dataRead.WithLabelValues(id).Observe(float64(s.DataRead))
17
+ c.mx.port.dataWritten.WithLabelValues(id).Observe(float64(s.DataWritten))
18
+ }
19
+}
20
+
21
+func (c *Collector) collectPhyStats() {
22
+ stats, err := c.client.PhyStatistics()
23
+ if err != nil {
24
+ c.Warningf("error collecting PHY statistics: %v", err)
25
+ return
26
+ }
27
+
28
+ // Aggregate PHY errors per port (multiple PHYs per port).
29
+ type portErrors struct {
30
+ disparity, lost, invalid int64
31
+ }
32
+ byPort := make(map[string]*portErrors)
33
+
34
+ for _, s := range stats {
35
+ // Normalize to match port durable ID format (e.g. "A0" → "hostport_A0").
36
+ portID := "hostport_" + s.Port
37
+ pe, ok := byPort[portID]
38
+ if !ok {
39
+ pe = &portErrors{}
40
+ byPort[portID] = pe
41
+ }
42
+ pe.disparity += s.DisparityErrors
43
+ pe.lost += s.LostDwords
44
+ pe.invalid += s.InvalidDwords
45
+ }
46
+
47
+ for portID, pe := range byPort {
48
+ c.mx.phy.disparityErrors.WithLabelValues(portID).Observe(float64(pe.disparity))
49
+ c.mx.phy.lostDwords.WithLabelValues(portID).Observe(float64(pe.lost))
50
+ c.mx.phy.invalidDwords.WithLabelValues(portID).Observe(float64(pe.invalid))
51
+ }
52
+}
src/go/plugin/go.d/collector/powervault/collect_sensors.go
new
+59
@@ -0,0 +1,59 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+import (
6
+ "strconv"
7
+ "strings"
8
+)
9
+
10
+// collectSensorMetrics reports per-sensor readings by type.
11
+// Uses cached discovery data — no API calls.
12
+//
13
+// Sensor types and value formats:
14
+// - Temperature: "32 C" → parse integer before " C"
15
+// - Voltage: "3.30 V" → parse float before " V", report in millivolts
16
+// - Current: "1.25 A" → parse float before " A", report in milliamps
17
+// - Charge Capacity: "95%" → parse integer before "%"
18
+func (c *Collector) collectSensorMetrics() {
19
+ for _, s := range c.discovered.sensors {
20
+ id := s.DurableID
21
+
22
+ switch s.SensorType {
23
+ case "Temperature":
24
+ if v, ok := parseIntBefore(s.Value, " C"); ok {
25
+ c.mx.sensor.temperature.WithLabelValues(id).Observe(float64(v))
26
+ }
27
+ case "Voltage":
28
+ if v, ok := parseFloatBefore(s.Value, " V"); ok {
29
+ c.mx.sensor.voltage.WithLabelValues(id).Observe(v * 1000) // millivolts
30
+ }
31
+ case "Current Sensor":
32
+ if v, ok := parseFloatBefore(s.Value, " A"); ok {
33
+ c.mx.sensor.current.WithLabelValues(id).Observe(v * 1000) // milliamps
34
+ }
35
+ case "Charge Capacity":
36
+ if v, ok := parseIntBefore(s.Value, "%"); ok {
37
+ c.mx.sensor.chargeCapacity.WithLabelValues(id).Observe(float64(v))
38
+ }
39
+ }
40
+ }
41
+}
42
+
43
+func parseIntBefore(s, suffix string) (int64, bool) {
44
+ idx := strings.Index(s, suffix)
45
+ if idx < 0 {
46
+ idx = len(s)
47
+ }
48
+ v, err := strconv.ParseInt(strings.TrimSpace(s[:idx]), 10, 64)
49
+ return v, err == nil
50
+}
51
+
52
+func parseFloatBefore(s, suffix string) (float64, bool) {
53
+ idx := strings.Index(s, suffix)
54
+ if idx < 0 {
55
+ idx = len(s)
56
+ }
57
+ v, err := strconv.ParseFloat(strings.TrimSpace(s[:idx]), 64)
58
+ return v, err == nil
59
+}
src/go/plugin/go.d/collector/powervault/collect_system.go
new
+13
@@ -0,0 +1,13 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+// collectSystemHealth reports overall system health.
6
+// Uses cached discovery data — no API calls.
7
+// health-numeric: 0=OK, 1=Degraded, 2=Fault, 3=Unknown.
8
+func (c *Collector) collectSystemHealth() {
9
+ if len(c.discovered.system) == 0 {
10
+ return
11
+ }
12
+ c.mx.system.health.Observe(float64(c.discovered.system[0].HealthNumeric))
13
+}
src/go/plugin/go.d/collector/powervault/collect_volumes.go
new
+32
@@ -0,0 +1,32 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+func (c *Collector) collectVolumeStats() {
6
+ stats, err := c.client.VolumeStatistics()
7
+ if err != nil {
8
+ c.Warningf("error collecting volume statistics: %v", err)
9
+ return
10
+ }
11
+
12
+ for _, s := range stats {
13
+ name := s.VolumeName
14
+ if !c.volumeMatches(name) {
15
+ continue
16
+ }
17
+ c.mx.volume.iops.WithLabelValues(name).Observe(float64(s.IOPS))
18
+ c.mx.volume.throughput.WithLabelValues(name).Observe(float64(s.BytesPerSecond))
19
+ c.mx.volume.writeCachePercent.WithLabelValues(name).Observe(float64(s.WriteCachePercent))
20
+ c.mx.volume.dataRead.WithLabelValues(name).Observe(float64(s.DataRead))
21
+ c.mx.volume.dataWritten.WithLabelValues(name).Observe(float64(s.DataWritten))
22
+ c.mx.volume.readOps.WithLabelValues(name).Observe(float64(s.NumReads))
23
+ c.mx.volume.writeOps.WithLabelValues(name).Observe(float64(s.NumWrites))
24
+ c.mx.volume.writeCacheHits.WithLabelValues(name).Observe(float64(s.WriteCacheHits))
25
+ c.mx.volume.writeCacheMisses.WithLabelValues(name).Observe(float64(s.WriteCacheMisses))
26
+ c.mx.volume.readCacheHits.WithLabelValues(name).Observe(float64(s.ReadCacheHits))
27
+ c.mx.volume.readCacheMisses.WithLabelValues(name).Observe(float64(s.ReadCacheMisses))
28
+ c.mx.volume.tierSSD.WithLabelValues(name).Observe(float64(s.TierSSD))
29
+ c.mx.volume.tierSAS.WithLabelValues(name).Observe(float64(s.TierSAS))
30
+ c.mx.volume.tierSATA.WithLabelValues(name).Observe(float64(s.TierSATA))
31
+ }
32
+}
src/go/plugin/go.d/collector/powervault/collector.go
new
+160
@@ -0,0 +1,160 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+import (
6
+ "context"
7
+ _ "embed"
8
+ "errors"
9
+ "fmt"
10
+ "time"
11
+
12
+ "github.com/netdata/netdata/go/plugins/pkg/confopt"
13
+ "github.com/netdata/netdata/go/plugins/pkg/matcher"
14
+ "github.com/netdata/netdata/go/plugins/pkg/metrix"
15
+ "github.com/netdata/netdata/go/plugins/pkg/web"
16
+ "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/powervault/client"
18
+)
19
+
20
+//go:embed "config_schema.json"
21
+var configSchema string
22
+
23
+//go:embed "charts.yaml"
24
+var chartTemplateYAML string
25
+
26
+func init() {
27
+ collectorapi.Register("powervault", collectorapi.Creator{
28
+ JobConfigSchema: configSchema,
29
+ Defaults: collectorapi.Defaults{
30
+ UpdateEvery: 30,
31
+ },
32
+ CreateV2: func() collectorapi.CollectorV2 { return New() },
33
+ Config: func() any { return &Config{} },
34
+ })
35
+}
36
+
37
+func New() *Collector {
38
+ store := metrix.NewCollectorStore()
39
+ mx := newCollectorMetrics(store)
40
+
41
+ return &Collector{
42
+ Config: Config{
43
+ HTTPConfig: web.HTTPConfig{
44
+ RequestConfig: web.RequestConfig{
45
+ URL: "https://127.0.0.1",
46
+ },
47
+ ClientConfig: web.ClientConfig{
48
+ Timeout: confopt.Duration(30 * time.Second),
49
+ },
50
+ },
51
+ AuthDigest: "sha256",
52
+ },
53
+ store: store,
54
+ mx: mx,
55
+ }
56
+}
57
+
58
+type Config struct {
59
+ Vnode string `yaml:"vnode,omitempty" json:"vnode"`
60
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
61
+ AutoDetectionRetry int `yaml:"autodetection_retry,omitempty" json:"autodetection_retry"`
62
+ web.HTTPConfig `yaml:",inline" json:""`
63
+ AuthDigest string `yaml:"auth_digest,omitempty" json:"auth_digest"`
64
+ VolumeSelector string `yaml:"volume_selector,omitempty" json:"volume_selector"`
65
+}
66
+
67
+type (
68
+ Collector struct {
69
+ collectorapi.Base
70
+ Config `yaml:",inline" json:""`
71
+
72
+ store metrix.CollectorStore
73
+ mx *collectorMetrics
74
+
75
+ client *client.Client
76
+
77
+ discovered discovered
78
+ lastDiscoveryOK bool
79
+ runs int
80
+
81
+ volMatcher matcher.Matcher
82
+ }
83
+ discovered struct {
84
+ system []client.SystemInfo
85
+ controllers []client.Controller
86
+ drives []client.Drive
87
+ fans []client.Fan
88
+ psus []client.PowerSupply
89
+ sensors []client.Sensor
90
+ frus []client.FRU
91
+ volumes map[string]client.Volume // keyed by volume-name
92
+ pools []client.Pool
93
+ ports []client.Port
94
+ }
95
+)
96
+
97
+func (c *Collector) Configuration() any {
98
+ return c.Config
99
+}
100
+
101
+func (c *Collector) Init(context.Context) error {
102
+ if c.Username == "" || c.Password == "" {
103
+ return errors.New("config: username and password aren't set")
104
+ }
105
+
106
+ digest := c.AuthDigest
107
+ if digest == "" {
108
+ digest = "sha256"
109
+ }
110
+ if digest != "sha256" && digest != "md5" {
111
+ return fmt.Errorf("config: auth_digest must be 'sha256' or 'md5', got %q", digest)
112
+ }
113
+
114
+ cli, err := client.New(c.ClientConfig, c.RequestConfig, digest)
115
+ if err != nil {
116
+ return fmt.Errorf("error creating PowerVault client: %v", err)
117
+ }
118
+ c.client = cli
119
+
120
+ if c.VolumeSelector != "" {
121
+ m, err := matcher.NewSimplePatternsMatcher(c.VolumeSelector)
122
+ if err != nil {
123
+ return fmt.Errorf("error creating volume selector: %v", err)
124
+ }
125
+ c.volMatcher = m
126
+ }
127
+
128
+ c.Debugf("using URL %s", c.URL)
129
+ c.Debugf("using timeout: %s", c.Timeout)
130
+ c.Debugf("using auth digest: %s", digest)
131
+
132
+ return nil
133
+}
134
+
135
+func (c *Collector) volumeMatches(name string) bool {
136
+ if c.volMatcher == nil {
137
+ return true
138
+ }
139
+ return c.volMatcher.MatchString(name)
140
+}
141
+
142
+func (c *Collector) Check(context.Context) error {
143
+ if err := c.client.Login(); err != nil {
144
+ return err
145
+ }
146
+ if err := c.client.SetLocale(); err != nil {
147
+ c.Warningf("error setting locale: %v", err)
148
+ }
149
+ return c.discovery()
150
+}
151
+
152
+func (c *Collector) Collect(context.Context) error {
153
+ return c.collect()
154
+}
155
+
156
+func (c *Collector) MetricStore() metrix.CollectorStore { return c.store }
157
+
158
+func (c *Collector) ChartTemplateYAML() string { return chartTemplateYAML }
159
+
160
+func (c *Collector) Cleanup(context.Context) {}
src/go/plugin/go.d/collector/powervault/collector_test.go
new
+395
@@ -0,0 +1,395 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+import (
6
+ "context"
7
+ "net/http"
8
+ "net/http/httptest"
9
+ "os"
10
+ "strings"
11
+ "testing"
12
+
13
+ "github.com/netdata/netdata/go/plugins/pkg/metrix"
14
+ "github.com/netdata/netdata/go/plugins/pkg/web"
15
+ "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
16
+ "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
17
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
18
+ "github.com/stretchr/testify/assert"
19
+ "github.com/stretchr/testify/require"
20
+)
21
+
22
+func TestCollector_Init(t *testing.T) {
23
+ tests := map[string]struct {
24
+ config Config
25
+ wantFail bool
26
+ }{
27
+ "success with valid config": {
28
+ config: Config{
29
+ HTTPConfig: web.HTTPConfig{
30
+ RequestConfig: web.RequestConfig{
31
+ URL: "https://127.0.0.1",
32
+ Username: "admin",
33
+ Password: "password",
34
+ },
35
+ },
36
+ },
37
+ },
38
+ "success with md5 digest": {
39
+ config: Config{
40
+ HTTPConfig: web.HTTPConfig{
41
+ RequestConfig: web.RequestConfig{
42
+ URL: "https://127.0.0.1",
43
+ Username: "admin",
44
+ Password: "password",
45
+ },
46
+ },
47
+ AuthDigest: "md5",
48
+ },
49
+ },
50
+ "fail without username": {
51
+ wantFail: true,
52
+ config: Config{
53
+ HTTPConfig: web.HTTPConfig{
54
+ RequestConfig: web.RequestConfig{
55
+ URL: "https://127.0.0.1",
56
+ Password: "password",
57
+ },
58
+ },
59
+ },
60
+ },
61
+ "fail without password": {
62
+ wantFail: true,
63
+ config: Config{
64
+ HTTPConfig: web.HTTPConfig{
65
+ RequestConfig: web.RequestConfig{
66
+ URL: "https://127.0.0.1",
67
+ Username: "admin",
68
+ },
69
+ },
70
+ },
71
+ },
72
+ "fail with invalid digest": {
73
+ wantFail: true,
74
+ config: Config{
75
+ HTTPConfig: web.HTTPConfig{
76
+ RequestConfig: web.RequestConfig{
77
+ URL: "https://127.0.0.1",
78
+ Username: "admin",
79
+ Password: "password",
80
+ },
81
+ },
82
+ AuthDigest: "sha1",
83
+ },
84
+ },
85
+ }
86
+
87
+ for name, test := range tests {
88
+ t.Run(name, func(t *testing.T) {
89
+ collr := New()
90
+ collr.Config = test.config
91
+
92
+ if test.wantFail {
93
+ assert.Error(t, collr.Init(context.Background()))
94
+ } else {
95
+ assert.NoError(t, collr.Init(context.Background()))
96
+ }
97
+ })
98
+ }
99
+}
100
+
101
+func TestCollector_Check(t *testing.T) {
102
+ srv := newMockPowerVaultServer()
103
+ defer srv.Close()
104
+
105
+ collr := New()
106
+ collr.Config = Config{
107
+ HTTPConfig: web.HTTPConfig{
108
+ RequestConfig: web.RequestConfig{
109
+ URL: srv.URL,
110
+ Username: "admin",
111
+ Password: "password",
112
+ },
113
+ },
114
+ }
115
+
116
+ require.NoError(t, collr.Init(context.Background()))
117
+ assert.NoError(t, collr.Check(context.Background()))
118
+}
119
+
120
+func TestCollector_Collect(t *testing.T) {
121
+ srv := newMockPowerVaultServer()
122
+ defer srv.Close()
123
+
124
+ collr := New()
125
+ collr.Config = Config{
126
+ HTTPConfig: web.HTTPConfig{
127
+ RequestConfig: web.RequestConfig{
128
+ URL: srv.URL,
129
+ Username: "admin",
130
+ Password: "password",
131
+ },
132
+ },
133
+ }
134
+
135
+ require.NoError(t, collr.Init(context.Background()))
136
+ require.NoError(t, collr.Check(context.Background()))
137
+
138
+ cc := mustCycleController(t, collr.MetricStore())
139
+ cc.BeginCycle()
140
+ require.NoError(t, collr.Collect(context.Background()))
141
+ cc.CommitCycleSuccess()
142
+
143
+ r := collr.MetricStore().Read(metrix.ReadRaw())
144
+
145
+ // System health
146
+ assertValue(t, r, "system_health", nil, 0)
147
+
148
+ // Hardware health: 2 controllers OK, 3 drives (2 OK + 1 Global SP→OK), 2 fans OK, 2 PSUs OK
149
+ assertValue(t, r, "hw_controller_ok", nil, 2)
150
+ assertValue(t, r, "hw_controller_degraded", nil, 0)
151
+ assertValue(t, r, "hw_controller_fault", nil, 0)
152
+ assertValue(t, r, "hw_controller_unknown", nil, 0)
153
+
154
+ assertValue(t, r, "hw_drive_ok", nil, 3) // 2 healthy + 1 global spare treated as OK
155
+ assertValue(t, r, "hw_drive_degraded", nil, 0)
156
+ assertValue(t, r, "hw_drive_fault", nil, 0)
157
+ assertValue(t, r, "hw_drive_unknown", nil, 0)
158
+
159
+ assertValue(t, r, "hw_fan_ok", nil, 2)
160
+ assertValue(t, r, "hw_psu_ok", nil, 2)
161
+
162
+ // FRUs: 2 OK + 1 N/A (status=5 → OK)
163
+ assertValue(t, r, "hw_fru_ok", nil, 3)
164
+ assertValue(t, r, "hw_fru_degraded", nil, 0)
165
+
166
+ // Ports: 2 OK, 1 with health=4 (N/A) excluded
167
+ assertValue(t, r, "hw_port_ok", nil, 2)
168
+ assertValue(t, r, "hw_port_unknown", nil, 0)
169
+
170
+ // Controller stats
171
+ ctrlA := metrix.Labels{"controller": "controller_a"}
172
+ ctrlB := metrix.Labels{"controller": "controller_b"}
173
+ assertValue(t, r, "controller_iops", ctrlA, 1250)
174
+ assertValue(t, r, "controller_throughput", ctrlA, 52428800)
175
+ assertValue(t, r, "controller_cpu_load", ctrlA, 23.5)
176
+ assertValue(t, r, "controller_write_cache_used", ctrlA, 45)
177
+ assertValue(t, r, "controller_forwarded_cmds", ctrlA, 12500)
178
+ assertValue(t, r, "controller_data_read", ctrlA, 107374182400)
179
+ assertValue(t, r, "controller_data_written", ctrlA, 53687091200)
180
+ assertValue(t, r, "controller_read_ops", ctrlA, 5000000)
181
+ assertValue(t, r, "controller_write_ops", ctrlA, 2500000)
182
+ assertValue(t, r, "controller_write_cache_hits", ctrlA, 1800000)
183
+ assertValue(t, r, "controller_write_cache_misses", ctrlA, 200000)
184
+ assertValue(t, r, "controller_read_cache_hits", ctrlA, 3500000)
185
+ assertValue(t, r, "controller_read_cache_misses", ctrlA, 500000)
186
+ assertHasValue(t, r, "controller_iops", ctrlB)
187
+
188
+ // Volume stats
189
+ v1Labels := metrix.Labels{"volume": "prod-db-01"}
190
+ v2Labels := metrix.Labels{"volume": "prod-app-01"}
191
+ v3Labels := metrix.Labels{"volume": "test-backup"}
192
+ assertValue(t, r, "volume_iops", v1Labels, 850)
193
+ assertValue(t, r, "volume_throughput", v1Labels, 35651584)
194
+ assertValue(t, r, "volume_write_cache_percent", v1Labels, 52)
195
+ assertValue(t, r, "volume_tier_ssd", v1Labels, 60)
196
+ assertValue(t, r, "volume_tier_sas", v1Labels, 30)
197
+ assertValue(t, r, "volume_tier_sata", v1Labels, 10)
198
+ assertHasValue(t, r, "volume_iops", v2Labels)
199
+ assertHasValue(t, r, "volume_iops", v3Labels)
200
+
201
+ // Port stats
202
+ pA0 := metrix.Labels{"port": "hostport_A0"}
203
+ pA1 := metrix.Labels{"port": "hostport_A1"}
204
+ assertValue(t, r, "port_read_ops", pA0, 8500000)
205
+ assertValue(t, r, "port_write_ops", pA0, 4200000)
206
+ assertValue(t, r, "port_data_read", pA0, 180388626432)
207
+ assertValue(t, r, "port_data_written", pA0, 90194313216)
208
+ assertHasValue(t, r, "port_read_ops", pA1)
209
+
210
+ // PHY errors (aggregated per port: hostport_A0 has phy0+phy1, hostport_A1 has phy0)
211
+ phyA0 := metrix.Labels{"port": "hostport_A0"}
212
+ phyA1 := metrix.Labels{"port": "hostport_A1"}
213
+ assertValue(t, r, "phy_disparity_errors", phyA0, 8) // 5+3
214
+ assertValue(t, r, "phy_lost_dwords", phyA0, 2) // 2+0
215
+ assertValue(t, r, "phy_invalid_dwords", phyA0, 1) // 1+0
216
+ assertValue(t, r, "phy_disparity_errors", phyA1, 0)
217
+ assertValue(t, r, "phy_lost_dwords", phyA1, 1)
218
+
219
+ // Pool capacity (values × 512 to convert from blocks to bytes)
220
+ poolA := metrix.Labels{"pool": "Pool-A"}
221
+ poolB := metrix.Labels{"pool": "Pool-B"}
222
+ assertValue(t, r, "pool_total_bytes", poolA, 19531250000*512)
223
+ assertValue(t, r, "pool_available_bytes", poolA, 8789062500*512)
224
+ assertHasValue(t, r, "pool_total_bytes", poolB)
225
+
226
+ // Drive metrics
227
+ d00 := metrix.Labels{"drive": "0.0"}
228
+ d01 := metrix.Labels{"drive": "0.1"}
229
+ assertValue(t, r, "drive_temperature", d00, 32)
230
+ assertValue(t, r, "drive_power_on_hours", d00, 18750)
231
+ // HDD: ssd-life-left=255 → not reported
232
+ _, hasSSDLife := r.Value("drive_ssd_life_left", d00)
233
+ assert.False(t, hasSSDLife, "HDD should not report SSD life left")
234
+ // SSD: ssd-life-left=97 → reported
235
+ assertValue(t, r, "drive_ssd_life_left", d01, 97)
236
+
237
+ // Sensor metrics
238
+ tempSensor := metrix.Labels{"sensor": "sensor_temp_ctrl_A.1"}
239
+ assertValue(t, r, "sensor_temperature", tempSensor, 42)
240
+
241
+ voltSensor := metrix.Labels{"sensor": "sensor_volt_ctrl_A.1"}
242
+ assertValue(t, r, "sensor_voltage", voltSensor, 3300) // 3.30V → 3300mV
243
+
244
+ currSensor := metrix.Labels{"sensor": "sensor_curr_ctrl_A.1"}
245
+ assertValue(t, r, "sensor_current", currSensor, 1250) // 1.25A → 1250mA
246
+
247
+ chrgSensor := metrix.Labels{"sensor": "sensor_chrg_ctrl_A.1"}
248
+ assertValue(t, r, "sensor_charge_capacity", chrgSensor, 95)
249
+
250
+ // Chart coverage
251
+ collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
252
+}
253
+
254
+func TestCollector_CollectWithVolumeSelector(t *testing.T) {
255
+ srv := newMockPowerVaultServer()
256
+ defer srv.Close()
257
+
258
+ collr := New()
259
+ collr.Config = Config{
260
+ HTTPConfig: web.HTTPConfig{
261
+ RequestConfig: web.RequestConfig{
262
+ URL: srv.URL,
263
+ Username: "admin",
264
+ Password: "password",
265
+ },
266
+ },
267
+ VolumeSelector: "prod-*",
268
+ }
269
+
270
+ require.NoError(t, collr.Init(context.Background()))
271
+ require.NoError(t, collr.Check(context.Background()))
272
+
273
+ cc := mustCycleController(t, collr.MetricStore())
274
+ cc.BeginCycle()
275
+ require.NoError(t, collr.Collect(context.Background()))
276
+ cc.CommitCycleSuccess()
277
+
278
+ r := collr.MetricStore().Read(metrix.ReadRaw())
279
+
280
+ // prod-* volumes should be included
281
+ assertHasValue(t, r, "volume_iops", metrix.Labels{"volume": "prod-db-01"})
282
+ assertHasValue(t, r, "volume_iops", metrix.Labels{"volume": "prod-app-01"})
283
+ // test-backup should be excluded
284
+ _, ok := r.Value("volume_iops", metrix.Labels{"volume": "test-backup"})
285
+ assert.False(t, ok, "test-backup should be excluded by volume selector")
286
+}
287
+
288
+func TestCollector_Cleanup(t *testing.T) {
289
+ collr := New()
290
+ assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
291
+
292
+ srv := newMockPowerVaultServer()
293
+ defer srv.Close()
294
+
295
+ collr.Config = Config{
296
+ HTTPConfig: web.HTTPConfig{
297
+ RequestConfig: web.RequestConfig{
298
+ URL: srv.URL,
299
+ Username: "admin",
300
+ Password: "password",
301
+ },
302
+ },
303
+ }
304
+
305
+ require.NoError(t, collr.Init(context.Background()))
306
+ require.NoError(t, collr.Check(context.Background()))
307
+ assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
308
+}
309
+
310
+func TestCollector_ChartTemplateYAML(t *testing.T) {
311
+ templateYAML := New().ChartTemplateYAML()
312
+ collecttest.AssertChartTemplateSchema(t, templateYAML)
313
+
314
+ spec, err := charttpl.DecodeYAML([]byte(templateYAML))
315
+ require.NoError(t, err)
316
+ require.NoError(t, spec.Validate())
317
+
318
+ _, err = chartengine.Compile(spec, 1)
319
+ require.NoError(t, err)
320
+}
321
+
322
+func mustCycleController(t *testing.T, store metrix.CollectorStore) metrix.CycleController {
323
+ t.Helper()
324
+ managed, ok := metrix.AsCycleManagedStore(store)
325
+ require.True(t, ok, "store does not expose cycle control")
326
+ return managed.CycleController()
327
+}
328
+
329
+func assertValue(t *testing.T, r metrix.Reader, name string, labels metrix.Labels, want float64) {
330
+ t.Helper()
331
+ got, ok := r.Value(name, labels)
332
+ require.Truef(t, ok, "expected metric %s labels=%v", name, labels)
333
+ assert.InDeltaf(t, want, got, 1e-9, "unexpected value for %s labels=%v: got %v want %v", name, labels, got, want)
334
+}
335
+
336
+func assertHasValue(t *testing.T, r metrix.Reader, name string, labels metrix.Labels) {
337
+ t.Helper()
338
+ _, ok := r.Value(name, labels)
339
+ assert.Truef(t, ok, "expected metric %s labels=%v to exist", name, labels)
340
+}
341
+
342
+func newMockPowerVaultServer() *httptest.Server {
343
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
344
+ w.Header().Set("Content-Type", "application/json")
345
+
346
+ p := r.URL.Path
347
+
348
+ switch {
349
+ case strings.Contains(p, "/api/login/"):
350
+ writeTestData(w, "testdata/login.json")
351
+ case strings.HasSuffix(p, "/api/set/cli-parameters/locale/English"):
352
+ w.WriteHeader(http.StatusOK)
353
+ _, _ = w.Write([]byte(`{"status":[{"response-type":"Success","response":"Command completed successfully.","return-code":0,"response-type-numeric":0}]}`))
354
+ case strings.HasSuffix(p, "/api/show/system"):
355
+ writeTestData(w, "testdata/system.json")
356
+ case strings.HasSuffix(p, "/api/show/controllers"):
357
+ writeTestData(w, "testdata/controllers.json")
358
+ case strings.HasSuffix(p, "/api/show/disks"):
359
+ writeTestData(w, "testdata/drives.json")
360
+ case strings.HasSuffix(p, "/api/show/fans"):
361
+ writeTestData(w, "testdata/fans.json")
362
+ case strings.HasSuffix(p, "/api/show/power-supplies"):
363
+ writeTestData(w, "testdata/power_supplies.json")
364
+ case strings.HasSuffix(p, "/api/show/sensor-status"):
365
+ writeTestData(w, "testdata/sensors.json")
366
+ case strings.HasSuffix(p, "/api/show/frus"):
367
+ writeTestData(w, "testdata/frus.json")
368
+ case strings.HasSuffix(p, "/api/show/volumes"):
369
+ writeTestData(w, "testdata/volumes.json")
370
+ case strings.HasSuffix(p, "/api/show/pools"):
371
+ writeTestData(w, "testdata/pools.json")
372
+ case strings.HasSuffix(p, "/api/show/ports"):
373
+ writeTestData(w, "testdata/ports.json")
374
+ case strings.HasSuffix(p, "/api/show/controller-statistics"):
375
+ writeTestData(w, "testdata/controller_statistics.json")
376
+ case strings.HasSuffix(p, "/api/show/volume-statistics"):
377
+ writeTestData(w, "testdata/volume_statistics.json")
378
+ case strings.HasSuffix(p, "/api/show/host-port-statistics"):
379
+ writeTestData(w, "testdata/port_statistics.json")
380
+ case strings.HasSuffix(p, "/api/show/host-phy-statistics"):
381
+ writeTestData(w, "testdata/phy_statistics.json")
382
+ default:
383
+ w.WriteHeader(http.StatusNotFound)
384
+ }
385
+ }))
386
+}
387
+
388
+func writeTestData(w http.ResponseWriter, path string) {
389
+ data, err := os.ReadFile(path)
390
+ if err != nil {
391
+ w.WriteHeader(http.StatusInternalServerError)
392
+ return
393
+ }
394
+ _, _ = w.Write(data)
395
+}
src/go/plugin/go.d/collector/powervault/config_schema.json
new
+200
@@ -0,0 +1,200 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Dell PowerVault ME4/ME5 collector configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 30
13
+ },
14
+ "autodetection_retry": {
15
+ "title": "Detection retry",
16
+ "description": "Recheck interval in seconds. Zero means no recheck will be scheduled.",
17
+ "type": "integer",
18
+ "minimum": 0,
19
+ "default": 0
20
+ },
21
+ "url": {
22
+ "title": "URL",
23
+ "description": "The base URL of the PowerVault Management Controller Interface (MCI) API.",
24
+ "type": "string",
25
+ "default": "https://127.0.0.1",
26
+ "format": "uri"
27
+ },
28
+ "timeout": {
29
+ "title": "Timeout",
30
+ "description": "The timeout in seconds for HTTP requests.",
31
+ "type": "number",
32
+ "minimum": 0.5,
33
+ "default": 30
34
+ },
35
+ "not_follow_redirects": {
36
+ "title": "Not follow redirects",
37
+ "description": "If set, the client will not follow HTTP redirects automatically.",
38
+ "type": "boolean"
39
+ },
40
+ "username": {
41
+ "title": "Username",
42
+ "description": "The username for PowerVault MCI API authentication.",
43
+ "type": "string",
44
+ "sensitive": true
45
+ },
46
+ "password": {
47
+ "title": "Password",
48
+ "description": "The password for PowerVault MCI API authentication.",
49
+ "type": "string",
50
+ "sensitive": true
51
+ },
52
+ "auth_digest": {
53
+ "title": "Auth digest",
54
+ "description": "The hash algorithm used for authentication. Use sha256 (default) or md5 (legacy ME4 firmware before G280).",
55
+ "type": "string",
56
+ "enum": [
57
+ "sha256",
58
+ "md5"
59
+ ],
60
+ "default": "sha256"
61
+ },
62
+ "volume_selector": {
63
+ "title": "Volume selector",
64
+ "description": "Specifies a space-separated list of volume name patterns to monitor. Use * for wildcards, prefix with ! to exclude.",
65
+ "type": "string",
66
+ "default": ""
67
+ },
68
+ "proxy_url": {
69
+ "title": "Proxy URL",
70
+ "description": "The URL of the proxy server.",
71
+ "type": "string"
72
+ },
73
+ "proxy_username": {
74
+ "title": "Proxy username",
75
+ "description": "The username for proxy authentication.",
76
+ "type": "string",
77
+ "sensitive": true
78
+ },
79
+ "proxy_password": {
80
+ "title": "Proxy password",
81
+ "description": "The password for proxy authentication.",
82
+ "type": "string",
83
+ "sensitive": true
84
+ },
85
+ "headers": {
86
+ "title": "Headers",
87
+ "description": "Additional HTTP headers to include in requests.",
88
+ "type": [
89
+ "object",
90
+ "null"
91
+ ],
92
+ "additionalProperties": {
93
+ "type": "string"
94
+ }
95
+ },
96
+ "tls_skip_verify": {
97
+ "title": "Skip TLS verification",
98
+ "description": "If set, TLS certificate verification will be skipped.",
99
+ "type": "boolean"
100
+ },
101
+ "tls_ca": {
102
+ "title": "TLS CA",
103
+ "description": "The path to the CA certificate file for TLS verification.",
104
+ "type": "string",
105
+ "pattern": "^$|^/"
106
+ },
107
+ "tls_cert": {
108
+ "title": "TLS certificate",
109
+ "description": "The path to the client TLS certificate file.",
110
+ "type": "string",
111
+ "pattern": "^$|^/"
112
+ },
113
+ "tls_key": {
114
+ "title": "TLS key",
115
+ "description": "The path to the client TLS key file.",
116
+ "type": "string",
117
+ "pattern": "^$|^/"
118
+ },
119
+ "vnode": {
120
+ "title": "Virtual Node",
121
+ "description": "Associates this data collection job with a [Virtual Node](https://learn.netdata.cloud/docs/netdata-agent/configuration/organize-systems-metrics-and-alerts#virtual-nodes).",
122
+ "type": "string"
123
+ }
124
+ },
125
+ "required": [
126
+ "url",
127
+ "username",
128
+ "password"
129
+ ],
130
+ "additionalProperties": false,
131
+ "patternProperties": {
132
+ "^name$": {}
133
+ }
134
+ },
135
+ "uiSchema": {
136
+ "uiOptions": {
137
+ "fullPage": true
138
+ },
139
+ "url": {
140
+ "ui:placeholder": "https://127.0.0.1"
141
+ },
142
+ "timeout": {
143
+ "ui:help": "Accepts decimals for sub-second values (e.g., 0.5 = 500ms)."
144
+ },
145
+ "password": {
146
+ "ui:widget": "password"
147
+ },
148
+ "proxy_password": {
149
+ "ui:widget": "password"
150
+ },
151
+ "ui:flavour": "tabs",
152
+ "ui:options": {
153
+ "tabs": [
154
+ {
155
+ "title": "Base",
156
+ "fields": [
157
+ "update_every",
158
+ "autodetection_retry",
159
+ "url",
160
+ "timeout",
161
+ "not_follow_redirects",
162
+ "volume_selector",
163
+ "vnode"
164
+ ]
165
+ },
166
+ {
167
+ "title": "Auth",
168
+ "fields": [
169
+ "username",
170
+ "password",
171
+ "auth_digest"
172
+ ]
173
+ },
174
+ {
175
+ "title": "TLS",
176
+ "fields": [
177
+ "tls_skip_verify",
178
+ "tls_ca",
179
+ "tls_cert",
180
+ "tls_key"
181
+ ]
182
+ },
183
+ {
184
+ "title": "Proxy",
185
+ "fields": [
186
+ "proxy_url",
187
+ "proxy_username",
188
+ "proxy_password"
189
+ ]
190
+ },
191
+ {
192
+ "title": "Headers",
193
+ "fields": [
194
+ "headers"
195
+ ]
196
+ }
197
+ ]
198
+ }
199
+ }
200
+}
src/go/plugin/go.d/collector/powervault/metadata.yaml
new
+555
@@ -0,0 +1,555 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-powervault
5
+ plugin_name: go.d.plugin
6
+ module_name: powervault
7
+ monitored_instance:
8
+ name: Dell PowerVault ME4/ME5
9
+ link: https://www.dell.com/en-us/shop/povw/dell-powervault-me5
10
+ icon_filename: dell.svg
11
+ categories:
12
+ - data-collection.storage
13
+ keywords:
14
+ - powervault
15
+ - dell
16
+ - storage
17
+ - me4
18
+ - me5
19
+ - san
20
+ - das
21
+ related_resources:
22
+ integrations:
23
+ list: []
24
+ info_provided_to_referring_integrations:
25
+ description: ""
26
+ overview:
27
+ data_collection:
28
+ metrics_description: |
29
+ This collector monitors Dell PowerVault ME4 and ME5 storage arrays via the Management Controller Interface (MCI) REST API.
30
+
31
+ It collects metrics for the following components:
32
+
33
+ - **System Health** — overall array health status
34
+ - **Hardware Health** — component-level status counts (controllers, drives, fans, PSUs, FRUs, ports) by health state (OK, degraded, fault, unknown)
35
+ - **Controller Performance** — IOPS, throughput, CPU load, write cache usage, forwarded commands, cumulative I/O and cache hit/miss statistics
36
+ - **Volume Performance** — IOPS, throughput, write cache usage, cumulative I/O, cache hit/miss statistics, storage tier distribution (SSD/SAS/SATA)
37
+ - **Port I/O** — cumulative read/write operations and data transferred per host port
38
+ - **SAS PHY Errors** — disparity errors, lost dwords, invalid dwords aggregated per port
39
+ - **Pool Capacity** — total and available bytes per storage pool
40
+ - **Drive Metrics** — temperature, power-on hours, SSD life remaining per drive
41
+ - **Sensor Readings** — temperature, voltage, current, and charge capacity per sensor
42
+ method_description: |
43
+ The collector uses the MCI REST API (`/api/show/*` and `/api/show/*-statistics` endpoints) to fetch data.
44
+ Authentication uses a hash-based login (SHA-256 by default, MD5 for legacy firmware) — the collector
45
+ hashes `username_password`, calls `/api/login/<hash>`, and uses the returned session key for subsequent requests.
46
+
47
+ Discovery runs on startup and then every 10 collection cycles to refresh the hardware inventory.
48
+ Performance statistics (controllers, volumes, ports, PHY) are collected via concurrent API calls
49
+ each cycle. Hardware health, drive metrics, sensor readings, pool capacity, and system health
50
+ are computed from the cached discovery data without additional API calls.
51
+ supported_platforms:
52
+ include: []
53
+ exclude: []
54
+ multi_instance: true
55
+ additional_permissions:
56
+ description: |
57
+ A user account on the PowerVault management controller is required. Any role with API read access
58
+ is sufficient (e.g., the built-in `monitor` role). The account does not need write permissions.
59
+ default_behavior:
60
+ auto_detection:
61
+ description: |
62
+ The collector does not auto-detect PowerVault arrays. You must configure the management IP address,
63
+ username, and password explicitly.
64
+ limits:
65
+ description: |
66
+ The collector makes approximately 14 API calls per collection cycle (10 for discovery when needed,
67
+ 4 for statistics). With the default 30-second collection interval, this is well within the MCI API's
68
+ capabilities. Each API call returns all entities of that type in a single response.
69
+ performance_impact:
70
+ description: |
71
+ The MCI API is lightweight and read-only. The collector's impact on the storage array is negligible.
72
+ Session keys are reused across collection cycles and automatically refreshed on expiry (HTTP 401).
73
+ setup:
74
+ prerequisites:
75
+ list:
76
+ - title: PowerVault API access
77
+ description: |
78
+ 1. A user account on the PowerVault management controller with at least read-only access.
79
+ The built-in `monitor` role is sufficient.
80
+ 2. Network connectivity from the Netdata Agent to the PowerVault management IP (HTTPS, port 443).
81
+ 3. The management controller must have its REST API enabled (enabled by default on ME4/ME5).
82
+ - title: TLS certificate
83
+ description: |
84
+ PowerVault controllers typically use self-signed TLS certificates. You will likely need to set
85
+ `tls_skip_verify: yes` in the configuration, or provide the controller's CA certificate via `tls_ca`.
86
+ configuration:
87
+ file:
88
+ name: go.d/powervault.conf
89
+ options:
90
+ description: |
91
+ The following options can be defined globally: update_every, autodetection_retry.
92
+ folding:
93
+ title: Config options
94
+ enabled: true
95
+ list:
96
+ - name: update_every
97
+ description: Data collection interval (seconds).
98
+ default_value: 30
99
+ required: false
100
+ group: Collection
101
+ - name: autodetection_retry
102
+ description: Autodetection retry interval (seconds). Set 0 to disable.
103
+ default_value: 0
104
+ required: false
105
+ group: Collection
106
+
107
+ - name: url
108
+ description: PowerVault management controller URL.
109
+ default_value: https://127.0.0.1
110
+ required: true
111
+ group: Target
112
+ - name: timeout
113
+ description: HTTP request timeout (seconds).
114
+ default_value: 30
115
+ required: false
116
+ group: Target
117
+
118
+ - name: username
119
+ description: Username for MCI API authentication.
120
+ default_value: ""
121
+ required: true
122
+ group: HTTP Auth
123
+ - name: password
124
+ description: Password for MCI API authentication.
125
+ default_value: ""
126
+ required: true
127
+ group: HTTP Auth
128
+ - name: auth_digest
129
+ description: "Hash algorithm for authentication. Use `sha256` (default) or `md5` (legacy ME4 firmware < G280)."
130
+ default_value: sha256
131
+ required: false
132
+ group: HTTP Auth
133
+
134
+ - name: volume_selector
135
+ description: |
136
+ Space-separated [simple patterns](https://github.com/netdata/netdata/tree/master/src/libnetdata/simple_pattern#readme)
137
+ to filter which volumes to monitor. Use `*` for wildcards, `!` prefix to exclude.
138
+ An empty value monitors all volumes.
139
+ default_value: ""
140
+ required: false
141
+ group: Filtering
142
+
143
+ - name: tls_skip_verify
144
+ description: Skip TLS certificate and hostname verification (insecure, but often needed for self-signed certs on storage controllers).
145
+ default_value: no
146
+ required: false
147
+ group: TLS
148
+ - name: tls_ca
149
+ description: Path to CA bundle used to validate the server certificate.
150
+ default_value: ""
151
+ required: false
152
+ group: TLS
153
+ - name: tls_cert
154
+ description: Path to client TLS certificate (for mTLS).
155
+ default_value: ""
156
+ required: false
157
+ group: TLS
158
+ - name: tls_key
159
+ description: Path to client TLS private key (for mTLS).
160
+ default_value: ""
161
+ required: false
162
+ group: TLS
163
+
164
+ - name: proxy_url
165
+ description: HTTP proxy URL.
166
+ default_value: ""
167
+ required: false
168
+ group: Proxy
169
+ - name: proxy_username
170
+ description: Username for proxy Basic HTTP authentication.
171
+ default_value: ""
172
+ required: false
173
+ group: Proxy
174
+ - name: proxy_password
175
+ description: Password for proxy Basic HTTP authentication.
176
+ default_value: ""
177
+ required: false
178
+ group: Proxy
179
+
180
+ - name: headers
181
+ description: "Additional HTTP headers (one per line as key: value)."
182
+ default_value: ""
183
+ required: false
184
+ group: Request
185
+ - name: not_follow_redirects
186
+ description: Do not follow HTTP redirects.
187
+ default_value: no
188
+ required: false
189
+ group: Request
190
+
191
+ - name: vnode
192
+ description: Associates this data collection job with a [Virtual Node](https://learn.netdata.cloud/docs/netdata-agent/configuration/organize-systems-metrics-and-alerts#virtual-nodes).
193
+ default_value: ""
194
+ required: false
195
+ group: Virtual Node
196
+ examples:
197
+ folding:
198
+ title: Config
199
+ enabled: true
200
+ list:
201
+ - name: Basic
202
+ description: A minimal configuration connecting to a PowerVault controller with a self-signed certificate.
203
+ config: |
204
+ jobs:
205
+ - name: pv-lab
206
+ url: https://10.0.0.1
207
+ username: monitor
208
+ password: monitor123
209
+ tls_skip_verify: yes
210
+ - name: With volume filtering
211
+ description: Monitor only production volumes, excluding temporary and test volumes.
212
+ config: |
213
+ jobs:
214
+ - name: pv-prod
215
+ url: https://10.0.0.1
216
+ username: monitor
217
+ password: monitor123
218
+ tls_skip_verify: yes
219
+ volume_selector: "prod-* !*-temp !*-test"
220
+ - name: Legacy ME4 with MD5 auth
221
+ description: Connecting to an older ME4 controller that requires MD5 authentication (firmware before G280).
222
+ config: |
223
+ jobs:
224
+ - name: pv-legacy
225
+ url: https://10.0.0.2
226
+ username: manage
227
+ password: manage123
228
+ auth_digest: md5
229
+ tls_skip_verify: yes
230
+ - name: Multi-instance
231
+ description: |
232
+ > **Note**: When you define multiple jobs, their names must be unique.
233
+
234
+ Monitoring multiple PowerVault arrays.
235
+ config: |
236
+ jobs:
237
+ - name: pv-site-a
238
+ url: https://10.0.0.1
239
+ username: monitor
240
+ password: monitor123
241
+ tls_skip_verify: yes
242
+
243
+ - name: pv-site-b
244
+ url: https://10.0.0.2
245
+ username: monitor
246
+ password: monitor456
247
+ tls_skip_verify: yes
248
+ troubleshooting:
249
+ problems:
250
+ list:
251
+ - name: Authentication failure
252
+ description: |
253
+ If the collector fails with `login: authentication failed`:
254
+
255
+ 1. Verify the username and password are correct.
256
+ 2. Check the `auth_digest` setting. ME4 firmware before G280 requires `auth_digest: md5`.
257
+ ME5 and newer ME4 firmware use `sha256` (the default).
258
+ 3. Ensure the user account is not locked or expired on the controller.
259
+ - name: Connection refused or timeout
260
+ description: |
261
+ If the collector fails with connection errors:
262
+
263
+ 1. Verify the `url` points to the management controller IP (not a data port).
264
+ 2. Ensure HTTPS port 443 is reachable from the Netdata Agent host.
265
+ 3. If using a firewall, allow outbound HTTPS to the controller.
266
+ 4. Try increasing `timeout` if the controller is on a high-latency link.
267
+ - name: TLS certificate errors
268
+ description: |
269
+ PowerVault controllers ship with self-signed certificates. If you see TLS errors:
270
+
271
+ 1. Set `tls_skip_verify: yes` in the configuration (most common solution).
272
+ 2. Alternatively, export the controller's certificate and provide it via `tls_ca`.
273
+ alerts: []
274
+ metrics:
275
+ folding:
276
+ title: Metrics
277
+ enabled: false
278
+ description: ""
279
+ availability: []
280
+ scopes:
281
+ - name: global
282
+ description: These metrics refer to the entire monitored PowerVault array.
283
+ labels: []
284
+ metrics:
285
+ - name: powervault.system_health
286
+ description: System Health Status
287
+ unit: status
288
+ chart_type: line
289
+ dimensions:
290
+ - name: health
291
+ - name: powervault.hw_health_controller
292
+ description: Controller Health Status
293
+ unit: controllers
294
+ chart_type: stacked
295
+ dimensions:
296
+ - name: ok
297
+ - name: degraded
298
+ - name: fault
299
+ - name: unknown
300
+ - name: powervault.hw_health_drive
301
+ description: Drive Health Status
302
+ unit: drives
303
+ chart_type: stacked
304
+ dimensions:
305
+ - name: ok
306
+ - name: degraded
307
+ - name: fault
308
+ - name: unknown
309
+ - name: powervault.hw_health_fan
310
+ description: Fan Health Status
311
+ unit: fans
312
+ chart_type: stacked
313
+ dimensions:
314
+ - name: ok
315
+ - name: degraded
316
+ - name: fault
317
+ - name: unknown
318
+ - name: powervault.hw_health_psu
319
+ description: Power Supply Health Status
320
+ unit: PSUs
321
+ chart_type: stacked
322
+ dimensions:
323
+ - name: ok
324
+ - name: degraded
325
+ - name: fault
326
+ - name: unknown
327
+ - name: powervault.hw_health_fru
328
+ description: FRU Health Status
329
+ unit: FRUs
330
+ chart_type: stacked
331
+ dimensions:
332
+ - name: ok
333
+ - name: degraded
334
+ - name: fault
335
+ - name: unknown
336
+ - name: powervault.hw_health_port
337
+ description: Port Health Status
338
+ unit: ports
339
+ chart_type: stacked
340
+ dimensions:
341
+ - name: ok
342
+ - name: degraded
343
+ - name: fault
344
+ - name: unknown
345
+ - name: controller
346
+ description: These metrics refer to individual RAID controllers (typically controller_a and controller_b in a dual-controller configuration).
347
+ labels:
348
+ - name: controller
349
+ description: Controller durable ID (e.g., `controller_a`, `controller_b`).
350
+ metrics:
351
+ - name: powervault.controller_iops
352
+ description: Controller IOPS
353
+ unit: ops/s
354
+ chart_type: line
355
+ dimensions:
356
+ - name: iops
357
+ - name: powervault.controller_throughput
358
+ description: Controller Throughput
359
+ unit: bytes/s
360
+ chart_type: line
361
+ dimensions:
362
+ - name: throughput
363
+ - name: powervault.controller_cpu_load
364
+ description: Controller CPU Load
365
+ unit: percentage
366
+ chart_type: line
367
+ dimensions:
368
+ - name: cpu_load
369
+ - name: powervault.controller_write_cache_used
370
+ description: Controller Write Cache Usage
371
+ unit: percentage
372
+ chart_type: line
373
+ dimensions:
374
+ - name: write_cache_used
375
+ - name: powervault.controller_forwarded_cmds
376
+ description: Controller Forwarded Commands
377
+ unit: commands
378
+ chart_type: line
379
+ dimensions:
380
+ - name: forwarded
381
+ - name: powervault.controller_io
382
+ description: Controller Cumulative I/O
383
+ unit: bytes
384
+ chart_type: area
385
+ dimensions:
386
+ - name: read
387
+ - name: written
388
+ - name: powervault.controller_ops
389
+ description: Controller Cumulative Operations
390
+ unit: ops
391
+ chart_type: area
392
+ dimensions:
393
+ - name: read
394
+ - name: write
395
+ - name: powervault.controller_cache_hits
396
+ description: Controller Cache Hits and Misses
397
+ unit: hits
398
+ chart_type: stacked
399
+ dimensions:
400
+ - name: read_hits
401
+ - name: read_misses
402
+ - name: write_hits
403
+ - name: write_misses
404
+ - name: volume
405
+ description: These metrics refer to individual storage volumes. Volumes can be filtered using the `volume_selector` configuration option.
406
+ labels:
407
+ - name: volume
408
+ description: Volume name (e.g., `prod-db-01`).
409
+ metrics:
410
+ - name: powervault.volume_iops
411
+ description: Volume IOPS
412
+ unit: ops/s
413
+ chart_type: line
414
+ dimensions:
415
+ - name: iops
416
+ - name: powervault.volume_throughput
417
+ description: Volume Throughput
418
+ unit: bytes/s
419
+ chart_type: line
420
+ dimensions:
421
+ - name: throughput
422
+ - name: powervault.volume_write_cache_percent
423
+ description: Volume Write Cache Usage
424
+ unit: percentage
425
+ chart_type: line
426
+ dimensions:
427
+ - name: write_cache
428
+ - name: powervault.volume_io
429
+ description: Volume Cumulative I/O
430
+ unit: bytes
431
+ chart_type: area
432
+ dimensions:
433
+ - name: read
434
+ - name: written
435
+ - name: powervault.volume_ops
436
+ description: Volume Cumulative Operations
437
+ unit: ops
438
+ chart_type: area
439
+ dimensions:
440
+ - name: read
441
+ - name: write
442
+ - name: powervault.volume_cache_hits
443
+ description: Volume Cache Hits and Misses
444
+ unit: hits
445
+ chart_type: stacked
446
+ dimensions:
447
+ - name: read_hits
448
+ - name: read_misses
449
+ - name: write_hits
450
+ - name: write_misses
451
+ - name: powervault.volume_tier_distribution
452
+ description: Volume Storage Tier Distribution
453
+ unit: percentage
454
+ chart_type: stacked
455
+ dimensions:
456
+ - name: ssd
457
+ - name: sas
458
+ - name: sata
459
+ - name: port
460
+ description: These metrics refer to individual host ports (SAS/FC ports used for host connectivity).
461
+ labels:
462
+ - name: port
463
+ description: Port durable ID (e.g., `hostport_A0`).
464
+ metrics:
465
+ - name: powervault.port_io
466
+ description: Port Cumulative I/O
467
+ unit: bytes
468
+ chart_type: area
469
+ dimensions:
470
+ - name: read
471
+ - name: written
472
+ - name: powervault.port_ops
473
+ description: Port Cumulative Operations
474
+ unit: ops
475
+ chart_type: area
476
+ dimensions:
477
+ - name: read
478
+ - name: write
479
+ - name: powervault.phy_errors
480
+ description: SAS PHY Errors
481
+ unit: errors
482
+ chart_type: stacked
483
+ dimensions:
484
+ - name: disparity
485
+ - name: lost_dwords
486
+ - name: invalid_dwords
487
+ - name: pool
488
+ description: These metrics refer to individual storage pools (also known as disk groups or virtual pools).
489
+ labels:
490
+ - name: pool
491
+ description: Pool name (e.g., `Pool-A`).
492
+ metrics:
493
+ - name: powervault.pool_capacity
494
+ description: Pool Capacity
495
+ unit: bytes
496
+ chart_type: line
497
+ dimensions:
498
+ - name: total
499
+ - name: available
500
+ - name: drive
501
+ description: These metrics refer to individual physical drives (HDDs and SSDs).
502
+ labels:
503
+ - name: drive
504
+ description: Drive location (e.g., `0.0` for enclosure 0, slot 0).
505
+ metrics:
506
+ - name: powervault.drive_temperature
507
+ description: Drive Temperature
508
+ unit: Celsius
509
+ chart_type: line
510
+ dimensions:
511
+ - name: temperature
512
+ - name: powervault.drive_power_on_hours
513
+ description: Drive Power-On Hours
514
+ unit: hours
515
+ chart_type: line
516
+ dimensions:
517
+ - name: power_on_hours
518
+ - name: powervault.drive_ssd_life_left
519
+ description: Drive SSD Life Remaining
520
+ unit: percentage
521
+ chart_type: line
522
+ dimensions:
523
+ - name: life_left
524
+ - name: sensor
525
+ description: |
526
+ These metrics refer to individual hardware sensors. Sensor types include temperature,
527
+ voltage, current, and charge capacity (supercapacitor/battery backup units).
528
+ labels:
529
+ - name: sensor
530
+ description: Sensor durable ID (e.g., `sensor_temp_ctrl_A.1`).
531
+ metrics:
532
+ - name: powervault.sensor_temperature
533
+ description: Sensor Temperature
534
+ unit: Celsius
535
+ chart_type: line
536
+ dimensions:
537
+ - name: temperature
538
+ - name: powervault.sensor_voltage
539
+ description: Sensor Voltage
540
+ unit: millivolts
541
+ chart_type: line
542
+ dimensions:
543
+ - name: voltage
544
+ - name: powervault.sensor_current
545
+ description: Sensor Current
546
+ unit: milliamps
547
+ chart_type: line
548
+ dimensions:
549
+ - name: current
550
+ - name: powervault.sensor_charge_capacity
551
+ description: Sensor Charge Capacity
552
+ unit: percentage
553
+ chart_type: line
554
+ dimensions:
555
+ - name: charge_capacity
src/go/plugin/go.d/collector/powervault/metrics.go
new
+204
@@ -0,0 +1,204 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package powervault
4
+
5
+import "github.com/netdata/netdata/go/plugins/pkg/metrix"
6
+
7
+type collectorMetrics struct {
8
+ controller controllerGauges
9
+ volume volumeGauges
10
+ port portGauges
11
+ phy phyGauges
12
+ pool poolGauges
13
+ drive driveGauges
14
+ sensor sensorGauges
15
+ hardware hardwareGauges
16
+ system systemGauges
17
+}
18
+
19
+// Controller performance: mix of gauges (instant) and counters (cumulative).
20
+type controllerGauges struct {
21
+ iops metrix.SnapshotGaugeVec
22
+ throughput metrix.SnapshotGaugeVec
23
+ cpuLoad metrix.SnapshotGaugeVec
24
+ writeCacheUsed metrix.SnapshotGaugeVec
25
+ forwardedCmds metrix.SnapshotGaugeVec
26
+ dataRead metrix.SnapshotGaugeVec
27
+ dataWritten metrix.SnapshotGaugeVec
28
+ readOps metrix.SnapshotGaugeVec
29
+ writeOps metrix.SnapshotGaugeVec
30
+ writeCacheHits metrix.SnapshotGaugeVec
31
+ writeCacheMisses metrix.SnapshotGaugeVec
32
+ readCacheHits metrix.SnapshotGaugeVec
33
+ readCacheMisses metrix.SnapshotGaugeVec
34
+}
35
+
36
+// Volume performance: same pattern as controller.
37
+type volumeGauges struct {
38
+ iops metrix.SnapshotGaugeVec
39
+ throughput metrix.SnapshotGaugeVec
40
+ writeCachePercent metrix.SnapshotGaugeVec
41
+ dataRead metrix.SnapshotGaugeVec
42
+ dataWritten metrix.SnapshotGaugeVec
43
+ readOps metrix.SnapshotGaugeVec
44
+ writeOps metrix.SnapshotGaugeVec
45
+ writeCacheHits metrix.SnapshotGaugeVec
46
+ writeCacheMisses metrix.SnapshotGaugeVec
47
+ readCacheHits metrix.SnapshotGaugeVec
48
+ readCacheMisses metrix.SnapshotGaugeVec
49
+ tierSSD metrix.SnapshotGaugeVec
50
+ tierSAS metrix.SnapshotGaugeVec
51
+ tierSATA metrix.SnapshotGaugeVec
52
+}
53
+
54
+// Port I/O counters (cumulative).
55
+type portGauges struct {
56
+ readOps metrix.SnapshotGaugeVec
57
+ writeOps metrix.SnapshotGaugeVec
58
+ dataRead metrix.SnapshotGaugeVec
59
+ dataWritten metrix.SnapshotGaugeVec
60
+}
61
+
62
+// SAS PHY error counters.
63
+type phyGauges struct {
64
+ disparityErrors metrix.SnapshotGaugeVec
65
+ lostDwords metrix.SnapshotGaugeVec
66
+ invalidDwords metrix.SnapshotGaugeVec
67
+}
68
+
69
+// Pool capacity.
70
+type poolGauges struct {
71
+ totalBytes metrix.SnapshotGaugeVec
72
+ availableBytes metrix.SnapshotGaugeVec
73
+}
74
+
75
+// Per-drive metrics.
76
+type driveGauges struct {
77
+ temperature metrix.SnapshotGaugeVec
78
+ powerOnHours metrix.SnapshotGaugeVec
79
+ ssdLifeLeft metrix.SnapshotGaugeVec
80
+}
81
+
82
+// Per-sensor metrics by type.
83
+type sensorGauges struct {
84
+ temperature metrix.SnapshotGaugeVec
85
+ voltage metrix.SnapshotGaugeVec
86
+ current metrix.SnapshotGaugeVec
87
+ chargeCapacity metrix.SnapshotGaugeVec
88
+}
89
+
90
+// Hardware health counts (global).
91
+type hardwareGauges struct {
92
+ controllerOK, controllerDegraded, controllerFault, controllerUnknown metrix.SnapshotGauge
93
+ driveOK, driveDegraded, driveFault, driveUnknown metrix.SnapshotGauge
94
+ fanOK, fanDegraded, fanFault, fanUnknown metrix.SnapshotGauge
95
+ psuOK, psuDegraded, psuFault, psuUnknown metrix.SnapshotGauge
96
+ fruOK, fruDegraded, fruFault, fruUnknown metrix.SnapshotGauge
97
+ portOK, portDegraded, portFault, portUnknown metrix.SnapshotGauge
98
+}
99
+
100
+// System-level health.
101
+type systemGauges struct {
102
+ health metrix.SnapshotGauge
103
+}
104
+
105
+func newCollectorMetrics(store metrix.CollectorStore) *collectorMetrics {
106
+ meter := store.Write().SnapshotMeter("")
107
+
108
+ ctrlVec := meter.Vec("controller")
109
+ volVec := meter.Vec("volume")
110
+ portVec := meter.Vec("port")
111
+ phyVec := meter.Vec("port") // PHY errors aggregated per port
112
+ poolVec := meter.Vec("pool")
113
+ driveVec := meter.Vec("drive")
114
+ sensorVec := meter.Vec("sensor")
115
+
116
+ return &collectorMetrics{
117
+ controller: controllerGauges{
118
+ iops: ctrlVec.Gauge("controller_iops"),
119
+ throughput: ctrlVec.Gauge("controller_throughput"),
120
+ cpuLoad: ctrlVec.Gauge("controller_cpu_load"),
121
+ writeCacheUsed: ctrlVec.Gauge("controller_write_cache_used"),
122
+ forwardedCmds: ctrlVec.Gauge("controller_forwarded_cmds"),
123
+ dataRead: ctrlVec.Gauge("controller_data_read"),
124
+ dataWritten: ctrlVec.Gauge("controller_data_written"),
125
+ readOps: ctrlVec.Gauge("controller_read_ops"),
126
+ writeOps: ctrlVec.Gauge("controller_write_ops"),
127
+ writeCacheHits: ctrlVec.Gauge("controller_write_cache_hits"),
128
+ writeCacheMisses: ctrlVec.Gauge("controller_write_cache_misses"),
129
+ readCacheHits: ctrlVec.Gauge("controller_read_cache_hits"),
130
+ readCacheMisses: ctrlVec.Gauge("controller_read_cache_misses"),
131
+ },
132
+ volume: volumeGauges{
133
+ iops: volVec.Gauge("volume_iops"),
134
+ throughput: volVec.Gauge("volume_throughput"),
135
+ writeCachePercent: volVec.Gauge("volume_write_cache_percent"),
136
+ dataRead: volVec.Gauge("volume_data_read"),
137
+ dataWritten: volVec.Gauge("volume_data_written"),
138
+ readOps: volVec.Gauge("volume_read_ops"),
139
+ writeOps: volVec.Gauge("volume_write_ops"),
140
+ writeCacheHits: volVec.Gauge("volume_write_cache_hits"),
141
+ writeCacheMisses: volVec.Gauge("volume_write_cache_misses"),
142
+ readCacheHits: volVec.Gauge("volume_read_cache_hits"),
143
+ readCacheMisses: volVec.Gauge("volume_read_cache_misses"),
144
+ tierSSD: volVec.Gauge("volume_tier_ssd"),
145
+ tierSAS: volVec.Gauge("volume_tier_sas"),
146
+ tierSATA: volVec.Gauge("volume_tier_sata"),
147
+ },
148
+ port: portGauges{
149
+ readOps: portVec.Gauge("port_read_ops"),
150
+ writeOps: portVec.Gauge("port_write_ops"),
151
+ dataRead: portVec.Gauge("port_data_read"),
152
+ dataWritten: portVec.Gauge("port_data_written"),
153
+ },
154
+ phy: phyGauges{
155
+ disparityErrors: phyVec.Gauge("phy_disparity_errors"),
156
+ lostDwords: phyVec.Gauge("phy_lost_dwords"),
157
+ invalidDwords: phyVec.Gauge("phy_invalid_dwords"),
158
+ },
159
+ pool: poolGauges{
160
+ totalBytes: poolVec.Gauge("pool_total_bytes"),
161
+ availableBytes: poolVec.Gauge("pool_available_bytes"),
162
+ },
163
+ drive: driveGauges{
164
+ temperature: driveVec.Gauge("drive_temperature"),
165
+ powerOnHours: driveVec.Gauge("drive_power_on_hours"),
166
+ ssdLifeLeft: driveVec.Gauge("drive_ssd_life_left"),
167
+ },
168
+ sensor: sensorGauges{
169
+ temperature: sensorVec.Gauge("sensor_temperature"),
170
+ voltage: sensorVec.Gauge("sensor_voltage"),
171
+ current: sensorVec.Gauge("sensor_current"),
172
+ chargeCapacity: sensorVec.Gauge("sensor_charge_capacity"),
173
+ },
174
+ hardware: hardwareGauges{
175
+ controllerOK: meter.Gauge("hw_controller_ok"),
176
+ controllerDegraded: meter.Gauge("hw_controller_degraded"),
177
+ controllerFault: meter.Gauge("hw_controller_fault"),
178
+ controllerUnknown: meter.Gauge("hw_controller_unknown"),
179
+ driveOK: meter.Gauge("hw_drive_ok"),
180
+ driveDegraded: meter.Gauge("hw_drive_degraded"),
181
+ driveFault: meter.Gauge("hw_drive_fault"),
182
+ driveUnknown: meter.Gauge("hw_drive_unknown"),
183
+ fanOK: meter.Gauge("hw_fan_ok"),
184
+ fanDegraded: meter.Gauge("hw_fan_degraded"),
185
+ fanFault: meter.Gauge("hw_fan_fault"),
186
+ fanUnknown: meter.Gauge("hw_fan_unknown"),
187
+ psuOK: meter.Gauge("hw_psu_ok"),
188
+ psuDegraded: meter.Gauge("hw_psu_degraded"),
189
+ psuFault: meter.Gauge("hw_psu_fault"),
190
+ psuUnknown: meter.Gauge("hw_psu_unknown"),
191
+ fruOK: meter.Gauge("hw_fru_ok"),
192
+ fruDegraded: meter.Gauge("hw_fru_degraded"),
193
+ fruFault: meter.Gauge("hw_fru_fault"),
194
+ fruUnknown: meter.Gauge("hw_fru_unknown"),
195
+ portOK: meter.Gauge("hw_port_ok"),
196
+ portDegraded: meter.Gauge("hw_port_degraded"),
197
+ portFault: meter.Gauge("hw_port_fault"),
198
+ portUnknown: meter.Gauge("hw_port_unknown"),
199
+ },
200
+ system: systemGauges{
201
+ health: meter.Gauge("system_health"),
202
+ },
203
+ }
204
+}
src/go/plugin/go.d/collector/powervault/testdata/controller_statistics.json
new
+44
@@ -0,0 +1,44 @@
1
+{
2
+ "controller-statistics": [
3
+ {
4
+ "durable-id": "controller_a",
5
+ "iops": 1250,
6
+ "bytes-per-second-numeric": 52428800,
7
+ "cpu-load": 23.5,
8
+ "data-read-numeric": 107374182400,
9
+ "data-written-numeric": 53687091200,
10
+ "number-of-reads": 5000000,
11
+ "number-of-writes": 2500000,
12
+ "write-cache-used": 45,
13
+ "write-cache-hits": 1800000,
14
+ "write-cache-misses": 200000,
15
+ "read-cache-hits": 3500000,
16
+ "read-cache-misses": 500000,
17
+ "num-forwarded-cmds": 12500
18
+ },
19
+ {
20
+ "durable-id": "controller_b",
21
+ "iops": 980,
22
+ "bytes-per-second-numeric": 41943040,
23
+ "cpu-load": 18.2,
24
+ "data-read-numeric": 85899345920,
25
+ "data-written-numeric": 42949672960,
26
+ "number-of-reads": 4000000,
27
+ "number-of-writes": 2000000,
28
+ "write-cache-used": 32,
29
+ "write-cache-hits": 1400000,
30
+ "write-cache-misses": 160000,
31
+ "read-cache-hits": 2800000,
32
+ "read-cache-misses": 400000,
33
+ "num-forwarded-cmds": 8700
34
+ }
35
+ ],
36
+ "status": [
37
+ {
38
+ "response-type": "Success",
39
+ "response": "Command completed successfully.",
40
+ "return-code": 0,
41
+ "response-type-numeric": 0
42
+ }
43
+ ]
44
+}
src/go/plugin/go.d/collector/powervault/testdata/controllers.json
new
+30
@@ -0,0 +1,30 @@
1
+{
2
+ "controllers": [
3
+ {
4
+ "durable-id": "controller_a",
5
+ "description": "Controller A",
6
+ "status": "Operational",
7
+ "health": "OK",
8
+ "health-numeric": 0,
9
+ "serial-number": "SN-CTRL-A-001",
10
+ "redundancy-status": "Redundant"
11
+ },
12
+ {
13
+ "durable-id": "controller_b",
14
+ "description": "Controller B",
15
+ "status": "Operational",
16
+ "health": "OK",
17
+ "health-numeric": 0,
18
+ "serial-number": "SN-CTRL-B-001",
19
+ "redundancy-status": "Redundant"
20
+ }
21
+ ],
22
+ "status": [
23
+ {
24
+ "response-type": "Success",
25
+ "response": "Command completed successfully.",
26
+ "return-code": 0,
27
+ "response-type-numeric": 0
28
+ }
29
+ ]
30
+}
src/go/plugin/go.d/collector/powervault/testdata/drives.json
new
+87
@@ -0,0 +1,87 @@
1
+{
2
+ "drives": [
3
+ {
4
+ "durable-id": "disk_00.00",
5
+ "location": "0.0",
6
+ "serial-number": "S3TNNA0K100001",
7
+ "model": "ST1800MM0129",
8
+ "vendor": "SEAGATE",
9
+ "description": "Disk 0.0",
10
+ "architecture": "HDD",
11
+ "interface": "SAS",
12
+ "size": "1.8TB",
13
+ "size-numeric": 3518437376,
14
+ "health": "OK",
15
+ "health-numeric": 0,
16
+ "status": "Up",
17
+ "temperature-numeric": 32,
18
+ "ssd-life-left-numeric": 255,
19
+ "power-on-hours": 18750,
20
+ "usage": "VIRTUAL POOL",
21
+ "usage-numeric": 9,
22
+ "enclosure-id": 0,
23
+ "slot": 0,
24
+ "storage-tier": "Performance",
25
+ "disk-group": "dg01",
26
+ "storage-pool-name": "Pool-A"
27
+ },
28
+ {
29
+ "durable-id": "disk_00.01",
30
+ "location": "0.1",
31
+ "serial-number": "S3TNNA0K100002",
32
+ "model": "MZ7LH960HAJR",
33
+ "vendor": "SAMSUNG",
34
+ "description": "Disk 0.1",
35
+ "architecture": "SSD",
36
+ "interface": "SAS",
37
+ "size": "960GB",
38
+ "size-numeric": 1875385344,
39
+ "health": "OK",
40
+ "health-numeric": 0,
41
+ "status": "Up",
42
+ "temperature-numeric": 28,
43
+ "ssd-life-left-numeric": 97,
44
+ "power-on-hours": 12400,
45
+ "usage": "VIRTUAL POOL",
46
+ "usage-numeric": 9,
47
+ "enclosure-id": 0,
48
+ "slot": 1,
49
+ "storage-tier": "Performance",
50
+ "disk-group": "dg01",
51
+ "storage-pool-name": "Pool-A"
52
+ },
53
+ {
54
+ "durable-id": "disk_00.23",
55
+ "location": "0.23",
56
+ "serial-number": "S3TNNA0K100003",
57
+ "model": "ST1800MM0129",
58
+ "vendor": "SEAGATE",
59
+ "description": "Disk 0.23",
60
+ "architecture": "HDD",
61
+ "interface": "SAS",
62
+ "size": "1.8TB",
63
+ "size-numeric": 3518437376,
64
+ "health": "Unknown",
65
+ "health-numeric": 3,
66
+ "status": "Up",
67
+ "temperature-numeric": 30,
68
+ "ssd-life-left-numeric": 255,
69
+ "power-on-hours": 18750,
70
+ "usage": "GLOBAL SP",
71
+ "usage-numeric": 3,
72
+ "enclosure-id": 0,
73
+ "slot": 23,
74
+ "storage-tier": "Performance",
75
+ "disk-group": "",
76
+ "storage-pool-name": ""
77
+ }
78
+ ],
79
+ "status": [
80
+ {
81
+ "response-type": "Success",
82
+ "response": "Command completed successfully.",
83
+ "return-code": 0,
84
+ "response-type-numeric": 0
85
+ }
86
+ ]
87
+}
src/go/plugin/go.d/collector/powervault/testdata/fans.json
new
+30
@@ -0,0 +1,30 @@
1
+{
2
+ "fan": [
3
+ {
4
+ "durable-id": "fan_0.0",
5
+ "name": "Fan 0",
6
+ "location": "Enclosure 0, Left",
7
+ "health": "OK",
8
+ "health-numeric": 0,
9
+ "status": "Up",
10
+ "speed": 4800
11
+ },
12
+ {
13
+ "durable-id": "fan_0.1",
14
+ "name": "Fan 1",
15
+ "location": "Enclosure 0, Right",
16
+ "health": "OK",
17
+ "health-numeric": 0,
18
+ "status": "Up",
19
+ "speed": 4700
20
+ }
21
+ ],
22
+ "status": [
23
+ {
24
+ "response-type": "Success",
25
+ "response": "Command completed successfully.",
26
+ "return-code": 0,
27
+ "response-type-numeric": 0
28
+ }
29
+ ]
30
+}
src/go/plugin/go.d/collector/powervault/testdata/frus.json
new
+33
@@ -0,0 +1,33 @@
1
+{
2
+ "enclosure-fru": [
3
+ {
4
+ "name": "RAID Controller Module A",
5
+ "fru-location": "Enclosure 0, Controller A",
6
+ "fru-status": "OK",
7
+ "fru-status-numeric": 0,
8
+ "part-number": "0F3P10"
9
+ },
10
+ {
11
+ "name": "RAID Controller Module B",
12
+ "fru-location": "Enclosure 0, Controller B",
13
+ "fru-status": "OK",
14
+ "fru-status-numeric": 0,
15
+ "part-number": "0F3P10"
16
+ },
17
+ {
18
+ "name": "Midplane",
19
+ "fru-location": "Enclosure 0",
20
+ "fru-status": "Not Available",
21
+ "fru-status-numeric": 5,
22
+ "part-number": "0D3M7X"
23
+ }
24
+ ],
25
+ "status": [
26
+ {
27
+ "response-type": "Success",
28
+ "response": "Command completed successfully.",
29
+ "return-code": 0,
30
+ "response-type-numeric": 0
31
+ }
32
+ ]
33
+}
src/go/plugin/go.d/collector/powervault/testdata/login.json
new
+10
@@ -0,0 +1,10 @@
1
+{
2
+ "status": [
3
+ {
4
+ "response-type": "Success",
5
+ "response": "abc123sessionkey",
6
+ "return-code": 0,
7
+ "response-type-numeric": 0
8
+ }
9
+ ]
10
+}
src/go/plugin/go.d/collector/powervault/testdata/phy_statistics.json
new
+33
@@ -0,0 +1,33 @@
1
+{
2
+ "sas-host-phy-statistics": [
3
+ {
4
+ "port": "A0",
5
+ "phy": "0",
6
+ "disparity-errors": 5,
7
+ "lost-dwords": 2,
8
+ "invalid-dwords": 1
9
+ },
10
+ {
11
+ "port": "A0",
12
+ "phy": "1",
13
+ "disparity-errors": 3,
14
+ "lost-dwords": 0,
15
+ "invalid-dwords": 0
16
+ },
17
+ {
18
+ "port": "A1",
19
+ "phy": "0",
20
+ "disparity-errors": 0,
21
+ "lost-dwords": 1,
22
+ "invalid-dwords": 0
23
+ }
24
+ ],
25
+ "status": [
26
+ {
27
+ "response-type": "Success",
28
+ "response": "Command completed successfully.",
29
+ "return-code": 0,
30
+ "response-type-numeric": 0
31
+ }
32
+ ]
33
+}
src/go/plugin/go.d/collector/powervault/testdata/pools.json
new
+30
@@ -0,0 +1,30 @@
1
+{
2
+ "pools": [
3
+ {
4
+ "name": "Pool-A",
5
+ "health": "OK",
6
+ "health-numeric": 0,
7
+ "total-size": "10.0TB",
8
+ "total-avail": "4.5TB",
9
+ "total-size-numeric": 19531250000,
10
+ "total-avail-numeric": 8789062500
11
+ },
12
+ {
13
+ "name": "Pool-B",
14
+ "health": "OK",
15
+ "health-numeric": 0,
16
+ "total-size": "5.0TB",
17
+ "total-avail": "3.2TB",
18
+ "total-size-numeric": 9765625000,
19
+ "total-avail-numeric": 6250000000
20
+ }
21
+ ],
22
+ "status": [
23
+ {
24
+ "response-type": "Success",
25
+ "response": "Command completed successfully.",
26
+ "return-code": 0,
27
+ "response-type-numeric": 0
28
+ }
29
+ ]
30
+}
src/go/plugin/go.d/collector/powervault/testdata/port_statistics.json
new
+26
@@ -0,0 +1,26 @@
1
+{
2
+ "host-port-statistics": [
3
+ {
4
+ "durable-id": "hostport_A0",
5
+ "number-of-reads": 8500000,
6
+ "number-of-writes": 4200000,
7
+ "data-read-numeric": 180388626432,
8
+ "data-write-numeric": 90194313216
9
+ },
10
+ {
11
+ "durable-id": "hostport_A1",
12
+ "number-of-reads": 6200000,
13
+ "number-of-writes": 3100000,
14
+ "data-read-numeric": 128849018880,
15
+ "data-write-numeric": 64424509440
16
+ }
17
+ ],
18
+ "status": [
19
+ {
20
+ "response-type": "Success",
21
+ "response": "Command completed successfully.",
22
+ "return-code": 0,
23
+ "response-type-numeric": 0
24
+ }
25
+ ]
26
+}
src/go/plugin/go.d/collector/powervault/testdata/ports.json
new
+42
@@ -0,0 +1,42 @@
1
+{
2
+ "port": [
3
+ {
4
+ "durable-id": "hostport_A0",
5
+ "port": "A0",
6
+ "health": "OK",
7
+ "health-numeric": 0,
8
+ "status": "Up",
9
+ "status-numeric": 0,
10
+ "actual-speed": "12Gbps",
11
+ "target-id": "0"
12
+ },
13
+ {
14
+ "durable-id": "hostport_A1",
15
+ "port": "A1",
16
+ "health": "OK",
17
+ "health-numeric": 0,
18
+ "status": "Up",
19
+ "status-numeric": 0,
20
+ "actual-speed": "12Gbps",
21
+ "target-id": "1"
22
+ },
23
+ {
24
+ "durable-id": "hostport_B0",
25
+ "port": "B0",
26
+ "health": "N/A",
27
+ "health-numeric": 4,
28
+ "status": "Not Installed",
29
+ "status-numeric": 2,
30
+ "actual-speed": "",
31
+ "target-id": "2"
32
+ }
33
+ ],
34
+ "status": [
35
+ {
36
+ "response-type": "Success",
37
+ "response": "Command completed successfully.",
38
+ "return-code": 0,
39
+ "response-type-numeric": 0
40
+ }
41
+ ]
42
+}
src/go/plugin/go.d/collector/powervault/testdata/power_supplies.json
new
+34
@@ -0,0 +1,34 @@
1
+{
2
+ "power-supplies": [
3
+ {
4
+ "durable-id": "psu_0.0",
5
+ "name": "PSU 0",
6
+ "location": "Enclosure 0, Left",
7
+ "health": "OK",
8
+ "health-numeric": 0,
9
+ "status": "Up",
10
+ "serial-number": "PSU-SN-001",
11
+ "model": "PWR-580W",
12
+ "position": "Left"
13
+ },
14
+ {
15
+ "durable-id": "psu_0.1",
16
+ "name": "PSU 1",
17
+ "location": "Enclosure 0, Right",
18
+ "health": "OK",
19
+ "health-numeric": 0,
20
+ "status": "Up",
21
+ "serial-number": "PSU-SN-002",
22
+ "model": "PWR-580W",
23
+ "position": "Right"
24
+ }
25
+ ],
26
+ "status": [
27
+ {
28
+ "response-type": "Success",
29
+ "response": "Command completed successfully.",
30
+ "return-code": 0,
31
+ "response-type-numeric": 0
32
+ }
33
+ ]
34
+}
src/go/plugin/go.d/collector/powervault/testdata/sensors.json
new
+52
@@ -0,0 +1,52 @@
1
+{
2
+ "sensors": [
3
+ {
4
+ "durable-id": "sensor_temp_ctrl_A.1",
5
+ "sensor-name": "Controller A CPU Temperature",
6
+ "sensor-type": "Temperature",
7
+ "value": "42 C",
8
+ "status": "OK",
9
+ "status-numeric": 1
10
+ },
11
+ {
12
+ "durable-id": "sensor_temp_ctrl_A.2",
13
+ "sensor-name": "Controller A Board Temperature",
14
+ "sensor-type": "Temperature",
15
+ "value": "35 C",
16
+ "status": "OK",
17
+ "status-numeric": 1
18
+ },
19
+ {
20
+ "durable-id": "sensor_volt_ctrl_A.1",
21
+ "sensor-name": "Controller A 3.3V",
22
+ "sensor-type": "Voltage",
23
+ "value": "3.30 V",
24
+ "status": "OK",
25
+ "status-numeric": 1
26
+ },
27
+ {
28
+ "durable-id": "sensor_curr_ctrl_A.1",
29
+ "sensor-name": "Controller A 12V Current",
30
+ "sensor-type": "Current Sensor",
31
+ "value": "1.25 A",
32
+ "status": "OK",
33
+ "status-numeric": 1
34
+ },
35
+ {
36
+ "durable-id": "sensor_chrg_ctrl_A.1",
37
+ "sensor-name": "Controller A Supercap",
38
+ "sensor-type": "Charge Capacity",
39
+ "value": "95%",
40
+ "status": "OK",
41
+ "status-numeric": 1
42
+ }
43
+ ],
44
+ "status": [
45
+ {
46
+ "response-type": "Success",
47
+ "response": "Command completed successfully.",
48
+ "return-code": 0,
49
+ "response-type-numeric": 0
50
+ }
51
+ ]
52
+}
src/go/plugin/go.d/collector/powervault/testdata/system.json
new
+26
@@ -0,0 +1,26 @@
1
+{
2
+ "system": [
3
+ {
4
+ "system-name": "ME4024-LAB",
5
+ "product-id": "ME4024",
6
+ "health-numeric": 0,
7
+ "health": "OK",
8
+ "midplane-serial-number": "CX12345678",
9
+ "vendor-name": "DELL",
10
+ "platform-brand": "Dell Storage",
11
+ "enclosure-count": 1,
12
+ "system-location": "Lab Rack A",
13
+ "system-contact": "admin@example.com",
14
+ "scsi-product-id": "ME4024",
15
+ "current-node-wwn": "500c0ff0123456ab"
16
+ }
17
+ ],
18
+ "status": [
19
+ {
20
+ "response-type": "Success",
21
+ "response": "Command completed successfully.",
22
+ "return-code": 0,
23
+ "response-type-numeric": 0
24
+ }
25
+ ]
26
+}
src/go/plugin/go.d/collector/powervault/testdata/volume_statistics.json
new
+63
@@ -0,0 +1,63 @@
1
+{
2
+ "volume-statistics": [
3
+ {
4
+ "volume-name": "prod-db-01",
5
+ "iops": 850,
6
+ "bytes-per-second-numeric": 35651584,
7
+ "data-read-numeric": 64424509440,
8
+ "data-written-numeric": 32212254720,
9
+ "number-of-reads": 3200000,
10
+ "number-of-writes": 1600000,
11
+ "write-cache-percent": 52,
12
+ "write-cache-hits": 1200000,
13
+ "write-cache-misses": 120000,
14
+ "read-cache-hits": 2400000,
15
+ "read-cache-misses": 320000,
16
+ "percent-tier-ssd": 60,
17
+ "percent-tier-sas": 30,
18
+ "percent-tier-sata": 10
19
+ },
20
+ {
21
+ "volume-name": "prod-app-01",
22
+ "iops": 320,
23
+ "bytes-per-second-numeric": 13107200,
24
+ "data-read-numeric": 21474836480,
25
+ "data-written-numeric": 10737418240,
26
+ "number-of-reads": 1500000,
27
+ "number-of-writes": 800000,
28
+ "write-cache-percent": 28,
29
+ "write-cache-hits": 600000,
30
+ "write-cache-misses": 80000,
31
+ "read-cache-hits": 1100000,
32
+ "read-cache-misses": 150000,
33
+ "percent-tier-ssd": 40,
34
+ "percent-tier-sas": 40,
35
+ "percent-tier-sata": 20
36
+ },
37
+ {
38
+ "volume-name": "test-backup",
39
+ "iops": 50,
40
+ "bytes-per-second-numeric": 2097152,
41
+ "data-read-numeric": 5368709120,
42
+ "data-written-numeric": 10737418240,
43
+ "number-of-reads": 200000,
44
+ "number-of-writes": 400000,
45
+ "write-cache-percent": 8,
46
+ "write-cache-hits": 100000,
47
+ "write-cache-misses": 50000,
48
+ "read-cache-hits": 150000,
49
+ "read-cache-misses": 80000,
50
+ "percent-tier-ssd": 0,
51
+ "percent-tier-sas": 50,
52
+ "percent-tier-sata": 50
53
+ }
54
+ ],
55
+ "status": [
56
+ {
57
+ "response-type": "Success",
58
+ "response": "Command completed successfully.",
59
+ "return-code": 0,
60
+ "response-type-numeric": 0
61
+ }
62
+ ]
63
+}
src/go/plugin/go.d/collector/powervault/testdata/volumes.json
new
+36
@@ -0,0 +1,36 @@
1
+{
2
+ "volumes": [
3
+ {
4
+ "durable-id": "V001",
5
+ "volume-name": "prod-db-01",
6
+ "health": "OK",
7
+ "health-numeric": 0,
8
+ "total-size": "2.0TB",
9
+ "owner-numeric": 0
10
+ },
11
+ {
12
+ "durable-id": "V002",
13
+ "volume-name": "prod-app-01",
14
+ "health": "OK",
15
+ "health-numeric": 0,
16
+ "total-size": "500.0GB",
17
+ "owner-numeric": 1
18
+ },
19
+ {
20
+ "durable-id": "V003",
21
+ "volume-name": "test-backup",
22
+ "health": "OK",
23
+ "health-numeric": 0,
24
+ "total-size": "1.0TB",
25
+ "owner-numeric": 0
26
+ }
27
+ ],
28
+ "status": [
29
+ {
30
+ "response-type": "Success",
31
+ "response": "Command completed successfully.",
32
+ "return-code": 0,
33
+ "response-type-numeric": 0
34
+ }
35
+ ]
36
+}
src/go/plugin/go.d/config/go.d/powervault.conf
new
+9
@@ -0,0 +1,9 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/powervault#readme
3
+
4
+#jobs:
5
+# - name : powervault1
6
+# url : https://10.0.0.1
7
+# username : monitor
8
+# password : monitor123
9
+# tls_skip_verify : yes