@cryptotaxi247 / netdata-1 / commits / 94f65cead

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