@cryptotaxi247 / netdata-1 / commits / fa04535fe

go.d/powerstore: add Dell PowerStore storage array collector (V2 framework) (#21929)

Costa Tsaousis committed Mar 19, 2026 at 00:12 UTC fa04535fea7a646342ed338f98404fc7e64da8e2
47 files changed +3683
src/go/plugin/go.d/collector/init.go
+1
@@ -90,6 +90,7 @@ import (
90 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/postgres"
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/prometheus"
95 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/proxysql"
96 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/pulsar"
src/go/plugin/go.d/collector/powerstore/charts.yaml new
+789
@@ -0,0 +1,789 @@
1 +version: v1
2 +context_namespace: powerstore
3 +groups:
4 + - family: Cluster Capacity
5 + metrics:
6 + - cluster_space_physical_total
7 + - cluster_space_physical_used
8 + - cluster_space_logical_provisioned
9 + - cluster_space_logical_used
10 + - cluster_space_data_physical_used
11 + - cluster_space_shared_logical_used
12 + - cluster_space_efficiency_ratio
13 + - cluster_space_data_reduction
14 + - cluster_space_snapshot_savings
15 + - cluster_space_thin_savings
16 + charts:
17 + - id: cluster_space_usage
18 + title: Cluster Space Usage
19 + context: cluster_space_usage
20 + units: bytes
21 + algorithm: absolute
22 + dimensions:
23 + - selector: cluster_space_physical_used
24 + name: used
25 + - selector: cluster_space_physical_total
26 + name: total
27 + - id: cluster_space_logical
28 + title: Cluster Logical Space
29 + context: cluster_space_logical
30 + units: bytes
31 + algorithm: absolute
32 + dimensions:
33 + - selector: cluster_space_logical_provisioned
34 + name: provisioned
35 + - selector: cluster_space_logical_used
36 + name: used
37 + - selector: cluster_space_data_physical_used
38 + name: data_physical
39 + - selector: cluster_space_shared_logical_used
40 + name: shared
41 + - id: cluster_space_efficiency
42 + title: Cluster Space Efficiency Ratios
43 + context: cluster_space_efficiency
44 + units: ratio
45 + algorithm: absolute
46 + dimensions:
47 + - selector: cluster_space_efficiency_ratio
48 + name: efficiency
49 + options:
50 + float: true
51 + - selector: cluster_space_data_reduction
52 + name: data_reduction
53 + options:
54 + float: true
55 + - selector: cluster_space_snapshot_savings
56 + name: snapshot_savings
57 + options:
58 + float: true
59 + - selector: cluster_space_thin_savings
60 + name: thin_savings
61 + options:
62 + float: true
63 +
64 + - family: Hardware Health
65 + metrics:
66 + - hardware_fan_ok
67 + - hardware_fan_degraded
68 + - hardware_fan_failed
69 + - hardware_fan_unknown
70 + - hardware_psu_ok
71 + - hardware_psu_degraded
72 + - hardware_psu_failed
73 + - hardware_psu_unknown
74 + - hardware_drive_ok
75 + - hardware_drive_degraded
76 + - hardware_drive_failed
77 + - hardware_drive_unknown
78 + - hardware_battery_ok
79 + - hardware_battery_degraded
80 + - hardware_battery_failed
81 + - hardware_battery_unknown
82 + - hardware_node_ok
83 + - hardware_node_degraded
84 + - hardware_node_failed
85 + - hardware_node_unknown
86 + charts:
87 + - id: hardware_health_fan
88 + title: Fan Health Status
89 + context: hardware_health_fan
90 + units: fans
91 + type: stacked
92 + algorithm: absolute
93 + dimensions:
94 + - selector: hardware_fan_ok
95 + name: ok
96 + - selector: hardware_fan_degraded
97 + name: degraded
98 + - selector: hardware_fan_failed
99 + name: failed
100 + - selector: hardware_fan_unknown
101 + name: unknown
102 + - id: hardware_health_psu
103 + title: Power Supply Health Status
104 + context: hardware_health_psu
105 + units: PSUs
106 + type: stacked
107 + algorithm: absolute
108 + dimensions:
109 + - selector: hardware_psu_ok
110 + name: ok
111 + - selector: hardware_psu_degraded
112 + name: degraded
113 + - selector: hardware_psu_failed
114 + name: failed
115 + - selector: hardware_psu_unknown
116 + name: unknown
117 + - id: hardware_health_drive
118 + title: Drive Health Status
119 + context: hardware_health_drive
120 + units: drives
121 + type: stacked
122 + algorithm: absolute
123 + dimensions:
124 + - selector: hardware_drive_ok
125 + name: ok
126 + - selector: hardware_drive_degraded
127 + name: degraded
128 + - selector: hardware_drive_failed
129 + name: failed
130 + - selector: hardware_drive_unknown
131 + name: unknown
132 + - id: hardware_health_battery
133 + title: Battery Health Status
134 + context: hardware_health_battery
135 + units: batteries
136 + type: stacked
137 + algorithm: absolute
138 + dimensions:
139 + - selector: hardware_battery_ok
140 + name: ok
141 + - selector: hardware_battery_degraded
142 + name: degraded
143 + - selector: hardware_battery_failed
144 + name: failed
145 + - selector: hardware_battery_unknown
146 + name: unknown
147 + - id: hardware_health_node
148 + title: Node Health Status
149 + context: hardware_health_node
150 + units: nodes
151 + type: stacked
152 + algorithm: absolute
153 + dimensions:
154 + - selector: hardware_node_ok
155 + name: ok
156 + - selector: hardware_node_degraded
157 + name: degraded
158 + - selector: hardware_node_failed
159 + name: failed
160 + - selector: hardware_node_unknown
161 + name: unknown
162 +
163 + - family: Alerts
164 + metrics:
165 + - alerts_critical
166 + - alerts_major
167 + - alerts_minor
168 + - alerts_info
169 + charts:
170 + - id: alerts_active
171 + title: Active Alerts by Severity
172 + context: alerts_active
173 + units: alerts
174 + type: stacked
175 + algorithm: absolute
176 + dimensions:
177 + - selector: alerts_critical
178 + name: critical
179 + - selector: alerts_major
180 + name: major
181 + - selector: alerts_minor
182 + name: minor
183 + - selector: alerts_info
184 + name: info
185 +
186 + - family: NAS
187 + metrics:
188 + - nas_started
189 + - nas_stopped
190 + - nas_degraded
191 + - nas_unknown
192 + charts:
193 + - id: nas_server_status
194 + title: NAS Server Status
195 + context: nas_server_status
196 + units: servers
197 + type: stacked
198 + algorithm: absolute
199 + dimensions:
200 + - selector: nas_started
201 + name: started
202 + - selector: nas_stopped
203 + name: stopped
204 + - selector: nas_degraded
205 + name: degraded
206 + - selector: nas_unknown
207 + name: unknown
208 +
209 + - family: Replication
210 + metrics:
211 + - copy_data_remaining
212 + - copy_data_transferred
213 + - copy_transfer_rate
214 + charts:
215 + - id: copy_data
216 + title: Replication Data
217 + context: copy_data
218 + units: bytes
219 + algorithm: absolute
220 + dimensions:
221 + - selector: copy_data_remaining
222 + name: remaining
223 + - selector: copy_data_transferred
224 + name: transferred
225 + - id: copy_transfer_rate
226 + title: Replication Transfer Rate
227 + context: copy_transfer_rate
228 + units: bytes/s
229 + algorithm: absolute
230 + dimensions:
231 + - selector: copy_transfer_rate
232 + name: rate
233 + options:
234 + float: true
235 +
236 + - family: Appliance
237 + metrics:
238 + - appliance_perf_read_iops
239 + - appliance_perf_write_iops
240 + - appliance_perf_read_bandwidth
241 + - appliance_perf_write_bandwidth
242 + - appliance_perf_avg_read_latency
243 + - appliance_perf_avg_write_latency
244 + - appliance_perf_avg_latency
245 + - appliance_cpu_utilization
246 + - appliance_space_physical_used
247 + - appliance_space_physical_total
248 + - appliance_space_logical_provisioned
249 + - appliance_space_logical_used
250 + - appliance_space_data_physical_used
251 + - appliance_space_shared_logical_used
252 + - appliance_space_efficiency_ratio
253 + - appliance_space_data_reduction
254 + - appliance_space_snapshot_savings
255 + - appliance_space_thin_savings
256 + charts:
257 + - id: appliance_iops
258 + title: Appliance IOPS
259 + context: appliance_iops
260 + units: ops/s
261 + type: area
262 + algorithm: absolute
263 + instances:
264 + by_labels: [appliance]
265 + dimensions:
266 + - selector: appliance_perf_read_iops
267 + name: read
268 + options:
269 + float: true
270 + - selector: appliance_perf_write_iops
271 + name: write
272 + options:
273 + multiplier: -1
274 + float: true
275 + - id: appliance_bandwidth
276 + title: Appliance Bandwidth
277 + context: appliance_bandwidth
278 + units: bytes/s
279 + type: area
280 + algorithm: absolute
281 + instances:
282 + by_labels: [appliance]
283 + dimensions:
284 + - selector: appliance_perf_read_bandwidth
285 + name: read
286 + options:
287 + float: true
288 + - selector: appliance_perf_write_bandwidth
289 + name: write
290 + options:
291 + multiplier: -1
292 + float: true
293 + - id: appliance_latency
294 + title: Appliance Latency
295 + context: appliance_latency
296 + units: microseconds
297 + algorithm: absolute
298 + instances:
299 + by_labels: [appliance]
300 + dimensions:
301 + - selector: appliance_perf_avg_read_latency
302 + name: read
303 + options:
304 + float: true
305 + - selector: appliance_perf_avg_write_latency
306 + name: write
307 + options:
308 + float: true
309 + - selector: appliance_perf_avg_latency
310 + name: avg
311 + options:
312 + float: true
313 + - id: appliance_cpu
314 + title: Appliance IO Workload CPU Utilization
315 + context: appliance_cpu
316 + units: percentage
317 + algorithm: absolute
318 + instances:
319 + by_labels: [appliance]
320 + dimensions:
321 + - selector: appliance_cpu_utilization
322 + name: utilization
323 + options:
324 + float: true
325 + - id: appliance_space
326 + title: Appliance Space Usage
327 + context: appliance_space
328 + units: bytes
329 + algorithm: absolute
330 + instances:
331 + by_labels: [appliance]
332 + dimensions:
333 + - selector: appliance_space_physical_used
334 + name: used
335 + - selector: appliance_space_physical_total
336 + name: total
337 + - id: appliance_space_logical
338 + title: Appliance Logical Space
339 + context: appliance_space_logical
340 + units: bytes
341 + algorithm: absolute
342 + instances:
343 + by_labels: [appliance]
344 + dimensions:
345 + - selector: appliance_space_logical_provisioned
346 + name: provisioned
347 + - selector: appliance_space_logical_used
348 + name: used
349 + - selector: appliance_space_data_physical_used
350 + name: data_physical
351 + - selector: appliance_space_shared_logical_used
352 + name: shared
353 + - id: appliance_space_efficiency
354 + title: Appliance Space Efficiency Ratios
355 + context: appliance_space_efficiency
356 + units: ratio
357 + algorithm: absolute
358 + instances:
359 + by_labels: [appliance]
360 + dimensions:
361 + - selector: appliance_space_efficiency_ratio
362 + name: efficiency
363 + options:
364 + float: true
365 + - selector: appliance_space_data_reduction
366 + name: data_reduction
367 + options:
368 + float: true
369 + - selector: appliance_space_snapshot_savings
370 + name: snapshot_savings
371 + options:
372 + float: true
373 + - selector: appliance_space_thin_savings
374 + name: thin_savings
375 + options:
376 + float: true
377 +
378 + - family: Volume
379 + metrics:
380 + - volume_perf_read_iops
381 + - volume_perf_write_iops
382 + - volume_perf_read_bandwidth
383 + - volume_perf_write_bandwidth
384 + - volume_perf_avg_read_latency
385 + - volume_perf_avg_write_latency
386 + - volume_perf_avg_latency
387 + - volume_space_logical_provisioned
388 + - volume_space_logical_used
389 + - volume_space_thin_savings
390 + charts:
391 + - id: volume_iops
392 + title: Volume IOPS
393 + context: volume_iops
394 + units: ops/s
395 + type: area
396 + algorithm: absolute
397 + instances:
398 + by_labels: [volume]
399 + dimensions:
400 + - selector: volume_perf_read_iops
401 + name: read
402 + options:
403 + float: true
404 + - selector: volume_perf_write_iops
405 + name: write
406 + options:
407 + multiplier: -1
408 + float: true
409 + - id: volume_bandwidth
410 + title: Volume Bandwidth
411 + context: volume_bandwidth
412 + units: bytes/s
413 + type: area
414 + algorithm: absolute
415 + instances:
416 + by_labels: [volume]
417 + dimensions:
418 + - selector: volume_perf_read_bandwidth
419 + name: read
420 + options:
421 + float: true
422 + - selector: volume_perf_write_bandwidth
423 + name: write
424 + options:
425 + multiplier: -1
426 + float: true
427 + - id: volume_latency
428 + title: Volume Latency
429 + context: volume_latency
430 + units: microseconds
431 + algorithm: absolute
432 + instances:
433 + by_labels: [volume]
434 + dimensions:
435 + - selector: volume_perf_avg_read_latency
436 + name: read
437 + options:
438 + float: true
439 + - selector: volume_perf_avg_write_latency
440 + name: write
441 + options:
442 + float: true
443 + - selector: volume_perf_avg_latency
444 + name: avg
445 + options:
446 + float: true
447 + - id: volume_space
448 + title: Volume Space Usage
449 + context: volume_space
450 + units: bytes
451 + algorithm: absolute
452 + instances:
453 + by_labels: [volume]
454 + dimensions:
455 + - selector: volume_space_logical_provisioned
456 + name: provisioned
457 + - selector: volume_space_logical_used
458 + name: used
459 + - id: volume_space_savings
460 + title: Volume Thin Savings
461 + context: volume_space_savings
462 + units: ratio
463 + algorithm: absolute
464 + instances:
465 + by_labels: [volume]
466 + dimensions:
467 + - selector: volume_space_thin_savings
468 + name: thin_savings
469 + options:
470 + float: true
471 +
472 + - family: Node
473 + metrics:
474 + - node_perf_read_iops
475 + - node_perf_write_iops
476 + - node_perf_read_bandwidth
477 + - node_perf_write_bandwidth
478 + - node_perf_avg_read_latency
479 + - node_perf_avg_write_latency
480 + - node_perf_avg_latency
481 + - node_current_logins
482 + charts:
483 + - id: node_iops
484 + title: Node IOPS
485 + context: node_iops
486 + units: ops/s
487 + type: area
488 + algorithm: absolute
489 + instances:
490 + by_labels: [node]
491 + dimensions:
492 + - selector: node_perf_read_iops
493 + name: read
494 + options:
495 + float: true
496 + - selector: node_perf_write_iops
497 + name: write
498 + options:
499 + multiplier: -1
500 + float: true
501 + - id: node_bandwidth
502 + title: Node Bandwidth
503 + context: node_bandwidth
504 + units: bytes/s
505 + type: area
506 + algorithm: absolute
507 + instances:
508 + by_labels: [node]
509 + dimensions:
510 + - selector: node_perf_read_bandwidth
511 + name: read
512 + options:
513 + float: true
514 + - selector: node_perf_write_bandwidth
515 + name: write
516 + options:
517 + multiplier: -1
518 + float: true
519 + - id: node_latency
520 + title: Node Latency
521 + context: node_latency
522 + units: microseconds
523 + algorithm: absolute
524 + instances:
525 + by_labels: [node]
526 + dimensions:
527 + - selector: node_perf_avg_read_latency
528 + name: read
529 + options:
530 + float: true
531 + - selector: node_perf_avg_write_latency
532 + name: write
533 + options:
534 + float: true
535 + - selector: node_perf_avg_latency
536 + name: avg
537 + options:
538 + float: true
539 + - id: node_logins
540 + title: Node Current Logins
541 + context: node_logins
542 + units: logins
543 + algorithm: absolute
544 + instances:
545 + by_labels: [node]
546 + dimensions:
547 + - selector: node_current_logins
548 + name: logins
549 +
550 + - family: FC Port
551 + metrics:
552 + - fc_port_perf_read_iops
553 + - fc_port_perf_write_iops
554 + - fc_port_perf_read_bandwidth
555 + - fc_port_perf_write_bandwidth
556 + - fc_port_perf_avg_read_latency
557 + - fc_port_perf_avg_write_latency
558 + - fc_port_perf_avg_latency
559 + - fc_port_link_up
560 + charts:
561 + - id: fc_port_iops
562 + title: FC Port IOPS
563 + context: fc_port_iops
564 + units: ops/s
565 + type: area
566 + algorithm: absolute
567 + instances:
568 + by_labels: [fc_port]
569 + dimensions:
570 + - selector: fc_port_perf_read_iops
571 + name: read
572 + options:
573 + float: true
574 + - selector: fc_port_perf_write_iops
575 + name: write
576 + options:
577 + multiplier: -1
578 + float: true
579 + - id: fc_port_bandwidth
580 + title: FC Port Bandwidth
581 + context: fc_port_bandwidth
582 + units: bytes/s
583 + type: area
584 + algorithm: absolute
585 + instances:
586 + by_labels: [fc_port]
587 + dimensions:
588 + - selector: fc_port_perf_read_bandwidth
589 + name: read
590 + options:
591 + float: true
592 + - selector: fc_port_perf_write_bandwidth
593 + name: write
594 + options:
595 + multiplier: -1
596 + float: true
597 + - id: fc_port_latency
598 + title: FC Port Latency
599 + context: fc_port_latency
600 + units: microseconds
601 + algorithm: absolute
602 + instances:
603 + by_labels: [fc_port]
604 + dimensions:
605 + - selector: fc_port_perf_avg_read_latency
606 + name: read
607 + options:
608 + float: true
609 + - selector: fc_port_perf_avg_write_latency
610 + name: write
611 + options:
612 + float: true
613 + - selector: fc_port_perf_avg_latency
614 + name: avg
615 + options:
616 + float: true
617 + - id: fc_port_link_status
618 + title: FC Port Link Status
619 + context: fc_port_link_status
620 + units: status
621 + algorithm: absolute
622 + instances:
623 + by_labels: [fc_port]
624 + dimensions:
625 + - selector: fc_port_link_up
626 + name: up
627 +
628 + - family: Ethernet Port
629 + metrics:
630 + - eth_port_bytes_rx_ps
631 + - eth_port_bytes_tx_ps
632 + - eth_port_pkt_rx_ps
633 + - eth_port_pkt_tx_ps
634 + - eth_port_pkt_rx_crc_error_ps
635 + - eth_port_pkt_rx_no_buffer_error_ps
636 + - eth_port_pkt_tx_error_ps
637 + - eth_port_link_up
638 + charts:
639 + - id: eth_port_bytes
640 + title: Ethernet Port Bytes Rate
641 + context: eth_port_bytes
642 + units: bytes/s
643 + type: area
644 + algorithm: absolute
645 + instances:
646 + by_labels: [eth_port]
647 + dimensions:
648 + - selector: eth_port_bytes_rx_ps
649 + name: received
650 + options:
651 + float: true
652 + - selector: eth_port_bytes_tx_ps
653 + name: sent
654 + options:
655 + multiplier: -1
656 + float: true
657 + - id: eth_port_packets
658 + title: Ethernet Port Packets Rate
659 + context: eth_port_packets
660 + units: packets/s
661 + type: area
662 + algorithm: absolute
663 + instances:
664 + by_labels: [eth_port]
665 + dimensions:
666 + - selector: eth_port_pkt_rx_ps
667 + name: received
668 + options:
669 + float: true
670 + - selector: eth_port_pkt_tx_ps
671 + name: sent
672 + options:
673 + multiplier: -1
674 + float: true
675 + - id: eth_port_errors
676 + title: Ethernet Port Errors Rate
677 + context: eth_port_errors
678 + units: errors/s
679 + type: stacked
680 + algorithm: absolute
681 + instances:
682 + by_labels: [eth_port]
683 + dimensions:
684 + - selector: eth_port_pkt_rx_crc_error_ps
685 + name: rx_crc
686 + options:
687 + float: true
688 + - selector: eth_port_pkt_rx_no_buffer_error_ps
689 + name: rx_no_buffer
690 + options:
691 + float: true
692 + - selector: eth_port_pkt_tx_error_ps
693 + name: tx_error
694 + options:
695 + float: true
696 + - id: eth_port_link_status
697 + title: Ethernet Port Link Status
698 + context: eth_port_link_status
699 + units: status
700 + algorithm: absolute
701 + instances:
702 + by_labels: [eth_port]
703 + dimensions:
704 + - selector: eth_port_link_up
705 + name: up
706 +
707 + - family: File System
708 + metrics:
709 + - file_system_perf_read_iops
710 + - file_system_perf_write_iops
711 + - file_system_perf_read_bandwidth
712 + - file_system_perf_write_bandwidth
713 + - file_system_perf_avg_read_latency
714 + - file_system_perf_avg_write_latency
715 + - file_system_perf_avg_latency
716 + charts:
717 + - id: filesystem_iops
718 + title: File System IOPS
719 + context: filesystem_iops
720 + units: ops/s
721 + type: area
722 + algorithm: absolute
723 + instances:
724 + by_labels: [filesystem]
725 + dimensions:
726 + - selector: file_system_perf_read_iops
727 + name: read
728 + options:
729 + float: true
730 + - selector: file_system_perf_write_iops
731 + name: write
732 + options:
733 + multiplier: -1
734 + float: true
735 + - id: filesystem_bandwidth
736 + title: File System Bandwidth
737 + context: filesystem_bandwidth
738 + units: bytes/s
739 + type: area
740 + algorithm: absolute
741 + instances:
742 + by_labels: [filesystem]
743 + dimensions:
744 + - selector: file_system_perf_read_bandwidth
745 + name: read
746 + options:
747 + float: true
748 + - selector: file_system_perf_write_bandwidth
749 + name: write
750 + options:
751 + multiplier: -1
752 + float: true
753 + - id: filesystem_latency
754 + title: File System Latency
755 + context: filesystem_latency
756 + units: microseconds
757 + algorithm: absolute
758 + instances:
759 + by_labels: [filesystem]
760 + dimensions:
761 + - selector: file_system_perf_avg_read_latency
762 + name: read
763 + options:
764 + float: true
765 + - selector: file_system_perf_avg_write_latency
766 + name: write
767 + options:
768 + float: true
769 + - selector: file_system_perf_avg_latency
770 + name: avg
771 + options:
772 + float: true
773 +
774 + - family: Drive
775 + metrics:
776 + - drive_endurance_remaining
777 + charts:
778 + - id: drive_endurance
779 + title: Drive Endurance Remaining
780 + context: drive_endurance
781 + units: percentage
782 + algorithm: absolute
783 + instances:
784 + by_labels: [drive]
785 + dimensions:
786 + - selector: drive_endurance_remaining
787 + name: remaining
788 + options:
789 + float: true
src/go/plugin/go.d/collector/powerstore/client/client.go new
+364
@@ -0,0 +1,364 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package client
4 +
5 +import (
6 + "encoding/json"
7 + "fmt"
8 + "net/http"
9 + "net/http/cookiejar"
10 + "net/url"
11 + "path"
12 + "strconv"
13 + "sync"
14 +
15 + "github.com/netdata/netdata/go/plugins/pkg/web"
16 +)
17 +
18 +const (
19 + apiBasePath = "/api/rest"
20 + dellEMCToken = "DELL-EMC-TOKEN"
21 + defaultLimit = "2000"
22 +)
23 +
24 +// New creates a new PowerStore REST API client.
25 +func New(client web.ClientConfig, request web.RequestConfig) (*Client, error) {
26 + httpClient, err := web.NewHTTPClient(client)
27 + if err != nil {
28 + return nil, err
29 + }
30 +
31 + jar, err := cookiejar.New(nil)
32 + if err != nil {
33 + return nil, fmt.Errorf("error creating cookie jar: %v", err)
34 + }
35 + httpClient.Jar = jar
36 +
37 + return &Client{
38 + Request: request,
39 + httpClient: httpClient,
40 + csrf: &csrfToken{},
41 + }, nil
42 +}
43 +
44 +// Client represents a Dell PowerStore REST API client.
45 +type Client struct {
46 + Request web.RequestConfig
47 + httpClient *http.Client
48 + csrf *csrfToken
49 +}
50 +
51 +// Login authenticates with the PowerStore API.
52 +// GET /api/rest/login_session with Basic Auth.
53 +// Caches auth_cookie (via cookiejar) and DELL-EMC-TOKEN (from response header).
54 +func (c *Client) Login() error {
55 + req := c.createRequest("/login_session")
56 +
57 + resp, err := c.doOK(req)
58 + defer web.CloseBody(resp)
59 + if err != nil {
60 + return fmt.Errorf("login failed: %v", err)
61 + }
62 +
63 + c.cacheCSRFToken(resp)
64 + return nil
65 +}
66 +
67 +// Logout clears the client session state.
68 +func (c *Client) Logout() {
69 + c.csrf.unset()
70 +}
71 +
72 +// Clusters returns all clusters.
73 +func (c *Client) Clusters() ([]Cluster, error) {
74 + return doGetAllPages[Cluster](c, "/cluster", nil)
75 +}
76 +
77 +// Appliances returns all appliances.
78 +func (c *Client) Appliances() ([]Appliance, error) {
79 + return doGetAllPages[Appliance](c, "/appliance", nil)
80 +}
81 +
82 +// Volumes returns all volumes.
83 +func (c *Client) Volumes() ([]Volume, error) {
84 + return doGetAllPages[Volume](c, "/volume", nil)
85 +}
86 +
87 +// AllHardware returns all hardware components.
88 +func (c *Client) AllHardware() ([]Hardware, error) {
89 + return doGetAllPages[Hardware](c, "/hardware", nil)
90 +}
91 +
92 +// Alerts returns alerts filtered by state.
93 +func (c *Client) Alerts(state string) ([]Alert, error) {
94 + return doGetAllPages[Alert](c, "/alert", url.Values{"state": {"eq." + state}})
95 +}
96 +
97 +// FcPorts returns all Fibre Channel ports.
98 +func (c *Client) FcPorts() ([]FcPort, error) {
99 + return doGetAllPages[FcPort](c, "/fc_port", nil)
100 +}
101 +
102 +// EthPorts returns all Ethernet ports.
103 +func (c *Client) EthPorts() ([]EthPort, error) {
104 + return doGetAllPages[EthPort](c, "/eth_port", nil)
105 +}
106 +
107 +// FileSystems returns all file systems.
108 +func (c *Client) FileSystems() ([]FileSystem, error) {
109 + return doGetAllPages[FileSystem](c, "/file_system", nil)
110 +}
111 +
112 +// NASServers returns all NAS servers.
113 +func (c *Client) NASServers() ([]NAS, error) {
114 + return doGetAllPages[NAS](c, "/nas_server", nil)
115 +}
116 +
117 +// PerformanceMetricsByAppliance returns performance metrics for an appliance.
118 +func (c *Client) PerformanceMetricsByAppliance(id string) ([]PerformanceMetrics, error) {
119 + return doMetrics[PerformanceMetrics](c, "performance_metrics_by_appliance", id, "Five_Mins")
120 +}
121 +
122 +// PerformanceMetricsByVolume returns performance metrics for a volume.
123 +func (c *Client) PerformanceMetricsByVolume(id string) ([]PerformanceMetrics, error) {
124 + return doMetrics[PerformanceMetrics](c, "performance_metrics_by_volume", id, "Five_Mins")
125 +}
126 +
127 +// PerformanceMetricsByNode returns performance metrics for a node.
128 +func (c *Client) PerformanceMetricsByNode(id string) ([]PerformanceMetrics, error) {
129 + return doMetrics[PerformanceMetrics](c, "performance_metrics_by_node", id, "Five_Mins")
130 +}
131 +
132 +// PerformanceMetricsByFcPort returns performance metrics for an FC port.
133 +func (c *Client) PerformanceMetricsByFcPort(id string) ([]PerformanceMetrics, error) {
134 + return doMetrics[PerformanceMetrics](c, "performance_metrics_by_fe_fc_port", id, "Five_Mins")
135 +}
136 +
137 +// EthPortPerformanceMetrics returns performance metrics for an Ethernet port.
138 +func (c *Client) EthPortPerformanceMetrics(id string) ([]EthPortMetrics, error) {
139 + return doMetrics[EthPortMetrics](c, "performance_metrics_by_fe_eth_port", id, "Five_Mins")
140 +}
141 +
142 +// PerformanceMetricsByFileSystem returns performance metrics for a file system.
143 +func (c *Client) PerformanceMetricsByFileSystem(id string) ([]FileSystemMetrics, error) {
144 + return doMetrics[FileSystemMetrics](c, "performance_metrics_by_file_system", id, "Five_Mins")
145 +}
146 +
147 +// SpaceMetricsByCluster returns space metrics for a cluster.
148 +func (c *Client) SpaceMetricsByCluster(id string) ([]SpaceMetrics, error) {
149 + return doMetrics[SpaceMetrics](c, "space_metrics_by_cluster", id, "One_Day")
150 +}
151 +
152 +// SpaceMetricsByAppliance returns space metrics for an appliance.
153 +func (c *Client) SpaceMetricsByAppliance(id string) ([]SpaceMetrics, error) {
154 + return doMetrics[SpaceMetrics](c, "space_metrics_by_appliance", id, "One_Day")
155 +}
156 +
157 +// SpaceMetricsByVolume returns space metrics for a volume.
158 +func (c *Client) SpaceMetricsByVolume(id string) ([]SpaceMetrics, error) {
159 + return doMetrics[SpaceMetrics](c, "space_metrics_by_volume", id, "Five_Mins")
160 +}
161 +
162 +// WearMetricsByDrive returns wear metrics for a drive.
163 +func (c *Client) WearMetricsByDrive(id string) ([]WearMetrics, error) {
164 + return doMetrics[WearMetrics](c, "wear_metrics_by_drive", id, "Five_Mins")
165 +}
166 +
167 +// CopyMetricsByAppliance returns copy/replication metrics for an appliance.
168 +func (c *Client) CopyMetricsByAppliance(id string) ([]CopyMetrics, error) {
169 + return doMetrics[CopyMetrics](c, "copy_metrics_by_appliance", id, "Five_Mins")
170 +}
171 +
172 +func doMetrics[T any](c *Client, entity, entityID, interval string) ([]T, error) {
173 + body := MetricsRequest{Entity: entity, EntityID: entityID, Interval: interval}
174 + var v []T
175 + if err := c.doPostWithRetry(&v, "/metrics/generate", body); err != nil {
176 + return nil, err
177 + }
178 + return v, nil
179 +}
180 +
181 +func (c *Client) createRequest(urlPath string) web.RequestConfig {
182 + req := c.Request.Copy()
183 + u, _ := url.Parse(req.URL)
184 + u.Path = path.Join(u.Path, apiBasePath, urlPath)
185 + req.URL = u.String()
186 + return req
187 +}
188 +
189 +func (c *Client) createGetRequest(urlPath string, params url.Values) web.RequestConfig {
190 + req := c.createRequest(urlPath)
191 + u, _ := url.Parse(req.URL)
192 + q := u.Query()
193 + q.Set("select", "*")
194 + q.Set("limit", defaultLimit)
195 + for k, vals := range params {
196 + for _, v := range vals {
197 + q.Set(k, v)
198 + }
199 + }
200 + u.RawQuery = q.Encode()
201 + req.URL = u.String()
202 +
203 + if tok := c.csrf.get(); tok != "" {
204 + if req.Headers == nil {
205 + req.Headers = make(map[string]string)
206 + }
207 + req.Headers[dellEMCToken] = tok
208 + }
209 + return req
210 +}
211 +
212 +func (c *Client) createPostRequest(urlPath string, body any) (web.RequestConfig, error) {
213 + req := c.createRequest(urlPath)
214 +
215 + b, err := json.Marshal(body)
216 + if err != nil {
217 + return req, fmt.Errorf("error marshaling request body: %v", err)
218 + }
219 +
220 + if req.Headers == nil {
221 + req.Headers = make(map[string]string)
222 + }
223 + req.Headers["Content-Type"] = "application/json"
224 + if tok := c.csrf.get(); tok != "" {
225 + req.Headers[dellEMCToken] = tok
226 + }
227 + req.Method = http.MethodPost
228 + req.Body = string(b)
229 + return req, nil
230 +}
231 +
232 +func (c *Client) do(req web.RequestConfig) (*http.Response, error) {
233 + httpReq, err := web.NewHTTPRequest(req)
234 + if err != nil {
235 + return nil, fmt.Errorf("error creating http request to %s: %v", req.URL, err)
236 + }
237 + return c.httpClient.Do(httpReq)
238 +}
239 +
240 +func (c *Client) doOK(req web.RequestConfig) (*http.Response, error) {
241 + resp, err := c.do(req)
242 + if err != nil {
243 + return nil, err
244 + }
245 + if err = checkStatusCode(resp); err != nil {
246 + err = fmt.Errorf("%s returned %v", req.URL, err)
247 + }
248 + return resp, err
249 +}
250 +
251 +func (c *Client) doOKWithRetry(req web.RequestConfig) (*http.Response, error) {
252 + resp, err := c.do(req)
253 + if err != nil {
254 + return nil, err
255 + }
256 + // PowerStore returns 403 when the session/token is stale (not 401)
257 + if resp.StatusCode == http.StatusForbidden {
258 + web.CloseBody(resp)
259 + if err = c.Login(); err != nil {
260 + return nil, fmt.Errorf("re-login after 403 failed: %v", err)
261 + }
262 + req = c.applyCSRF(req)
263 + return c.doOK(req)
264 + }
265 + if err = checkStatusCode(resp); err != nil {
266 + err = fmt.Errorf("%s returned %v", req.URL, err)
267 + }
268 + return resp, err
269 +}
270 +
271 +// doGetAllPages fetches all pages from a paginated GET endpoint.
272 +// PowerStore returns HTTP 206 (Partial Content) when more pages are available,
273 +// with a server-enforced maximum of 2000 items per page.
274 +func doGetAllPages[T any](c *Client, urlPath string, params url.Values) ([]T, error) {
275 + var all []T
276 + offset := 0
277 +
278 + for {
279 + reqParams := make(url.Values)
280 + for k, v := range params {
281 + reqParams[k] = v
282 + }
283 + if offset > 0 {
284 + reqParams.Set("offset", strconv.Itoa(offset))
285 + }
286 +
287 + req := c.createGetRequest(urlPath, reqParams)
288 +
289 + resp, err := c.doOKWithRetry(req)
290 + if err != nil {
291 + web.CloseBody(resp)
292 + return nil, err
293 + }
294 +
295 + var page []T
296 + err = json.NewDecoder(resp.Body).Decode(&page)
297 + c.cacheCSRFToken(resp)
298 + partial := resp.StatusCode == http.StatusPartialContent
299 + web.CloseBody(resp)
300 +
301 + if err != nil {
302 + return nil, fmt.Errorf("error decoding %s response: %v", urlPath, err)
303 + }
304 +
305 + all = append(all, page...)
306 +
307 + if !partial || len(page) == 0 {
308 + break
309 + }
310 + offset += len(page)
311 + }
312 +
313 + return all, nil
314 +}
315 +
316 +func (c *Client) doPostWithRetry(dst any, urlPath string, body any) error {
317 + req, err := c.createPostRequest(urlPath, body)
318 + if err != nil {
319 + return err
320 + }
321 + resp, err := c.doOKWithRetry(req)
322 + defer web.CloseBody(resp)
323 + if err != nil {
324 + return err
325 + }
326 + c.cacheCSRFToken(resp)
327 + return json.NewDecoder(resp.Body).Decode(dst)
328 +}
329 +
330 +func (c *Client) cacheCSRFToken(resp *http.Response) {
331 + if resp == nil {
332 + return
333 + }
334 + if tok := resp.Header.Get(dellEMCToken); tok != "" {
335 + c.csrf.set(tok)
336 + }
337 +}
338 +
339 +func (c *Client) applyCSRF(req web.RequestConfig) web.RequestConfig {
340 + if tok := c.csrf.get(); tok != "" {
341 + if req.Headers == nil {
342 + req.Headers = make(map[string]string)
343 + }
344 + req.Headers[dellEMCToken] = tok
345 + }
346 + return req
347 +}
348 +
349 +func checkStatusCode(resp *http.Response) error {
350 + if resp.StatusCode < 200 || resp.StatusCode >= 300 {
351 + return fmt.Errorf("HTTP status code %d", resp.StatusCode)
352 + }
353 + return nil
354 +}
355 +
356 +// csrfToken safely stores the DELL-EMC-TOKEN value.
357 +type csrfToken struct {
358 + mux sync.RWMutex
359 + value string
360 +}
361 +
362 +func (t *csrfToken) get() string { t.mux.RLock(); defer t.mux.RUnlock(); return t.value }
363 +func (t *csrfToken) set(v string) { t.mux.Lock(); defer t.mux.Unlock(); t.value = v }
364 +func (t *csrfToken) unset() { t.set("") }
src/go/plugin/go.d/collector/powerstore/client/types.go new
+205
@@ -0,0 +1,205 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package client
4 +
5 +// Cluster represents a PowerStore cluster.
6 +type Cluster struct {
7 + ID string `json:"id"`
8 + GlobalID string `json:"global_id"`
9 + Name string `json:"name"`
10 + ManagementAddress string `json:"management_address"`
11 + State string `json:"state"`
12 +}
13 +
14 +// Appliance represents a PowerStore appliance.
15 +type Appliance struct {
16 + ID string `json:"id"`
17 + Name string `json:"name"`
18 + ServiceTag string `json:"service_tag,omitempty"`
19 +}
20 +
21 +// Volume represents a PowerStore volume.
22 +type Volume struct {
23 + ID string `json:"id"`
24 + Name string `json:"name"`
25 + Size int64 `json:"size"`
26 + LogicalUsed int64 `json:"logical_used"`
27 + State string `json:"state"`
28 + Type string `json:"type"`
29 + ApplianceID string `json:"appliance_id"`
30 +}
31 +
32 +// Hardware represents a PowerStore hardware component.
33 +type Hardware struct {
34 + ID string `json:"id"`
35 + Name string `json:"name"`
36 + Type string `json:"type"`
37 + LifecycleState string `json:"lifecycle_state"`
38 + StatusLED string `json:"status_led,omitempty"`
39 + ApplianceID string `json:"appliance_id,omitempty"`
40 + ExtraDetails string `json:"extra_details,omitempty"`
41 + PartNumber string `json:"part_number,omitempty"`
42 + SerialNumber string `json:"serial_number,omitempty"`
43 + Slot int `json:"slot,omitempty"`
44 +}
45 +
46 +// Alert represents a PowerStore alert.
47 +type Alert struct {
48 + ID string `json:"id"`
49 + Severity string `json:"severity"`
50 + State string `json:"state"`
51 + ResourceType string `json:"resource_type"`
52 + ResourceName string `json:"resource_name"`
53 + Description string `json:"description_l10n"`
54 + GeneratedTimestamp string `json:"generated_timestamp"`
55 + IsAcknowledged bool `json:"is_acknowledged"`
56 +}
57 +
58 +// FcPort represents a PowerStore Fibre Channel port.
59 +type FcPort struct {
60 + ID string `json:"id"`
61 + Name string `json:"name"`
62 + ApplianceID string `json:"appliance_id"`
63 + NodeID string `json:"node_id"`
64 + IsLinkUp bool `json:"is_link_up"`
65 + CurrentSpeed string `json:"current_speed"`
66 + Wwn string `json:"wwn"`
67 +}
68 +
69 +// EthPort represents a PowerStore Ethernet port.
70 +type EthPort struct {
71 + ID string `json:"id"`
72 + Name string `json:"name"`
73 + ApplianceID string `json:"appliance_id"`
74 + NodeID string `json:"node_id"`
75 + IsLinkUp bool `json:"is_link_up"`
76 + CurrentSpeed string `json:"current_speed"`
77 + MacAddress string `json:"mac_address"`
78 + CurrentMTU int32 `json:"current_mtu"`
79 +}
80 +
81 +// FileSystem represents a PowerStore file system.
82 +type FileSystem struct {
83 + ID string `json:"id"`
84 + Name string `json:"name"`
85 + NasServerID string `json:"nas_server_id"`
86 + SizeTotal int64 `json:"size_total"`
87 + SizeUsed int64 `json:"size_used"`
88 + FilesystemType string `json:"filesystem_type"`
89 +}
90 +
91 +// NAS represents a PowerStore NAS server.
92 +type NAS struct {
93 + ID string `json:"id"`
94 + Name string `json:"name"`
95 + OperationalStatus string `json:"operational_status"`
96 + CurrentNodeID string `json:"current_node_id"`
97 +}
98 +
99 +// Node represents a PowerStore node (queried from hardware endpoint).
100 +type Node struct {
101 + ID string `json:"id"`
102 + Name string `json:"name"`
103 + ApplianceID string `json:"appliance_id"`
104 +}
105 +
106 +// MetricsRequest is the POST body for /api/rest/metrics/generate.
107 +type MetricsRequest struct {
108 + Entity string `json:"entity"`
109 + EntityID string `json:"entity_id"`
110 + Interval string `json:"interval"`
111 +}
112 +
113 +// PerformanceMetrics contains common performance fields shared by most entity types.
114 +type PerformanceMetrics struct {
115 + ApplianceID string `json:"appliance_id,omitempty"`
116 + NodeID string `json:"node_id,omitempty"`
117 + VolumeID string `json:"volume_id,omitempty"`
118 + FePortID string `json:"fe_port_id,omitempty"`
119 +
120 + // Current values
121 + ReadIops float64 `json:"read_iops"`
122 + WriteIops float64 `json:"write_iops"`
123 + TotalIops float64 `json:"total_iops"`
124 + ReadBandwidth float64 `json:"read_bandwidth"`
125 + WriteBandwidth float64 `json:"write_bandwidth"`
126 + TotalBandwidth float64 `json:"total_bandwidth"`
127 +
128 + // Averages
129 + AvgReadLatency float64 `json:"avg_read_latency"`
130 + AvgWriteLatency float64 `json:"avg_write_latency"`
131 + AvgLatency float64 `json:"avg_latency"`
132 + AvgIoSize float64 `json:"avg_io_size"`
133 + AvgReadIops float64 `json:"avg_read_iops"`
134 + AvgWriteIops float64 `json:"avg_write_iops"`
135 +
136 + // Appliance-specific
137 + IoWorkloadCPUUtilization float64 `json:"io_workload_cpu_utilization,omitempty"`
138 + AvgIoWorkloadCPUUtilization float64 `json:"avg_io_workload_cpu_utilization,omitempty"`
139 +
140 + // Node-specific
141 + CurrentLogins *int64 `json:"current_logins,omitempty"`
142 +}
143 +
144 +// EthPortMetrics contains Ethernet port performance fields.
145 +type EthPortMetrics struct {
146 + ApplianceID string `json:"appliance_id,omitempty"`
147 + FePortID string `json:"fe_port_id,omitempty"`
148 + NodeID string `json:"node_id,omitempty"`
149 +
150 + BytesRxPs float64 `json:"bytes_rx_ps"`
151 + BytesTxPs float64 `json:"bytes_tx_ps"`
152 + PktRxPs float64 `json:"pkt_rx_ps"`
153 + PktTxPs float64 `json:"pkt_tx_ps"`
154 + PktRxCrcErrorPs float64 `json:"pkt_rx_crc_error_ps"`
155 + PktRxNoBufferErrorPs float64 `json:"pkt_rx_no_buffer_error_ps"`
156 + PktTxErrorPs float64 `json:"pkt_tx_error_ps"`
157 +}
158 +
159 +// SpaceMetrics contains capacity/space fields.
160 +type SpaceMetrics struct {
161 + ClusterID string `json:"cluster_id,omitempty"`
162 + ApplianceID string `json:"appliance_id,omitempty"`
163 + VolumeID string `json:"volume_id,omitempty"`
164 +
165 + PhysicalTotal *int64 `json:"physical_total,omitempty"`
166 + PhysicalUsed *int64 `json:"physical_used,omitempty"`
167 + LogicalProvisioned *int64 `json:"logical_provisioned,omitempty"`
168 + LogicalUsed *int64 `json:"logical_used,omitempty"`
169 + DataPhysicalUsed *int64 `json:"data_physical_used,omitempty"`
170 + SharedLogicalUsed *int64 `json:"shared_logical_used,omitempty"`
171 +
172 + EfficiencyRatio float64 `json:"efficiency_ratio"`
173 + DataReduction float64 `json:"data_reduction"`
174 + SnapshotSavings float64 `json:"snapshot_savings"`
175 + ThinSavings float64 `json:"thin_savings"`
176 +}
177 +
178 +// WearMetrics contains drive wear/endurance fields.
179 +type WearMetrics struct {
180 + DriveID string `json:"drive_id"`
181 + PercentEnduranceRemaining float64 `json:"percent_endurance_remaining"`
182 +}
183 +
184 +// CopyMetrics contains replication/copy fields.
185 +type CopyMetrics struct {
186 + ApplianceID string `json:"appliance_id,omitempty"`
187 + DataRemaining *int64 `json:"data_remaining,omitempty"`
188 + DataTransferred *int64 `json:"data_transferred,omitempty"`
189 + TransferRate float64 `json:"transfer_rate"`
190 +}
191 +
192 +// FileSystemMetrics contains file system performance fields.
193 +type FileSystemMetrics struct {
194 + FileSystemID string `json:"file_system_id"`
195 +
196 + ReadIops float64 `json:"read_iops"`
197 + WriteIops float64 `json:"write_iops"`
198 + TotalIops float64 `json:"total_iops"`
199 + ReadBandwidth float64 `json:"read_bandwidth"`
200 + WriteBandwidth float64 `json:"write_bandwidth"`
201 + TotalBandwidth float64 `json:"total_bandwidth"`
202 + AvgReadLatency float64 `json:"avg_read_latency"`
203 + AvgWriteLatency float64 `json:"avg_write_latency"`
204 + AvgLatency float64 `json:"avg_latency"`
205 +}
src/go/plugin/go.d/collector/powerstore/collect.go new
+157
@@ -0,0 +1,157 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import (
6 + "sync"
7 + "time"
8 +
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/powerstore/client"
10 +)
11 +
12 +const discoveryEvery = 5
13 +
14 +func (c *Collector) collect() error {
15 + c.runs++
16 + if !c.lastDiscoveryOK || c.runs%discoveryEvery == 0 {
17 + if err := c.discovery(); err != nil {
18 + if !c.lastDiscoveryOK {
19 + return err
20 + }
21 + c.Warningf("discovery refresh failed, using previous data: %v", err)
22 + }
23 + }
24 +
25 + // Collect metrics from API concurrently, bounded by semaphore.
26 + var wg sync.WaitGroup
27 + for _, fn := range []func(){
28 + c.collectClusterSpace,
29 + c.collectAppliances,
30 + c.collectVolumes,
31 + c.collectNodes,
32 + c.collectFcPorts,
33 + c.collectEthPorts,
34 + c.collectFileSystems,
35 + c.collectAlerts,
36 + c.collectDriveWear,
37 + c.collectReplication,
38 + } {
39 + wg.Add(1)
40 + go func(f func()) {
41 + defer wg.Done()
42 + f()
43 + }(fn)
44 + }
45 + wg.Wait()
46 +
47 + // These use cached discovery data, no API calls.
48 + c.collectHardwareHealth()
49 + c.collectNASStatus()
50 +
51 + return nil
52 +}
53 +
54 +func (c *Collector) discovery() error {
55 + start := time.Now()
56 + c.Debugf("starting discovery")
57 +
58 + // Build results in a temp struct; only swap on full success.
59 + var d discovered
60 +
61 + clusters, err := c.client.Clusters()
62 + if err != nil {
63 + c.lastDiscoveryOK = false
64 + return err
65 + }
66 + d.clusters = clusters
67 +
68 + appliances, err := c.client.Appliances()
69 + if err != nil {
70 + c.lastDiscoveryOK = false
71 + return err
72 + }
73 + d.appliances = make(map[string]client.Appliance, len(appliances))
74 + for _, a := range appliances {
75 + d.appliances[a.ID] = a
76 + }
77 +
78 + volumes, err := c.client.Volumes()
79 + if err != nil {
80 + c.lastDiscoveryOK = false
81 + return err
82 + }
83 + d.volumes = make(map[string]client.Volume, len(volumes))
84 + for _, v := range volumes {
85 + if c.volumeMatches(v.Name) {
86 + d.volumes[v.ID] = v
87 + }
88 + }
89 +
90 + allHW, err := c.client.AllHardware()
91 + if err != nil {
92 + c.lastDiscoveryOK = false
93 + return err
94 + }
95 + d.hardware = allHW
96 + d.nodes = make(map[string]client.Node)
97 + d.drives = make(map[string]client.Hardware)
98 + for _, h := range allHW {
99 + switch h.Type {
100 + case "Node":
101 + d.nodes[h.ID] = client.Node{ID: h.ID, Name: h.Name, ApplianceID: h.ApplianceID}
102 + case "Drive":
103 + d.drives[h.ID] = h
104 + }
105 + }
106 +
107 + fcPorts, err := c.client.FcPorts()
108 + if err != nil {
109 + c.lastDiscoveryOK = false
110 + return err
111 + }
112 + d.fcPorts = make(map[string]client.FcPort, len(fcPorts))
113 + for _, p := range fcPorts {
114 + d.fcPorts[p.ID] = p
115 + }
116 +
117 + ethPorts, err := c.client.EthPorts()
118 + if err != nil {
119 + c.lastDiscoveryOK = false
120 + return err
121 + }
122 + d.ethPorts = make(map[string]client.EthPort, len(ethPorts))
123 + for _, p := range ethPorts {
124 + d.ethPorts[p.ID] = p
125 + }
126 +
127 + fileSystems, err := c.client.FileSystems()
128 + if err != nil {
129 + c.lastDiscoveryOK = false
130 + return err
131 + }
132 + d.fileSystems = make(map[string]client.FileSystem, len(fileSystems))
133 + for _, fs := range fileSystems {
134 + d.fileSystems[fs.ID] = fs
135 + }
136 +
137 + nas, err := c.client.NASServers()
138 + if err != nil {
139 + c.lastDiscoveryOK = false
140 + return err
141 + }
142 + d.nasServers = make(map[string]client.NAS, len(nas))
143 + for _, n := range nas {
144 + d.nasServers[n.ID] = n
145 + }
146 +
147 + // All API calls succeeded — atomic swap.
148 + c.discovered = d
149 + c.lastDiscoveryOK = true
150 +
151 + c.Debugf("discovery: %d clusters, %d appliances, %d volumes, %d nodes, %d FC ports, %d ETH ports, %d file systems, %d NAS servers, %d drives (took %s)",
152 + len(d.clusters), len(d.appliances), len(d.volumes), len(d.nodes),
153 + len(d.fcPorts), len(d.ethPorts), len(d.fileSystems), len(d.nasServers), len(d.drives),
154 + time.Since(start))
155 +
156 + return nil
157 +}
src/go/plugin/go.d/collector/powerstore/collect_alerts.go new
+33
@@ -0,0 +1,33 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +func (c *Collector) collectAlerts() {
6 + c.sem <- struct{}{}
7 + defer func() { <-c.sem }()
8 +
9 + alerts, err := c.client.Alerts("ACTIVE")
10 + if err != nil {
11 + c.Warningf("error collecting alerts: %v", err)
12 + return
13 + }
14 +
15 + var crit, major, minor, info float64
16 + for _, a := range alerts {
17 + switch a.Severity {
18 + case "Critical":
19 + crit++
20 + case "Major":
21 + major++
22 + case "Minor":
23 + minor++
24 + case "Info":
25 + info++
26 + }
27 + }
28 +
29 + c.mx.alerts.critical.Observe(crit)
30 + c.mx.alerts.major.Observe(major)
31 + c.mx.alerts.minor.Observe(minor)
32 + c.mx.alerts.info.Observe(info)
33 +}
src/go/plugin/go.d/collector/powerstore/collect_appliance.go new
+64
@@ -0,0 +1,64 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "sync"
6 +
7 +func (c *Collector) collectAppliances() {
8 + var wg sync.WaitGroup
9 +
10 + for id, app := range c.discovered.appliances {
11 + wg.Add(1)
12 + go func(id, name string) {
13 + defer wg.Done()
14 + c.sem <- struct{}{}
15 + defer func() { <-c.sem }()
16 +
17 + pm, err := c.client.PerformanceMetricsByAppliance(id)
18 + if err != nil {
19 + c.Warningf("error collecting appliance %s perf metrics: %v", id, err)
20 + } else if len(pm) > 0 {
21 + last := pm[len(pm)-1]
22 + c.mx.appliance.perf.readIops.WithLabelValues(name).Observe(last.ReadIops)
23 + c.mx.appliance.perf.writeIops.WithLabelValues(name).Observe(last.WriteIops)
24 + c.mx.appliance.perf.readBandwidth.WithLabelValues(name).Observe(last.ReadBandwidth)
25 + c.mx.appliance.perf.writeBandwidth.WithLabelValues(name).Observe(last.WriteBandwidth)
26 + c.mx.appliance.perf.avgReadLatency.WithLabelValues(name).Observe(last.AvgReadLatency)
27 + c.mx.appliance.perf.avgWriteLatency.WithLabelValues(name).Observe(last.AvgWriteLatency)
28 + c.mx.appliance.perf.avgLatency.WithLabelValues(name).Observe(last.AvgLatency)
29 + c.mx.appliance.cpu.WithLabelValues(name).Observe(last.IoWorkloadCPUUtilization)
30 + }
31 +
32 + sm, err := c.client.SpaceMetricsByAppliance(id)
33 + if err != nil {
34 + c.Warningf("error collecting appliance %s space metrics: %v", id, err)
35 + } else if len(sm) > 0 {
36 + last := sm[len(sm)-1]
37 + if last.PhysicalTotal != nil {
38 + c.mx.appliance.space.physicalTotal.WithLabelValues(name).Observe(float64(*last.PhysicalTotal))
39 + }
40 + if last.PhysicalUsed != nil {
41 + c.mx.appliance.space.physicalUsed.WithLabelValues(name).Observe(float64(*last.PhysicalUsed))
42 + }
43 + if last.LogicalProvisioned != nil {
44 + c.mx.appliance.space.logicalProvisioned.WithLabelValues(name).Observe(float64(*last.LogicalProvisioned))
45 + }
46 + if last.LogicalUsed != nil {
47 + c.mx.appliance.space.logicalUsed.WithLabelValues(name).Observe(float64(*last.LogicalUsed))
48 + }
49 + if last.DataPhysicalUsed != nil {
50 + c.mx.appliance.space.dataPhysicalUsed.WithLabelValues(name).Observe(float64(*last.DataPhysicalUsed))
51 + }
52 + if last.SharedLogicalUsed != nil {
53 + c.mx.appliance.space.sharedLogicalUsed.WithLabelValues(name).Observe(float64(*last.SharedLogicalUsed))
54 + }
55 + c.mx.appliance.space.efficiencyRatio.WithLabelValues(name).Observe(last.EfficiencyRatio)
56 + c.mx.appliance.space.dataReduction.WithLabelValues(name).Observe(last.DataReduction)
57 + c.mx.appliance.space.snapshotSavings.WithLabelValues(name).Observe(last.SnapshotSavings)
58 + c.mx.appliance.space.thinSavings.WithLabelValues(name).Observe(last.ThinSavings)
59 + }
60 + }(id, app.Name)
61 + }
62 +
63 + wg.Wait()
64 +}
src/go/plugin/go.d/collector/powerstore/collect_cluster.go new
+47
@@ -0,0 +1,47 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +func (c *Collector) collectClusterSpace() {
6 + if len(c.discovered.clusters) == 0 {
7 + return
8 + }
9 +
10 + c.sem <- struct{}{}
11 + defer func() { <-c.sem }()
12 +
13 + cl := c.discovered.clusters[0]
14 + sm, err := c.client.SpaceMetricsByCluster(cl.ID)
15 + if err != nil {
16 + c.Warningf("error collecting cluster space metrics: %v", err)
17 + return
18 + }
19 + if len(sm) == 0 {
20 + return
21 + }
22 +
23 + last := sm[len(sm)-1]
24 + mx := c.mx.cluster
25 + if last.PhysicalTotal != nil {
26 + mx.spacePhysicalTotal.Observe(float64(*last.PhysicalTotal))
27 + }
28 + if last.PhysicalUsed != nil {
29 + mx.spacePhysicalUsed.Observe(float64(*last.PhysicalUsed))
30 + }
31 + if last.LogicalProvisioned != nil {
32 + mx.spaceLogicalProvisioned.Observe(float64(*last.LogicalProvisioned))
33 + }
34 + if last.LogicalUsed != nil {
35 + mx.spaceLogicalUsed.Observe(float64(*last.LogicalUsed))
36 + }
37 + if last.DataPhysicalUsed != nil {
38 + mx.spaceDataPhysicalUsed.Observe(float64(*last.DataPhysicalUsed))
39 + }
40 + if last.SharedLogicalUsed != nil {
41 + mx.spaceSharedLogicalUsed.Observe(float64(*last.SharedLogicalUsed))
42 + }
43 + mx.spaceEfficiencyRatio.Observe(float64(last.EfficiencyRatio))
44 + mx.spaceDataReduction.Observe(float64(last.DataReduction))
45 + mx.spaceSnapshotSavings.Observe(float64(last.SnapshotSavings))
46 + mx.spaceThinSavings.Observe(float64(last.ThinSavings))
47 +}
src/go/plugin/go.d/collector/powerstore/collect_drives.go new
+32
@@ -0,0 +1,32 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "sync"
6 +
7 +func (c *Collector) collectDriveWear() {
8 + var wg sync.WaitGroup
9 +
10 + for id, drv := range c.discovered.drives {
11 + wg.Add(1)
12 + go func(id, name string) {
13 + defer wg.Done()
14 + c.sem <- struct{}{}
15 + defer func() { <-c.sem }()
16 +
17 + wm, err := c.client.WearMetricsByDrive(id)
18 + if err != nil {
19 + c.Warningf("error collecting drive %s wear metrics: %v", id, err)
20 + return
21 + }
22 + if len(wm) == 0 {
23 + return
24 + }
25 +
26 + last := wm[len(wm)-1]
27 + c.mx.drive.enduranceRemaining.WithLabelValues(name).Observe(float64(last.PercentEnduranceRemaining))
28 + }(id, drv.Name)
29 + }
30 +
31 + wg.Wait()
32 +}
src/go/plugin/go.d/collector/powerstore/collect_filesystem.go new
+34
@@ -0,0 +1,34 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "sync"
6 +
7 +func (c *Collector) collectFileSystems() {
8 + var wg sync.WaitGroup
9 +
10 + for id, fs := range c.discovered.fileSystems {
11 + wg.Add(1)
12 + go func(id, name string) {
13 + defer wg.Done()
14 + c.sem <- struct{}{}
15 + defer func() { <-c.sem }()
16 +
17 + pm, err := c.client.PerformanceMetricsByFileSystem(id)
18 + if err != nil {
19 + c.Warningf("error collecting filesystem %s perf metrics: %v", id, err)
20 + } else if len(pm) > 0 {
21 + last := pm[len(pm)-1]
22 + c.mx.fileSystem.perf.readIops.WithLabelValues(name).Observe(last.ReadIops)
23 + c.mx.fileSystem.perf.writeIops.WithLabelValues(name).Observe(last.WriteIops)
24 + c.mx.fileSystem.perf.readBandwidth.WithLabelValues(name).Observe(last.ReadBandwidth)
25 + c.mx.fileSystem.perf.writeBandwidth.WithLabelValues(name).Observe(last.WriteBandwidth)
26 + c.mx.fileSystem.perf.avgReadLatency.WithLabelValues(name).Observe(last.AvgReadLatency)
27 + c.mx.fileSystem.perf.avgWriteLatency.WithLabelValues(name).Observe(last.AvgWriteLatency)
28 + c.mx.fileSystem.perf.avgLatency.WithLabelValues(name).Observe(last.AvgLatency)
29 + }
30 + }(id, fs.Name)
31 + }
32 +
33 + wg.Wait()
34 +}
src/go/plugin/go.d/collector/powerstore/collect_hardware.go new
+79
@@ -0,0 +1,79 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +// collectHardwareHealth classifies hardware components by lifecycle state.
6 +// Uses cached hardware data from discovery to avoid redundant API calls.
7 +func (c *Collector) collectHardwareHealth() {
8 + var fanOK, fanDeg, fanFail, fanUnk float64
9 + var psuOK, psuDeg, psuFail, psuUnk float64
10 + var drvOK, drvDeg, drvFail, drvUnk float64
11 + var batOK, batDeg, batFail, batUnk float64
12 + var nodOK, nodDeg, nodFail, nodUnk float64
13 +
14 + for _, h := range c.discovered.hardware {
15 + ok, deg, fail, unk := classifyState(h.LifecycleState)
16 + switch h.Type {
17 + case "Fan":
18 + fanOK += ok
19 + fanDeg += deg
20 + fanFail += fail
21 + fanUnk += unk
22 + case "Power_Supply":
23 + psuOK += ok
24 + psuDeg += deg
25 + psuFail += fail
26 + psuUnk += unk
27 + case "Drive":
28 + drvOK += ok
29 + drvDeg += deg
30 + drvFail += fail
31 + drvUnk += unk
32 + case "Battery":
33 + batOK += ok
34 + batDeg += deg
35 + batFail += fail
36 + batUnk += unk
37 + case "Node":
38 + nodOK += ok
39 + nodDeg += deg
40 + nodFail += fail
41 + nodUnk += unk
42 + }
43 + }
44 +
45 + hw := c.mx.hardware
46 + hw.fanOK.Observe(fanOK)
47 + hw.fanDegraded.Observe(fanDeg)
48 + hw.fanFailed.Observe(fanFail)
49 + hw.fanUnknown.Observe(fanUnk)
50 + hw.psuOK.Observe(psuOK)
51 + hw.psuDegraded.Observe(psuDeg)
52 + hw.psuFailed.Observe(psuFail)
53 + hw.psuUnknown.Observe(psuUnk)
54 + hw.driveOK.Observe(drvOK)
55 + hw.driveDegraded.Observe(drvDeg)
56 + hw.driveFailed.Observe(drvFail)
57 + hw.driveUnknown.Observe(drvUnk)
58 + hw.batteryOK.Observe(batOK)
59 + hw.batteryDegraded.Observe(batDeg)
60 + hw.batteryFailed.Observe(batFail)
61 + hw.batteryUnknown.Observe(batUnk)
62 + hw.nodeOK.Observe(nodOK)
63 + hw.nodeDegraded.Observe(nodDeg)
64 + hw.nodeFailed.Observe(nodFail)
65 + hw.nodeUnknown.Observe(nodUnk)
66 +}
67 +
68 +func classifyState(state string) (ok, degraded, failed, unknown float64) {
69 + switch state {
70 + case "Healthy", "healthy":
71 + return 1, 0, 0, 0
72 + case "Degraded", "degraded":
73 + return 0, 1, 0, 0
74 + case "Failed", "failed", "Error", "error":
75 + return 0, 0, 1, 0
76 + default:
77 + return 0, 0, 0, 1
78 + }
79 +}
src/go/plugin/go.d/collector/powerstore/collect_nas.go new
+25
@@ -0,0 +1,25 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +func (c *Collector) collectNASStatus() {
6 + var started, stopped, degraded, unknown float64
7 +
8 + for _, nas := range c.discovered.nasServers {
9 + switch nas.OperationalStatus {
10 + case "Started":
11 + started++
12 + case "Stopped":
13 + stopped++
14 + case "Degraded":
15 + degraded++
16 + default:
17 + unknown++
18 + }
19 + }
20 +
21 + c.mx.nas.started.Observe(started)
22 + c.mx.nas.stopped.Observe(stopped)
23 + c.mx.nas.degraded.Observe(degraded)
24 + c.mx.nas.unknown.Observe(unknown)
25 +}
src/go/plugin/go.d/collector/powerstore/collect_node.go new
+37
@@ -0,0 +1,37 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "sync"
6 +
7 +func (c *Collector) collectNodes() {
8 + var wg sync.WaitGroup
9 +
10 + for id, node := range c.discovered.nodes {
11 + wg.Add(1)
12 + go func(id, name string) {
13 + defer wg.Done()
14 + c.sem <- struct{}{}
15 + defer func() { <-c.sem }()
16 +
17 + pm, err := c.client.PerformanceMetricsByNode(id)
18 + if err != nil {
19 + c.Warningf("error collecting node %s perf metrics: %v", id, err)
20 + } else if len(pm) > 0 {
21 + last := pm[len(pm)-1]
22 + c.mx.node.perf.readIops.WithLabelValues(name).Observe(last.ReadIops)
23 + c.mx.node.perf.writeIops.WithLabelValues(name).Observe(last.WriteIops)
24 + c.mx.node.perf.readBandwidth.WithLabelValues(name).Observe(last.ReadBandwidth)
25 + c.mx.node.perf.writeBandwidth.WithLabelValues(name).Observe(last.WriteBandwidth)
26 + c.mx.node.perf.avgReadLatency.WithLabelValues(name).Observe(last.AvgReadLatency)
27 + c.mx.node.perf.avgWriteLatency.WithLabelValues(name).Observe(last.AvgWriteLatency)
28 + c.mx.node.perf.avgLatency.WithLabelValues(name).Observe(last.AvgLatency)
29 + if last.CurrentLogins != nil {
30 + c.mx.node.currentLogins.WithLabelValues(name).Observe(float64(*last.CurrentLogins))
31 + }
32 + }
33 + }(id, node.Name)
34 + }
35 +
36 + wg.Wait()
37 +}
src/go/plugin/go.d/collector/powerstore/collect_ports.go new
+75
@@ -0,0 +1,75 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "sync"
6 +
7 +func (c *Collector) collectFcPorts() {
8 + var wg sync.WaitGroup
9 +
10 + for id, port := range c.discovered.fcPorts {
11 + wg.Add(1)
12 + go func(id, name string, linkUp bool) {
13 + defer wg.Done()
14 + c.sem <- struct{}{}
15 + defer func() { <-c.sem }()
16 +
17 + var linkVal float64
18 + if linkUp {
19 + linkVal = 1
20 + }
21 + c.mx.fcPort.linkUp.WithLabelValues(name).Observe(linkVal)
22 +
23 + pm, err := c.client.PerformanceMetricsByFcPort(id)
24 + if err != nil {
25 + c.Warningf("error collecting FC port %s perf metrics: %v", id, err)
26 + } else if len(pm) > 0 {
27 + last := pm[len(pm)-1]
28 + c.mx.fcPort.perf.readIops.WithLabelValues(name).Observe(last.ReadIops)
29 + c.mx.fcPort.perf.writeIops.WithLabelValues(name).Observe(last.WriteIops)
30 + c.mx.fcPort.perf.readBandwidth.WithLabelValues(name).Observe(last.ReadBandwidth)
31 + c.mx.fcPort.perf.writeBandwidth.WithLabelValues(name).Observe(last.WriteBandwidth)
32 + c.mx.fcPort.perf.avgReadLatency.WithLabelValues(name).Observe(last.AvgReadLatency)
33 + c.mx.fcPort.perf.avgWriteLatency.WithLabelValues(name).Observe(last.AvgWriteLatency)
34 + c.mx.fcPort.perf.avgLatency.WithLabelValues(name).Observe(last.AvgLatency)
35 + }
36 + }(id, port.Name, port.IsLinkUp)
37 + }
38 +
39 + wg.Wait()
40 +}
41 +
42 +func (c *Collector) collectEthPorts() {
43 + var wg sync.WaitGroup
44 +
45 + for id, port := range c.discovered.ethPorts {
46 + wg.Add(1)
47 + go func(id, name string, linkUp bool) {
48 + defer wg.Done()
49 + c.sem <- struct{}{}
50 + defer func() { <-c.sem }()
51 +
52 + var linkVal float64
53 + if linkUp {
54 + linkVal = 1
55 + }
56 + c.mx.ethPort.linkUp.WithLabelValues(name).Observe(linkVal)
57 +
58 + pm, err := c.client.EthPortPerformanceMetrics(id)
59 + if err != nil {
60 + c.Warningf("error collecting ETH port %s perf metrics: %v", id, err)
61 + } else if len(pm) > 0 {
62 + last := pm[len(pm)-1]
63 + c.mx.ethPort.bytesRxPs.WithLabelValues(name).Observe(float64(last.BytesRxPs))
64 + c.mx.ethPort.bytesTxPs.WithLabelValues(name).Observe(float64(last.BytesTxPs))
65 + c.mx.ethPort.pktRxPs.WithLabelValues(name).Observe(float64(last.PktRxPs))
66 + c.mx.ethPort.pktTxPs.WithLabelValues(name).Observe(float64(last.PktTxPs))
67 + c.mx.ethPort.pktRxCrcErrorPs.WithLabelValues(name).Observe(float64(last.PktRxCrcErrorPs))
68 + c.mx.ethPort.pktRxNoBufferErrorPs.WithLabelValues(name).Observe(float64(last.PktRxNoBufferErrorPs))
69 + c.mx.ethPort.pktTxErrorPs.WithLabelValues(name).Observe(float64(last.PktTxErrorPs))
70 + }
71 + }(id, port.Name, port.IsLinkUp)
72 + }
73 +
74 + wg.Wait()
75 +}
src/go/plugin/go.d/collector/powerstore/collect_replication.go new
+49
@@ -0,0 +1,49 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "sync"
6 +
7 +func (c *Collector) collectReplication() {
8 + var wg sync.WaitGroup
9 + var mu sync.Mutex
10 +
11 + var dataRemaining, dataTransferred int64
12 + var transferRate float64
13 +
14 + for id := range c.discovered.appliances {
15 + wg.Add(1)
16 + go func(id string) {
17 + defer wg.Done()
18 + c.sem <- struct{}{}
19 + defer func() { <-c.sem }()
20 +
21 + cm, err := c.client.CopyMetricsByAppliance(id)
22 + if err != nil {
23 + c.Warningf("error collecting appliance %s copy metrics: %v", id, err)
24 + return
25 + }
26 + if len(cm) == 0 {
27 + return
28 + }
29 +
30 + last := cm[len(cm)-1]
31 +
32 + mu.Lock()
33 + if last.DataRemaining != nil {
34 + dataRemaining += *last.DataRemaining
35 + }
36 + if last.DataTransferred != nil {
37 + dataTransferred += *last.DataTransferred
38 + }
39 + transferRate += last.TransferRate
40 + mu.Unlock()
41 + }(id)
42 + }
43 +
44 + wg.Wait()
45 +
46 + c.mx.replication.dataRemaining.Observe(float64(dataRemaining))
47 + c.mx.replication.dataTransferred.Observe(float64(dataTransferred))
48 + c.mx.replication.transferRate.Observe(transferRate)
49 +}
src/go/plugin/go.d/collector/powerstore/collect_volume.go new
+48
@@ -0,0 +1,48 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "sync"
6 +
7 +func (c *Collector) collectVolumes() {
8 + var wg sync.WaitGroup
9 +
10 + for id, vol := range c.discovered.volumes {
11 + wg.Add(1)
12 + go func(id, name string) {
13 + defer wg.Done()
14 + c.sem <- struct{}{}
15 + defer func() { <-c.sem }()
16 +
17 + pm, err := c.client.PerformanceMetricsByVolume(id)
18 + if err != nil {
19 + c.Warningf("error collecting volume %s perf metrics: %v", id, err)
20 + } else if len(pm) > 0 {
21 + last := pm[len(pm)-1]
22 + c.mx.volume.perf.readIops.WithLabelValues(name).Observe(last.ReadIops)
23 + c.mx.volume.perf.writeIops.WithLabelValues(name).Observe(last.WriteIops)
24 + c.mx.volume.perf.readBandwidth.WithLabelValues(name).Observe(last.ReadBandwidth)
25 + c.mx.volume.perf.writeBandwidth.WithLabelValues(name).Observe(last.WriteBandwidth)
26 + c.mx.volume.perf.avgReadLatency.WithLabelValues(name).Observe(last.AvgReadLatency)
27 + c.mx.volume.perf.avgWriteLatency.WithLabelValues(name).Observe(last.AvgWriteLatency)
28 + c.mx.volume.perf.avgLatency.WithLabelValues(name).Observe(last.AvgLatency)
29 + }
30 +
31 + sm, err := c.client.SpaceMetricsByVolume(id)
32 + if err != nil {
33 + c.Warningf("error collecting volume %s space metrics: %v", id, err)
34 + } else if len(sm) > 0 {
35 + last := sm[len(sm)-1]
36 + if last.LogicalProvisioned != nil {
37 + c.mx.volume.spaceLogicalProv.WithLabelValues(name).Observe(float64(*last.LogicalProvisioned))
38 + }
39 + if last.LogicalUsed != nil {
40 + c.mx.volume.spaceLogicalUsed.WithLabelValues(name).Observe(float64(*last.LogicalUsed))
41 + }
42 + c.mx.volume.spaceThinSavings.WithLabelValues(name).Observe(last.ThinSavings)
43 + }
44 + }(id, vol.Name)
45 + }
46 +
47 + wg.Wait()
48 +}
src/go/plugin/go.d/collector/powerstore/collector.go new
+156
@@ -0,0 +1,156 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
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/powerstore/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("powerstore", 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 +const maxConcurrentAPIRequests = 10
38 +
39 +func New() *Collector {
40 + store := metrix.NewCollectorStore()
41 + mx := newCollectorMetrics(store)
42 +
43 + return &Collector{
44 + Config: Config{
45 + HTTPConfig: web.HTTPConfig{
46 + RequestConfig: web.RequestConfig{
47 + URL: "https://127.0.0.1",
48 + },
49 + ClientConfig: web.ClientConfig{
50 + Timeout: confopt.Duration(30 * time.Second),
51 + },
52 + },
53 + },
54 + store: store,
55 + mx: mx,
56 + sem: make(chan struct{}, maxConcurrentAPIRequests),
57 + }
58 +}
59 +
60 +type Config struct {
61 + Vnode string `yaml:"vnode,omitempty" json:"vnode"`
62 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
63 + AutoDetectionRetry int `yaml:"autodetection_retry,omitempty" json:"autodetection_retry"`
64 + web.HTTPConfig `yaml:",inline" json:""`
65 + VolumeSelector string `yaml:"volume_selector,omitempty" json:"volume_selector"`
66 +}
67 +
68 +type (
69 + Collector struct {
70 + collectorapi.Base
71 + Config `yaml:",inline" json:""`
72 +
73 + store metrix.CollectorStore
74 + mx *collectorMetrics
75 +
76 + client *client.Client
77 +
78 + discovered discovered
79 + lastDiscoveryOK bool
80 + runs int
81 +
82 + volMatcher matcher.Matcher
83 + sem chan struct{} // limits concurrent API calls
84 + }
85 + discovered struct {
86 + clusters []client.Cluster
87 + appliances map[string]client.Appliance
88 + volumes map[string]client.Volume
89 + nodes map[string]client.Node
90 + fcPorts map[string]client.FcPort
91 + ethPorts map[string]client.EthPort
92 + fileSystems map[string]client.FileSystem
93 + nasServers map[string]client.NAS
94 + drives map[string]client.Hardware
95 + hardware []client.Hardware
96 + }
97 +)
98 +
99 +func (c *Collector) Configuration() any {
100 + return c.Config
101 +}
102 +
103 +func (c *Collector) Init(context.Context) error {
104 + if c.Username == "" || c.Password == "" {
105 + return errors.New("config: username and password aren't set")
106 + }
107 +
108 + cli, err := client.New(c.ClientConfig, c.RequestConfig)
109 + if err != nil {
110 + return fmt.Errorf("error creating PowerStore client: %v", err)
111 + }
112 + c.client = cli
113 +
114 + if c.VolumeSelector != "" {
115 + m, err := matcher.NewSimplePatternsMatcher(c.VolumeSelector)
116 + if err != nil {
117 + return fmt.Errorf("error creating volume selector: %v", err)
118 + }
119 + c.volMatcher = m
120 + }
121 +
122 + c.Debugf("using URL %s", c.URL)
123 + c.Debugf("using timeout: %s", c.Timeout)
124 +
125 + return nil
126 +}
127 +
128 +func (c *Collector) volumeMatches(name string) bool {
129 + if c.volMatcher == nil {
130 + return true
131 + }
132 + return c.volMatcher.MatchString(name)
133 +}
134 +
135 +func (c *Collector) Check(context.Context) error {
136 + if err := c.client.Login(); err != nil {
137 + return err
138 + }
139 + // Verify API access by running discovery (no metrics written).
140 + return c.discovery()
141 +}
142 +
143 +func (c *Collector) Collect(context.Context) error {
144 + return c.collect()
145 +}
146 +
147 +func (c *Collector) MetricStore() metrix.CollectorStore { return c.store }
148 +
149 +func (c *Collector) ChartTemplateYAML() string { return chartTemplateYAML }
150 +
151 +func (c *Collector) Cleanup(context.Context) {
152 + if c.client == nil {
153 + return
154 + }
155 + c.client.Logout()
156 +}
src/go/plugin/go.d/collector/powerstore/collector_test.go new
+384
@@ -0,0 +1,384 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import (
6 + "context"
7 + "encoding/json"
8 + "net/http"
9 + "net/http/httptest"
10 + "os"
11 + "strings"
12 + "testing"
13 +
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/chartengine"
17 + "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
18 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
19 + "github.com/stretchr/testify/assert"
20 + "github.com/stretchr/testify/require"
21 +)
22 +
23 +func TestCollector_Init(t *testing.T) {
24 + tests := map[string]struct {
25 + config Config
26 + wantFail bool
27 + }{
28 + "success with valid config": {
29 + config: Config{
30 + HTTPConfig: web.HTTPConfig{
31 + RequestConfig: web.RequestConfig{
32 + URL: "https://127.0.0.1",
33 + Username: "admin",
34 + Password: "password",
35 + },
36 + },
37 + },
38 + },
39 + "fail without username": {
40 + wantFail: true,
41 + config: Config{
42 + HTTPConfig: web.HTTPConfig{
43 + RequestConfig: web.RequestConfig{
44 + URL: "https://127.0.0.1",
45 + Password: "password",
46 + },
47 + },
48 + },
49 + },
50 + "fail without password": {
51 + wantFail: true,
52 + config: Config{
53 + HTTPConfig: web.HTTPConfig{
54 + RequestConfig: web.RequestConfig{
55 + URL: "https://127.0.0.1",
56 + Username: "admin",
57 + },
58 + },
59 + },
60 + },
61 + }
62 +
63 + for name, test := range tests {
64 + t.Run(name, func(t *testing.T) {
65 + collr := New()
66 + collr.Config = test.config
67 +
68 + if test.wantFail {
69 + assert.Error(t, collr.Init(context.Background()))
70 + } else {
71 + assert.NoError(t, collr.Init(context.Background()))
72 + }
73 + })
74 + }
75 +}
76 +
77 +func TestCollector_Check(t *testing.T) {
78 + srv := newMockPowerStoreServer()
79 + defer srv.Close()
80 +
81 + collr := New()
82 + collr.Config = Config{
83 + HTTPConfig: web.HTTPConfig{
84 + RequestConfig: web.RequestConfig{
85 + URL: srv.URL,
86 + Username: "admin",
87 + Password: "password",
88 + },
89 + },
90 + }
91 +
92 + require.NoError(t, collr.Init(context.Background()))
93 + assert.NoError(t, collr.Check(context.Background()))
94 +}
95 +
96 +func TestCollector_Collect(t *testing.T) {
97 + srv := newMockPowerStoreServer()
98 + defer srv.Close()
99 +
100 + collr := New()
101 + collr.Config = Config{
102 + HTTPConfig: web.HTTPConfig{
103 + RequestConfig: web.RequestConfig{
104 + URL: srv.URL,
105 + Username: "admin",
106 + Password: "password",
107 + },
108 + },
109 + }
110 +
111 + require.NoError(t, collr.Init(context.Background()))
112 + require.NoError(t, collr.Check(context.Background()))
113 +
114 + cc := mustCycleController(t, collr.MetricStore())
115 + cc.BeginCycle()
116 + require.NoError(t, collr.Collect(context.Background()))
117 + cc.CommitCycleSuccess()
118 +
119 + r := collr.MetricStore().Read(metrix.ReadRaw())
120 +
121 + // Cluster space metrics
122 + assertValue(t, r, "cluster_space_physical_total", nil, 10995116277760)
123 + assertValue(t, r, "cluster_space_physical_used", nil, 5497558138880)
124 + assertValue(t, r, "cluster_space_efficiency_ratio", nil, 2.5)
125 +
126 + // Appliance metrics
127 + appLabels := metrix.Labels{"appliance": "Appliance-1"}
128 + assertHasValue(t, r, "appliance_perf_read_iops", appLabels)
129 + assertHasValue(t, r, "appliance_cpu_utilization", appLabels)
130 + assertHasValue(t, r, "appliance_space_physical_total", appLabels)
131 + assertHasValue(t, r, "appliance_space_logical_provisioned", appLabels)
132 + assertHasValue(t, r, "appliance_space_logical_used", appLabels)
133 + assertHasValue(t, r, "appliance_space_data_physical_used", appLabels)
134 + assertHasValue(t, r, "appliance_space_shared_logical_used", appLabels)
135 + assertValue(t, r, "appliance_space_efficiency_ratio", appLabels, 2.5)
136 + assertValue(t, r, "appliance_space_data_reduction", appLabels, 1.8)
137 + assertValue(t, r, "appliance_space_snapshot_savings", appLabels, 0.3)
138 + assertValue(t, r, "appliance_space_thin_savings", appLabels, 0.7)
139 +
140 + // Volume metrics
141 + v1Labels := metrix.Labels{"volume": "prod-vol-01"}
142 + v2Labels := metrix.Labels{"volume": "test-vol-01"}
143 + assertHasValue(t, r, "volume_perf_read_iops", v1Labels)
144 + assertHasValue(t, r, "volume_perf_read_iops", v2Labels)
145 + assertHasValue(t, r, "volume_space_logical_provisioned", v1Labels)
146 + assertHasValue(t, r, "volume_space_thin_savings", v1Labels)
147 +
148 + // Node metrics
149 + nodeLabels := metrix.Labels{"node": "Node-A"}
150 + assertHasValue(t, r, "node_perf_read_iops", nodeLabels)
151 + assertValue(t, r, "node_current_logins", nodeLabels, 5)
152 +
153 + // FC port metrics
154 + fcLabels := metrix.Labels{"fc_port": "FC-Port-0"}
155 + assertHasValue(t, r, "fc_port_perf_read_iops", fcLabels)
156 + assertHasValue(t, r, "fc_port_perf_avg_read_latency", fcLabels)
157 + assertValue(t, r, "fc_port_link_up", fcLabels, 1)
158 +
159 + // ETH port metrics
160 + ethLabels := metrix.Labels{"eth_port": "ETH-Port-0"}
161 + assertHasValue(t, r, "eth_port_bytes_rx_ps", ethLabels)
162 + assertValue(t, r, "eth_port_link_up", ethLabels, 1)
163 +
164 + // Filesystem metrics
165 + fsLabels := metrix.Labels{"filesystem": "nfs-share-01"}
166 + assertHasValue(t, r, "file_system_perf_read_iops", fsLabels)
167 +
168 + // Hardware health metrics (testdata: 2 fans OK, 1 degraded; 2 PSUs; 2 drives; 1 battery; 2 nodes)
169 + assertValue(t, r, "hardware_fan_ok", nil, 2)
170 + assertValue(t, r, "hardware_fan_degraded", nil, 1)
171 + assertValue(t, r, "hardware_psu_ok", nil, 2)
172 + assertValue(t, r, "hardware_drive_ok", nil, 2)
173 + assertValue(t, r, "hardware_battery_ok", nil, 1)
174 + assertValue(t, r, "hardware_node_ok", nil, 2)
175 +
176 + // Alert metrics
177 + assertValue(t, r, "alerts_minor", nil, 1)
178 + assertValue(t, r, "alerts_info", nil, 1)
179 + assertValue(t, r, "alerts_critical", nil, 0)
180 +
181 + // Drive wear metrics (D1 = "Drive-1" from hardware_all.json)
182 + driveLabels := metrix.Labels{"drive": "Drive-1"}
183 + assertHasValue(t, r, "drive_endurance_remaining", driveLabels)
184 +
185 + // NAS status metrics
186 + assertValue(t, r, "nas_started", nil, 1)
187 + assertValue(t, r, "nas_stopped", nil, 0)
188 +
189 + // Replication metrics
190 + assertValue(t, r, "copy_data_remaining", nil, 1073741824)
191 + assertValue(t, r, "copy_data_transferred", nil, 5368709120)
192 + assertHasValue(t, r, "copy_transfer_rate", nil)
193 +
194 + // Chart coverage: verify all chart template dimensions are materialized
195 + collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
196 +}
197 +
198 +func TestCollector_CollectWithVolumeSelector(t *testing.T) {
199 + srv := newMockPowerStoreServer()
200 + defer srv.Close()
201 +
202 + collr := New()
203 + collr.Config = Config{
204 + HTTPConfig: web.HTTPConfig{
205 + RequestConfig: web.RequestConfig{
206 + URL: srv.URL,
207 + Username: "admin",
208 + Password: "password",
209 + },
210 + },
211 + VolumeSelector: "prod-*",
212 + }
213 +
214 + require.NoError(t, collr.Init(context.Background()))
215 + require.NoError(t, collr.Check(context.Background()))
216 +
217 + cc := mustCycleController(t, collr.MetricStore())
218 + cc.BeginCycle()
219 + require.NoError(t, collr.Collect(context.Background()))
220 + cc.CommitCycleSuccess()
221 +
222 + r := collr.MetricStore().Read(metrix.ReadRaw())
223 +
224 + // prod-vol-01 should be included
225 + assertHasValue(t, r, "volume_perf_read_iops", metrix.Labels{"volume": "prod-vol-01"})
226 + // test-vol-01 should be excluded
227 + _, ok := r.Value("volume_perf_read_iops", metrix.Labels{"volume": "test-vol-01"})
228 + assert.False(t, ok, "test-vol-01 should be excluded by volume selector")
229 +}
230 +
231 +func TestCollector_Cleanup(t *testing.T) {
232 + collr := New()
233 + assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
234 +
235 + srv := newMockPowerStoreServer()
236 + defer srv.Close()
237 +
238 + collr.Config = Config{
239 + HTTPConfig: web.HTTPConfig{
240 + RequestConfig: web.RequestConfig{
241 + URL: srv.URL,
242 + Username: "admin",
243 + Password: "password",
244 + },
245 + },
246 + }
247 +
248 + require.NoError(t, collr.Init(context.Background()))
249 + require.NoError(t, collr.Check(context.Background()))
250 + assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
251 +}
252 +
253 +func TestCollector_ChartTemplateYAML(t *testing.T) {
254 + templateYAML := New().ChartTemplateYAML()
255 + collecttest.AssertChartTemplateSchema(t, templateYAML)
256 +
257 + spec, err := charttpl.DecodeYAML([]byte(templateYAML))
258 + require.NoError(t, err)
259 + require.NoError(t, spec.Validate())
260 +
261 + _, err = chartengine.Compile(spec, 1)
262 + require.NoError(t, err)
263 +}
264 +
265 +func mustCycleController(t *testing.T, store metrix.CollectorStore) metrix.CycleController {
266 + t.Helper()
267 + managed, ok := metrix.AsCycleManagedStore(store)
268 + require.True(t, ok, "store does not expose cycle control")
269 + return managed.CycleController()
270 +}
271 +
272 +func assertValue(t *testing.T, r metrix.Reader, name string, labels metrix.Labels, want float64) {
273 + t.Helper()
274 + got, ok := r.Value(name, labels)
275 + require.Truef(t, ok, "expected metric %s labels=%v", name, labels)
276 + assert.InDeltaf(t, want, got, 1e-9, "unexpected value for %s labels=%v: got %v want %v", name, labels, got, want)
277 +}
278 +
279 +func assertHasValue(t *testing.T, r metrix.Reader, name string, labels metrix.Labels) {
280 + t.Helper()
281 + _, ok := r.Value(name, labels)
282 + assert.Truef(t, ok, "expected metric %s labels=%v to exist", name, labels)
283 +}
284 +
285 +func newMockPowerStoreServer() *httptest.Server {
286 + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
287 + w.Header().Set("DELL-EMC-TOKEN", "mock-csrf-token")
288 + w.Header().Set("Content-Type", "application/json")
289 +
290 + p := r.URL.Path
291 +
292 + switch {
293 + case strings.HasSuffix(p, "/login_session"):
294 + w.WriteHeader(http.StatusOK)
295 + _, _ = w.Write([]byte(`{"id":"session-1"}`))
296 + case strings.HasSuffix(p, "/cluster"):
297 + writeTestData(w, "testdata/cluster.json")
298 + case strings.HasSuffix(p, "/appliance"):
299 + writeTestData(w, "testdata/appliance.json")
300 + case strings.HasSuffix(p, "/volume"):
301 + writeTestData(w, "testdata/volume.json")
302 + case strings.HasSuffix(p, "/hardware"):
303 + hwType := r.URL.Query().Get("type")
304 + switch hwType {
305 + case "eq.Node":
306 + writeTestData(w, "testdata/hardware_node.json")
307 + case "eq.Fan":
308 + writeTestData(w, "testdata/hardware_fan.json")
309 + case "eq.Power_Supply":
310 + writeTestData(w, "testdata/hardware_psu.json")
311 + case "eq.Drive":
312 + writeTestData(w, "testdata/hardware_drive.json")
313 + case "eq.Battery":
314 + writeTestData(w, "testdata/hardware_battery.json")
315 + case "":
316 + writeTestData(w, "testdata/hardware_all.json")
317 + default:
318 + _, _ = w.Write([]byte(`[]`))
319 + }
320 + case strings.HasSuffix(p, "/alert"):
321 + writeTestData(w, "testdata/alert.json")
322 + case strings.HasSuffix(p, "/fc_port"):
323 + writeTestData(w, "testdata/fc_port.json")
324 + case strings.HasSuffix(p, "/eth_port"):
325 + writeTestData(w, "testdata/eth_port.json")
326 + case strings.HasSuffix(p, "/file_system"):
327 + writeTestData(w, "testdata/file_system.json")
328 + case strings.HasSuffix(p, "/nas_server"):
329 + writeTestData(w, "testdata/nas_server.json")
330 + case strings.HasSuffix(p, "/metrics/generate"):
331 + handleMetricsGenerate(w, r)
332 + default:
333 + w.WriteHeader(http.StatusNotFound)
334 + }
335 + }))
336 +}
337 +
338 +func handleMetricsGenerate(w http.ResponseWriter, r *http.Request) {
339 + var body struct {
340 + Entity string `json:"entity"`
341 + EntityID string `json:"entity_id"`
342 + }
343 + if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
344 + w.WriteHeader(http.StatusBadRequest)
345 + return
346 + }
347 +
348 + // Route metrics by entity type, validating entity_id to prevent wrong attribution
349 + switch {
350 + case body.Entity == "performance_metrics_by_appliance" && body.EntityID == "A1":
351 + writeTestData(w, "testdata/metrics_perf_appliance.json")
352 + case body.Entity == "performance_metrics_by_volume" && (body.EntityID == "V1" || body.EntityID == "V2"):
353 + writeTestData(w, "testdata/metrics_perf_volume.json")
354 + case body.Entity == "performance_metrics_by_node" && body.EntityID == "N1":
355 + writeTestData(w, "testdata/metrics_perf_node.json")
356 + case body.Entity == "performance_metrics_by_fe_fc_port" && body.EntityID == "FC1":
357 + writeTestData(w, "testdata/metrics_perf_fc_port.json")
358 + case body.Entity == "performance_metrics_by_fe_eth_port" && body.EntityID == "ETH1":
359 + writeTestData(w, "testdata/metrics_perf_eth_port.json")
360 + case body.Entity == "performance_metrics_by_file_system" && body.EntityID == "FS1":
361 + writeTestData(w, "testdata/metrics_perf_filesystem.json")
362 + case body.Entity == "space_metrics_by_cluster":
363 + writeTestData(w, "testdata/metrics_space_cluster.json")
364 + case body.Entity == "space_metrics_by_appliance" && body.EntityID == "A1":
365 + writeTestData(w, "testdata/metrics_space_appliance.json")
366 + case body.Entity == "space_metrics_by_volume" && (body.EntityID == "V1" || body.EntityID == "V2"):
367 + writeTestData(w, "testdata/metrics_space_volume.json")
368 + case body.Entity == "wear_metrics_by_drive" && body.EntityID == "D1":
369 + writeTestData(w, "testdata/metrics_wear_drive.json")
370 + case body.Entity == "copy_metrics_by_appliance" && body.EntityID == "A1":
371 + writeTestData(w, "testdata/metrics_copy_appliance.json")
372 + default:
373 + _, _ = w.Write([]byte(`[]`))
374 + }
375 +}
376 +
377 +func writeTestData(w http.ResponseWriter, path string) {
378 + data, err := os.ReadFile(path)
379 + if err != nil {
380 + w.WriteHeader(http.StatusInternalServerError)
381 + return
382 + }
383 + _, _ = w.Write(data)
384 +}
src/go/plugin/go.d/collector/powerstore/config_schema.json new
+189
@@ -0,0 +1,189 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "Dell PowerStore 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 PowerStore management interface.",
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 PowerStore API authentication.",
43 + "type": "string",
44 + "sensitive": true
45 + },
46 + "password": {
47 + "title": "Password",
48 + "description": "The password for PowerStore API authentication.",
49 + "type": "string",
50 + "sensitive": true
51 + },
52 + "proxy_url": {
53 + "title": "Proxy URL",
54 + "description": "The URL of the proxy server.",
55 + "type": "string"
56 + },
57 + "proxy_username": {
58 + "title": "Proxy username",
59 + "description": "The username for proxy authentication.",
60 + "type": "string",
61 + "sensitive": true
62 + },
63 + "proxy_password": {
64 + "title": "Proxy password",
65 + "description": "The password for proxy authentication.",
66 + "type": "string",
67 + "sensitive": true
68 + },
69 + "headers": {
70 + "title": "Headers",
71 + "description": "Additional HTTP headers to include in requests.",
72 + "type": [
73 + "object",
74 + "null"
75 + ],
76 + "additionalProperties": {
77 + "type": "string"
78 + }
79 + },
80 + "tls_skip_verify": {
81 + "title": "Skip TLS verification",
82 + "description": "If set, TLS certificate verification will be skipped.",
83 + "type": "boolean"
84 + },
85 + "tls_ca": {
86 + "title": "TLS CA",
87 + "description": "The path to the CA certificate file for TLS verification.",
88 + "type": "string",
89 + "pattern": "^$|^/"
90 + },
91 + "tls_cert": {
92 + "title": "TLS certificate",
93 + "description": "The path to the client TLS certificate file.",
94 + "type": "string",
95 + "pattern": "^$|^/"
96 + },
97 + "tls_key": {
98 + "title": "TLS key",
99 + "description": "The path to the client TLS key file.",
100 + "type": "string",
101 + "pattern": "^$|^/"
102 + },
103 + "volume_selector": {
104 + "title": "Volume selector",
105 + "description": "Specifies a space-separated list of volume name patterns to monitor. Use * for wildcards, prefix with ! to exclude.",
106 + "type": "string",
107 + "default": "*"
108 + },
109 + "vnode": {
110 + "title": "Virtual Node",
111 + "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).",
112 + "type": "string"
113 + }
114 + },
115 + "required": [
116 + "url",
117 + "username",
118 + "password"
119 + ],
120 + "additionalProperties": false,
121 + "patternProperties": {
122 + "^name$": {}
123 + }
124 + },
125 + "uiSchema": {
126 + "uiOptions": {
127 + "fullPage": true
128 + },
129 + "url": {
130 + "ui:placeholder": "https://127.0.0.1"
131 + },
132 + "timeout": {
133 + "ui:help": "Accepts decimals for sub-second values (e.g., 0.5 = 500ms)."
134 + },
135 + "password": {
136 + "ui:widget": "password"
137 + },
138 + "proxy_password": {
139 + "ui:widget": "password"
140 + },
141 + "ui:flavour": "tabs",
142 + "ui:options": {
143 + "tabs": [
144 + {
145 + "title": "Base",
146 + "fields": [
147 + "update_every",
148 + "autodetection_retry",
149 + "url",
150 + "timeout",
151 + "not_follow_redirects",
152 + "volume_selector",
153 + "vnode"
154 + ]
155 + },
156 + {
157 + "title": "Auth",
158 + "fields": [
159 + "username",
160 + "password"
161 + ]
162 + },
163 + {
164 + "title": "TLS",
165 + "fields": [
166 + "tls_skip_verify",
167 + "tls_ca",
168 + "tls_cert",
169 + "tls_key"
170 + ]
171 + },
172 + {
173 + "title": "Proxy",
174 + "fields": [
175 + "proxy_url",
176 + "proxy_username",
177 + "proxy_password"
178 + ]
179 + },
180 + {
181 + "title": "Headers",
182 + "fields": [
183 + "headers"
184 + ]
185 + }
186 + ]
187 + }
188 + }
189 +}
src/go/plugin/go.d/collector/powerstore/metadata.yaml new
+630
@@ -0,0 +1,630 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - meta:
4 + id: collector-go.d.plugin-powerstore
5 + plugin_name: go.d.plugin
6 + module_name: powerstore
7 + monitored_instance:
8 + name: Dell PowerStore
9 + link: https://www.dell.com/en-us/dt/storage/powerstore-storage-appliance.htm
10 + icon_filename: dell.svg
11 + categories:
12 + - data-collection.storage
13 + keywords:
14 + - powerstore
15 + - dell
16 + - storage
17 + - san
18 + - nas
19 + - block
20 + - file
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 PowerStore storage arrays via the PowerStore REST API.
30 +
31 + It collects metrics for the following components:
32 +
33 + - **Cluster Capacity** — physical and logical space usage, efficiency ratios (data reduction, snapshot savings, thin provisioning)
34 + - **Appliance Performance** — IOPS, bandwidth, latency, CPU utilization, and space usage per appliance
35 + - **Volume Performance** — IOPS, bandwidth, latency, provisioned/used space, and thin savings per volume
36 + - **Node Performance** — IOPS, bandwidth, latency, and current login count per node
37 + - **FC Port Performance** — IOPS, bandwidth, latency, and link status per Fibre Channel port
38 + - **Ethernet Port Performance** — bytes/packets rate, error rates, and link status per Ethernet port
39 + - **File System Performance** — IOPS, bandwidth, and latency per file system
40 + - **Hardware Health** — component-level status counts (fans, PSUs, drives, batteries, nodes) by health state (OK, degraded, failed, unknown)
41 + - **Active Alerts** — count of currently active alerts by severity (critical, major, minor, info)
42 + - **Drive Wear** — SSD endurance remaining percentage per drive
43 + - **NAS Server Status** — operational status counts (started, stopped, degraded, unknown)
44 + - **Replication** — data remaining, data transferred, and transfer rate across appliances
45 + method_description: |
46 + The collector uses the PowerStore REST API (`/api/rest/*` endpoints) to fetch data.
47 + Authentication uses HTTP Basic Auth — the collector calls `/api/rest/login_session` to obtain
48 + a session cookie and a `DELL-EMC-TOKEN` CSRF token, which are reused for subsequent requests.
49 + Sessions are automatically refreshed on HTTP 403 (token expiry).
50 +
51 + Discovery runs on startup and then every 5 collection cycles to refresh the hardware inventory,
52 + appliances, volumes, nodes, ports, file systems, and NAS servers.
53 +
54 + Performance and space metrics are collected via the `/api/rest/metrics/generate` endpoint using
55 + the PowerStore metrics generation API. The collector requests `Five_Mins` interval data for
56 + performance metrics and `One_Day` for cluster space metrics, taking the most recent data point.
57 +
58 + Discovery API calls use pagination (2000 items per page, HTTP 206 for partial results).
59 + Performance metrics for different entities are collected concurrently (up to 10 parallel API calls).
60 + supported_platforms:
61 + include: []
62 + exclude: []
63 + multi_instance: true
64 + additional_permissions:
65 + description: |
66 + A user account on the PowerStore array is required. Any role with API read access
67 + is sufficient (e.g., the built-in `Operator` role). The account does not need write permissions.
68 + default_behavior:
69 + auto_detection:
70 + description: |
71 + The collector does not auto-detect PowerStore arrays. You must configure the management IP address,
72 + username, and password explicitly.
73 + limits:
74 + description: |
75 + The collector makes multiple API calls per collection cycle: discovery calls (once every 5 cycles)
76 + plus per-entity metrics calls (appliances, volumes, nodes, ports, file systems, drives, and
77 + replication — one API call per entity). With many volumes or ports, the number of API calls
78 + scales linearly. The default 30-second collection interval provides ample headroom.
79 + performance_impact:
80 + description: |
81 + The PowerStore REST API is lightweight and read-only. The collector uses the metrics generation
82 + API which returns pre-aggregated data, imposing minimal load on the array. Session tokens and
83 + CSRF tokens are cached across collection cycles.
84 + setup:
85 + prerequisites:
86 + list:
87 + - title: PowerStore API access
88 + description: |
89 + 1. A user account on the PowerStore array with at least read-only access.
90 + The built-in `Operator` role is sufficient.
91 + 2. Network connectivity from the Netdata Agent to the PowerStore management IP (HTTPS, port 443).
92 + 3. The PowerStore management REST API must be accessible (enabled by default).
93 + - title: TLS certificate
94 + description: |
95 + PowerStore arrays typically use self-signed TLS certificates. You will likely need to set
96 + `tls_skip_verify: yes` in the configuration, or provide the array's CA certificate via `tls_ca`.
97 + configuration:
98 + file:
99 + name: go.d/powerstore.conf
100 + options:
101 + description: |
102 + The following options can be defined globally: update_every, autodetection_retry.
103 + folding:
104 + title: Config options
105 + enabled: true
106 + list:
107 + - name: update_every
108 + description: Data collection interval (seconds).
109 + default_value: 30
110 + required: false
111 + group: Collection
112 + - name: autodetection_retry
113 + description: Autodetection retry interval (seconds). Set 0 to disable.
114 + default_value: 0
115 + required: false
116 + group: Collection
117 +
118 + - name: url
119 + description: PowerStore management URL.
120 + default_value: https://127.0.0.1
121 + required: true
122 + group: Target
123 + - name: timeout
124 + description: HTTP request timeout (seconds).
125 + default_value: 30
126 + required: false
127 + group: Target
128 +
129 + - name: username
130 + description: Username for PowerStore API authentication.
131 + default_value: ""
132 + required: true
133 + group: HTTP Auth
134 + - name: password
135 + description: Password for PowerStore API authentication.
136 + default_value: ""
137 + required: true
138 + group: HTTP Auth
139 +
140 + - name: volume_selector
141 + description: |
142 + Space-separated [simple patterns](https://github.com/netdata/netdata/tree/master/src/libnetdata/simple_pattern#readme)
143 + to filter which volumes to monitor. Use `*` for wildcards, `!` prefix to exclude.
144 + An empty value or `*` monitors all volumes.
145 + default_value: "*"
146 + required: false
147 + group: Filtering
148 +
149 + - name: tls_skip_verify
150 + description: Skip TLS certificate and hostname verification (insecure, but often needed for self-signed certs on storage arrays).
151 + default_value: no
152 + required: false
153 + group: TLS
154 + - name: tls_ca
155 + description: Path to CA bundle used to validate the server certificate.
156 + default_value: ""
157 + required: false
158 + group: TLS
159 + - name: tls_cert
160 + description: Path to client TLS certificate (for mTLS).
161 + default_value: ""
162 + required: false
163 + group: TLS
164 + - name: tls_key
165 + description: Path to client TLS private key (for mTLS).
166 + default_value: ""
167 + required: false
168 + group: TLS
169 +
170 + - name: proxy_url
171 + description: HTTP proxy URL.
172 + default_value: ""
173 + required: false
174 + group: Proxy
175 + - name: proxy_username
176 + description: Username for proxy Basic HTTP authentication.
177 + default_value: ""
178 + required: false
179 + group: Proxy
180 + - name: proxy_password
181 + description: Password for proxy Basic HTTP authentication.
182 + default_value: ""
183 + required: false
184 + group: Proxy
185 +
186 + - name: headers
187 + description: "Additional HTTP headers (one per line as key: value)."
188 + default_value: ""
189 + required: false
190 + group: Request
191 + - name: not_follow_redirects
192 + description: Do not follow HTTP redirects.
193 + default_value: no
194 + required: false
195 + group: Request
196 +
197 + - name: vnode
198 + 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).
199 + default_value: ""
200 + required: false
201 + group: Virtual Node
202 + examples:
203 + folding:
204 + title: Config
205 + enabled: true
206 + list:
207 + - name: Basic
208 + description: A minimal configuration connecting to a PowerStore array with a self-signed certificate.
209 + config: |
210 + jobs:
211 + - name: ps-lab
212 + url: https://10.0.0.1
213 + username: monitor
214 + password: monitor123
215 + tls_skip_verify: yes
216 + - name: With volume filtering
217 + description: Monitor only production volumes, excluding temporary and test volumes.
218 + config: |
219 + jobs:
220 + - name: ps-prod
221 + url: https://10.0.0.1
222 + username: monitor
223 + password: monitor123
224 + tls_skip_verify: yes
225 + volume_selector: "prod-* !*-temp !*-test"
226 + - name: Multi-instance
227 + description: |
228 + > **Note**: When you define multiple jobs, their names must be unique.
229 +
230 + Monitoring multiple PowerStore arrays.
231 + config: |
232 + jobs:
233 + - name: ps-site-a
234 + url: https://10.0.0.1
235 + username: monitor
236 + password: monitor123
237 + tls_skip_verify: yes
238 +
239 + - name: ps-site-b
240 + url: https://10.0.0.2
241 + username: monitor
242 + password: monitor456
243 + tls_skip_verify: yes
244 + troubleshooting:
245 + problems:
246 + list:
247 + - name: Authentication failure
248 + description: |
249 + If the collector fails with `login failed`:
250 +
251 + 1. Verify the username and password are correct.
252 + 2. Ensure the user account has at least `Operator` role privileges.
253 + 3. Check that the account is not locked or expired on the array.
254 + - name: Connection refused or timeout
255 + description: |
256 + If the collector fails with connection errors:
257 +
258 + 1. Verify the `url` points to the management IP of the PowerStore array.
259 + 2. Ensure HTTPS port 443 is reachable from the Netdata Agent host.
260 + 3. If using a firewall, allow outbound HTTPS to the array.
261 + 4. Try increasing `timeout` if the array is on a high-latency link.
262 + - name: TLS certificate errors
263 + description: |
264 + PowerStore arrays ship with self-signed certificates. If you see TLS errors:
265 +
266 + 1. Set `tls_skip_verify: yes` in the configuration (most common solution).
267 + 2. Alternatively, export the array's certificate and provide it via `tls_ca`.
268 + - name: HTTP 403 errors
269 + description: |
270 + PowerStore returns HTTP 403 when the session/CSRF token is stale. The collector
271 + automatically re-authenticates on 403, but if you see persistent 403 errors:
272 +
273 + 1. Verify the user account still has API access.
274 + 2. Check if the array's security policy has been changed to restrict API access.
275 + 3. Ensure no IP-based access controls are blocking the Netdata Agent host.
276 + alerts: []
277 + metrics:
278 + folding:
279 + title: Metrics
280 + enabled: false
281 + description: ""
282 + availability: []
283 + scopes:
284 + - name: global
285 + description: These metrics refer to the entire monitored PowerStore cluster.
286 + labels: []
287 + metrics:
288 + - name: powerstore.cluster_space_usage
289 + description: Cluster Space Usage
290 + unit: bytes
291 + chart_type: line
292 + dimensions:
293 + - name: used
294 + - name: total
295 + - name: powerstore.cluster_space_logical
296 + description: Cluster Logical Space
297 + unit: bytes
298 + chart_type: line
299 + dimensions:
300 + - name: provisioned
301 + - name: used
302 + - name: data_physical
303 + - name: shared
304 + - name: powerstore.cluster_space_efficiency
305 + description: Cluster Space Efficiency Ratios
306 + unit: ratio
307 + chart_type: line
308 + dimensions:
309 + - name: efficiency
310 + - name: data_reduction
311 + - name: snapshot_savings
312 + - name: thin_savings
313 + - name: powerstore.hardware_health_fan
314 + description: Fan Health Status
315 + unit: fans
316 + chart_type: stacked
317 + dimensions:
318 + - name: ok
319 + - name: degraded
320 + - name: failed
321 + - name: unknown
322 + - name: powerstore.hardware_health_psu
323 + description: Power Supply Health Status
324 + unit: PSUs
325 + chart_type: stacked
326 + dimensions:
327 + - name: ok
328 + - name: degraded
329 + - name: failed
330 + - name: unknown
331 + - name: powerstore.hardware_health_drive
332 + description: Drive Health Status
333 + unit: drives
334 + chart_type: stacked
335 + dimensions:
336 + - name: ok
337 + - name: degraded
338 + - name: failed
339 + - name: unknown
340 + - name: powerstore.hardware_health_battery
341 + description: Battery Health Status
342 + unit: batteries
343 + chart_type: stacked
344 + dimensions:
345 + - name: ok
346 + - name: degraded
347 + - name: failed
348 + - name: unknown
349 + - name: powerstore.hardware_health_node
350 + description: Node Health Status
351 + unit: nodes
352 + chart_type: stacked
353 + dimensions:
354 + - name: ok
355 + - name: degraded
356 + - name: failed
357 + - name: unknown
358 + - name: powerstore.alerts_active
359 + description: Active Alerts by Severity
360 + unit: alerts
361 + chart_type: stacked
362 + dimensions:
363 + - name: critical
364 + - name: major
365 + - name: minor
366 + - name: info
367 + - name: powerstore.nas_server_status
368 + description: NAS Server Status
369 + unit: servers
370 + chart_type: stacked
371 + dimensions:
372 + - name: started
373 + - name: stopped
374 + - name: degraded
375 + - name: unknown
376 + - name: powerstore.copy_data
377 + description: Replication Data
378 + unit: bytes
379 + chart_type: line
380 + dimensions:
381 + - name: remaining
382 + - name: transferred
383 + - name: powerstore.copy_transfer_rate
384 + description: Replication Transfer Rate
385 + unit: bytes/s
386 + chart_type: line
387 + dimensions:
388 + - name: rate
389 + - name: appliance
390 + description: These metrics refer to individual PowerStore appliances.
391 + labels:
392 + - name: appliance
393 + description: Appliance name (e.g., `Appliance-WX-D8031`).
394 + metrics:
395 + - name: powerstore.appliance_iops
396 + description: Appliance IOPS
397 + unit: ops/s
398 + chart_type: area
399 + dimensions:
400 + - name: read
401 + - name: write
402 + - name: powerstore.appliance_bandwidth
403 + description: Appliance Bandwidth
404 + unit: bytes/s
405 + chart_type: area
406 + dimensions:
407 + - name: read
408 + - name: write
409 + - name: powerstore.appliance_latency
410 + description: Appliance Latency
411 + unit: microseconds
412 + chart_type: line
413 + dimensions:
414 + - name: read
415 + - name: write
416 + - name: avg
417 + - name: powerstore.appliance_cpu
418 + description: Appliance IO Workload CPU Utilization
419 + unit: percentage
420 + chart_type: line
421 + dimensions:
422 + - name: utilization
423 + - name: powerstore.appliance_space
424 + description: Appliance Space Usage
425 + unit: bytes
426 + chart_type: line
427 + dimensions:
428 + - name: used
429 + - name: total
430 + - name: powerstore.appliance_space_logical
431 + description: Appliance Logical Space
432 + unit: bytes
433 + chart_type: line
434 + dimensions:
435 + - name: provisioned
436 + - name: used
437 + - name: data_physical
438 + - name: shared
439 + - name: powerstore.appliance_space_efficiency
440 + description: Appliance Space Efficiency Ratios
441 + unit: ratio
442 + chart_type: line
443 + dimensions:
444 + - name: efficiency
445 + - name: data_reduction
446 + - name: snapshot_savings
447 + - name: thin_savings
448 + - name: volume
449 + description: These metrics refer to individual PowerStore volumes. Volumes can be filtered using the `volume_selector` configuration option.
450 + labels:
451 + - name: volume
452 + description: Volume name (e.g., `prod-db-01`).
453 + metrics:
454 + - name: powerstore.volume_iops
455 + description: Volume IOPS
456 + unit: ops/s
457 + chart_type: area
458 + dimensions:
459 + - name: read
460 + - name: write
461 + - name: powerstore.volume_bandwidth
462 + description: Volume Bandwidth
463 + unit: bytes/s
464 + chart_type: area
465 + dimensions:
466 + - name: read
467 + - name: write
468 + - name: powerstore.volume_latency
469 + description: Volume Latency
470 + unit: microseconds
471 + chart_type: line
472 + dimensions:
473 + - name: read
474 + - name: write
475 + - name: avg
476 + - name: powerstore.volume_space
477 + description: Volume Space Usage
478 + unit: bytes
479 + chart_type: line
480 + dimensions:
481 + - name: provisioned
482 + - name: used
483 + - name: powerstore.volume_space_savings
484 + description: Volume Thin Savings
485 + unit: ratio
486 + chart_type: line
487 + dimensions:
488 + - name: thin_savings
489 + - name: node
490 + description: These metrics refer to individual PowerStore nodes within an appliance.
491 + labels:
492 + - name: node
493 + description: Node name (e.g., `Appliance-WX-D8031-node-A`).
494 + metrics:
495 + - name: powerstore.node_iops
496 + description: Node IOPS
497 + unit: ops/s
498 + chart_type: area
499 + dimensions:
500 + - name: read
501 + - name: write
502 + - name: powerstore.node_bandwidth
503 + description: Node Bandwidth
504 + unit: bytes/s
505 + chart_type: area
506 + dimensions:
507 + - name: read
508 + - name: write
509 + - name: powerstore.node_latency
510 + description: Node Latency
511 + unit: microseconds
512 + chart_type: line
513 + dimensions:
514 + - name: read
515 + - name: write
516 + - name: avg
517 + - name: powerstore.node_logins
518 + description: Node Current Logins
519 + unit: logins
520 + chart_type: line
521 + dimensions:
522 + - name: logins
523 + - name: fc port
524 + description: These metrics refer to individual Fibre Channel ports used for host connectivity.
525 + labels:
526 + - name: fc_port
527 + description: FC port name (e.g., `Appliance-WX-D8031-node-A-Port-0`).
528 + metrics:
529 + - name: powerstore.fc_port_iops
530 + description: FC Port IOPS
531 + unit: ops/s
532 + chart_type: area
533 + dimensions:
534 + - name: read
535 + - name: write
536 + - name: powerstore.fc_port_bandwidth
537 + description: FC Port Bandwidth
538 + unit: bytes/s
539 + chart_type: area
540 + dimensions:
541 + - name: read
542 + - name: write
543 + - name: powerstore.fc_port_latency
544 + description: FC Port Latency
545 + unit: microseconds
546 + chart_type: line
547 + dimensions:
548 + - name: read
549 + - name: write
550 + - name: avg
551 + - name: powerstore.fc_port_link_status
552 + description: FC Port Link Status
553 + unit: status
554 + chart_type: line
555 + dimensions:
556 + - name: up
557 + - name: ethernet port
558 + description: These metrics refer to individual Ethernet ports (management and iSCSI connectivity).
559 + labels:
560 + - name: eth_port
561 + description: Ethernet port name (e.g., `Appliance-WX-D8031-node-A-Port-eth0`).
562 + metrics:
563 + - name: powerstore.eth_port_bytes
564 + description: Ethernet Port Bytes Rate
565 + unit: bytes/s
566 + chart_type: area
567 + dimensions:
568 + - name: received
569 + - name: sent
570 + - name: powerstore.eth_port_packets
571 + description: Ethernet Port Packets Rate
572 + unit: packets/s
573 + chart_type: area
574 + dimensions:
575 + - name: received
576 + - name: sent
577 + - name: powerstore.eth_port_errors
578 + description: Ethernet Port Errors Rate
579 + unit: errors/s
580 + chart_type: stacked
581 + dimensions:
582 + - name: rx_crc
583 + - name: rx_no_buffer
584 + - name: tx_error
585 + - name: powerstore.eth_port_link_status
586 + description: Ethernet Port Link Status
587 + unit: status
588 + chart_type: line
589 + dimensions:
590 + - name: up
591 + - name: file system
592 + description: These metrics refer to individual file systems (NFS/SMB shares).
593 + labels:
594 + - name: filesystem
595 + description: File system name (e.g., `nfs-share-01`).
596 + metrics:
597 + - name: powerstore.filesystem_iops
598 + description: File System IOPS
599 + unit: ops/s
600 + chart_type: area
601 + dimensions:
602 + - name: read
603 + - name: write
604 + - name: powerstore.filesystem_bandwidth
605 + description: File System Bandwidth
606 + unit: bytes/s
607 + chart_type: area
608 + dimensions:
609 + - name: read
610 + - name: write
611 + - name: powerstore.filesystem_latency
612 + description: File System Latency
613 + unit: microseconds
614 + chart_type: line
615 + dimensions:
616 + - name: read
617 + - name: write
618 + - name: avg
619 + - name: drive
620 + description: These metrics refer to individual physical drives (SSDs).
621 + labels:
622 + - name: drive
623 + description: Drive name (e.g., `Drive_0_0_25`).
624 + metrics:
625 + - name: powerstore.drive_endurance
626 + description: Drive Endurance Remaining
627 + unit: percentage
628 + chart_type: line
629 + dimensions:
630 + - name: remaining
src/go/plugin/go.d/collector/powerstore/metrics.go new
+244
@@ -0,0 +1,244 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package powerstore
4 +
5 +import "github.com/netdata/netdata/go/plugins/pkg/metrix"
6 +
7 +type collectorMetrics struct {
8 + cluster clusterGauges
9 + appliance applianceGauges
10 + volume volumeGauges
11 + node nodeGauges
12 + fcPort fcPortGauges
13 + ethPort ethPortGauges
14 + fileSystem fileSystemGauges
15 + hardware hardwareGauges
16 + alerts alertGauges
17 + drive driveGauges
18 + nas nasGauges
19 + replication replicationGauges
20 +}
21 +
22 +type clusterGauges struct {
23 + spacePhysicalTotal metrix.SnapshotGauge
24 + spacePhysicalUsed metrix.SnapshotGauge
25 + spaceLogicalProvisioned metrix.SnapshotGauge
26 + spaceLogicalUsed metrix.SnapshotGauge
27 + spaceDataPhysicalUsed metrix.SnapshotGauge
28 + spaceSharedLogicalUsed metrix.SnapshotGauge
29 + spaceEfficiencyRatio metrix.SnapshotGauge
30 + spaceDataReduction metrix.SnapshotGauge
31 + spaceSnapshotSavings metrix.SnapshotGauge
32 + spaceThinSavings metrix.SnapshotGauge
33 +}
34 +
35 +type perfGaugeVecs struct {
36 + readIops metrix.SnapshotGaugeVec
37 + writeIops metrix.SnapshotGaugeVec
38 + readBandwidth metrix.SnapshotGaugeVec
39 + writeBandwidth metrix.SnapshotGaugeVec
40 + avgReadLatency metrix.SnapshotGaugeVec
41 + avgWriteLatency metrix.SnapshotGaugeVec
42 + avgLatency metrix.SnapshotGaugeVec
43 +}
44 +
45 +type spaceGaugeVecs struct {
46 + physicalTotal metrix.SnapshotGaugeVec
47 + physicalUsed metrix.SnapshotGaugeVec
48 + logicalProvisioned metrix.SnapshotGaugeVec
49 + logicalUsed metrix.SnapshotGaugeVec
50 + dataPhysicalUsed metrix.SnapshotGaugeVec
51 + sharedLogicalUsed metrix.SnapshotGaugeVec
52 + efficiencyRatio metrix.SnapshotGaugeVec
53 + dataReduction metrix.SnapshotGaugeVec
54 + snapshotSavings metrix.SnapshotGaugeVec
55 + thinSavings metrix.SnapshotGaugeVec
56 +}
57 +
58 +type applianceGauges struct {
59 + perf perfGaugeVecs
60 + space spaceGaugeVecs
61 + cpu metrix.SnapshotGaugeVec
62 +}
63 +
64 +type volumeGauges struct {
65 + perf perfGaugeVecs
66 + spaceLogicalProv metrix.SnapshotGaugeVec
67 + spaceLogicalUsed metrix.SnapshotGaugeVec
68 + spaceThinSavings metrix.SnapshotGaugeVec
69 +}
70 +
71 +type nodeGauges struct {
72 + perf perfGaugeVecs
73 + currentLogins metrix.SnapshotGaugeVec
74 +}
75 +
76 +type fcPortGauges struct {
77 + perf perfGaugeVecs
78 + linkUp metrix.SnapshotGaugeVec
79 +}
80 +
81 +type ethPortGauges struct {
82 + bytesRxPs metrix.SnapshotGaugeVec
83 + bytesTxPs metrix.SnapshotGaugeVec
84 + pktRxPs metrix.SnapshotGaugeVec
85 + pktTxPs metrix.SnapshotGaugeVec
86 + pktRxCrcErrorPs metrix.SnapshotGaugeVec
87 + pktRxNoBufferErrorPs metrix.SnapshotGaugeVec
88 + pktTxErrorPs metrix.SnapshotGaugeVec
89 + linkUp metrix.SnapshotGaugeVec
90 +}
91 +
92 +type fileSystemGauges struct {
93 + perf perfGaugeVecs
94 +}
95 +
96 +type hardwareGauges struct {
97 + fanOK, fanDegraded, fanFailed, fanUnknown metrix.SnapshotGauge
98 + psuOK, psuDegraded, psuFailed, psuUnknown metrix.SnapshotGauge
99 + driveOK, driveDegraded, driveFailed, driveUnknown metrix.SnapshotGauge
100 + batteryOK, batteryDegraded, batteryFailed, batteryUnknown metrix.SnapshotGauge
101 + nodeOK, nodeDegraded, nodeFailed, nodeUnknown metrix.SnapshotGauge
102 +}
103 +
104 +type alertGauges struct {
105 + critical metrix.SnapshotGauge
106 + major metrix.SnapshotGauge
107 + minor metrix.SnapshotGauge
108 + info metrix.SnapshotGauge
109 +}
110 +
111 +type driveGauges struct {
112 + enduranceRemaining metrix.SnapshotGaugeVec
113 +}
114 +
115 +type nasGauges struct {
116 + started metrix.SnapshotGauge
117 + stopped metrix.SnapshotGauge
118 + degraded metrix.SnapshotGauge
119 + unknown metrix.SnapshotGauge
120 +}
121 +
122 +type replicationGauges struct {
123 + dataRemaining metrix.SnapshotGauge
124 + dataTransferred metrix.SnapshotGauge
125 + transferRate metrix.SnapshotGauge
126 +}
127 +
128 +func newCollectorMetrics(store metrix.CollectorStore) *collectorMetrics {
129 + meter := store.Write().SnapshotMeter("")
130 +
131 + appVec := meter.Vec("appliance")
132 + volVec := meter.Vec("volume")
133 + nodeVec := meter.Vec("node")
134 + fcVec := meter.Vec("fc_port")
135 + ethVec := meter.Vec("eth_port")
136 + fsVec := meter.Vec("filesystem")
137 + driveVec := meter.Vec("drive")
138 +
139 + newPerfVecs := func(vec metrix.SnapshotVecMeter, prefix string) perfGaugeVecs {
140 + return perfGaugeVecs{
141 + readIops: vec.Gauge(prefix + "_perf_read_iops"),
142 + writeIops: vec.Gauge(prefix + "_perf_write_iops"),
143 + readBandwidth: vec.Gauge(prefix + "_perf_read_bandwidth"),
144 + writeBandwidth: vec.Gauge(prefix + "_perf_write_bandwidth"),
145 + avgReadLatency: vec.Gauge(prefix + "_perf_avg_read_latency"),
146 + avgWriteLatency: vec.Gauge(prefix + "_perf_avg_write_latency"),
147 + avgLatency: vec.Gauge(prefix + "_perf_avg_latency"),
148 + }
149 + }
150 +
151 + newSpaceVecs := func(vec metrix.SnapshotVecMeter, prefix string) spaceGaugeVecs {
152 + return spaceGaugeVecs{
153 + physicalTotal: vec.Gauge(prefix + "_space_physical_total"),
154 + physicalUsed: vec.Gauge(prefix + "_space_physical_used"),
155 + logicalProvisioned: vec.Gauge(prefix + "_space_logical_provisioned"),
156 + logicalUsed: vec.Gauge(prefix + "_space_logical_used"),
157 + dataPhysicalUsed: vec.Gauge(prefix + "_space_data_physical_used"),
158 + sharedLogicalUsed: vec.Gauge(prefix + "_space_shared_logical_used"),
159 + efficiencyRatio: vec.Gauge(prefix + "_space_efficiency_ratio"),
160 + dataReduction: vec.Gauge(prefix + "_space_data_reduction"),
161 + snapshotSavings: vec.Gauge(prefix + "_space_snapshot_savings"),
162 + thinSavings: vec.Gauge(prefix + "_space_thin_savings"),
163 + }
164 + }
165 +
166 + return &collectorMetrics{
167 + cluster: clusterGauges{
168 + spacePhysicalTotal: meter.Gauge("cluster_space_physical_total"),
169 + spacePhysicalUsed: meter.Gauge("cluster_space_physical_used"),
170 + spaceLogicalProvisioned: meter.Gauge("cluster_space_logical_provisioned"),
171 + spaceLogicalUsed: meter.Gauge("cluster_space_logical_used"),
172 + spaceDataPhysicalUsed: meter.Gauge("cluster_space_data_physical_used"),
173 + spaceSharedLogicalUsed: meter.Gauge("cluster_space_shared_logical_used"),
174 + spaceEfficiencyRatio: meter.Gauge("cluster_space_efficiency_ratio"),
175 + spaceDataReduction: meter.Gauge("cluster_space_data_reduction"),
176 + spaceSnapshotSavings: meter.Gauge("cluster_space_snapshot_savings"),
177 + spaceThinSavings: meter.Gauge("cluster_space_thin_savings"),
178 + },
179 + appliance: applianceGauges{
180 + perf: newPerfVecs(appVec, "appliance"),
181 + space: newSpaceVecs(appVec, "appliance"),
182 + cpu: appVec.Gauge("appliance_cpu_utilization"),
183 + },
184 + volume: volumeGauges{
185 + perf: newPerfVecs(volVec, "volume"),
186 + spaceLogicalProv: volVec.Gauge("volume_space_logical_provisioned"),
187 + spaceLogicalUsed: volVec.Gauge("volume_space_logical_used"),
188 + spaceThinSavings: volVec.Gauge("volume_space_thin_savings"),
189 + },
190 + node: nodeGauges{
191 + perf: newPerfVecs(nodeVec, "node"),
192 + currentLogins: nodeVec.Gauge("node_current_logins"),
193 + },
194 + fcPort: fcPortGauges{
195 + perf: newPerfVecs(fcVec, "fc_port"),
196 + linkUp: fcVec.Gauge("fc_port_link_up"),
197 + },
198 + ethPort: ethPortGauges{
199 + bytesRxPs: ethVec.Gauge("eth_port_bytes_rx_ps"),
200 + bytesTxPs: ethVec.Gauge("eth_port_bytes_tx_ps"),
201 + pktRxPs: ethVec.Gauge("eth_port_pkt_rx_ps"),
202 + pktTxPs: ethVec.Gauge("eth_port_pkt_tx_ps"),
203 + pktRxCrcErrorPs: ethVec.Gauge("eth_port_pkt_rx_crc_error_ps"),
204 + pktRxNoBufferErrorPs: ethVec.Gauge("eth_port_pkt_rx_no_buffer_error_ps"),
205 + pktTxErrorPs: ethVec.Gauge("eth_port_pkt_tx_error_ps"),
206 + linkUp: ethVec.Gauge("eth_port_link_up"),
207 + },
208 + fileSystem: fileSystemGauges{
209 + perf: newPerfVecs(fsVec, "file_system"),
210 + },
211 + hardware: hardwareGauges{
212 + fanOK: meter.Gauge("hardware_fan_ok"), fanDegraded: meter.Gauge("hardware_fan_degraded"),
213 + fanFailed: meter.Gauge("hardware_fan_failed"), fanUnknown: meter.Gauge("hardware_fan_unknown"),
214 + psuOK: meter.Gauge("hardware_psu_ok"), psuDegraded: meter.Gauge("hardware_psu_degraded"),
215 + psuFailed: meter.Gauge("hardware_psu_failed"), psuUnknown: meter.Gauge("hardware_psu_unknown"),
216 + driveOK: meter.Gauge("hardware_drive_ok"), driveDegraded: meter.Gauge("hardware_drive_degraded"),
217 + driveFailed: meter.Gauge("hardware_drive_failed"), driveUnknown: meter.Gauge("hardware_drive_unknown"),
218 + batteryOK: meter.Gauge("hardware_battery_ok"), batteryDegraded: meter.Gauge("hardware_battery_degraded"),
219 + batteryFailed: meter.Gauge("hardware_battery_failed"), batteryUnknown: meter.Gauge("hardware_battery_unknown"),
220 + nodeOK: meter.Gauge("hardware_node_ok"), nodeDegraded: meter.Gauge("hardware_node_degraded"),
221 + nodeFailed: meter.Gauge("hardware_node_failed"), nodeUnknown: meter.Gauge("hardware_node_unknown"),
222 + },
223 + alerts: alertGauges{
224 + critical: meter.Gauge("alerts_critical"),
225 + major: meter.Gauge("alerts_major"),
226 + minor: meter.Gauge("alerts_minor"),
227 + info: meter.Gauge("alerts_info"),
228 + },
229 + drive: driveGauges{
230 + enduranceRemaining: driveVec.Gauge("drive_endurance_remaining"),
231 + },
232 + nas: nasGauges{
233 + started: meter.Gauge("nas_started"),
234 + stopped: meter.Gauge("nas_stopped"),
235 + degraded: meter.Gauge("nas_degraded"),
236 + unknown: meter.Gauge("nas_unknown"),
237 + },
238 + replication: replicationGauges{
239 + dataRemaining: meter.Gauge("copy_data_remaining"),
240 + dataTransferred: meter.Gauge("copy_data_transferred"),
241 + transferRate: meter.Gauge("copy_transfer_rate"),
242 + },
243 + }
244 +}
src/go/plugin/go.d/collector/powerstore/testdata/alert.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"AL1","severity":"Minor","state":"ACTIVE","resource_type":"hardware","resource_name":"Drive-1","description_l10n":"Drive temperature above threshold","generated_timestamp":"2024-01-01T00:00:00Z","is_acknowledged":false},{"id":"AL2","severity":"Info","state":"ACTIVE","resource_type":"software_system","resource_name":"System","description_l10n":"Software update available","generated_timestamp":"2024-01-01T00:00:00Z","is_acknowledged":false}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/appliance.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"A1","name":"Appliance-1","service_tag":"SVC001"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/cluster.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"0","global_id":"PS00000000001","name":"PowerStore-Cluster","management_address":"10.0.0.1","state":"Configured"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/eth_port.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"ETH1","name":"ETH-Port-0","appliance_id":"A1","node_id":"N1","is_link_up":true,"current_speed":"25_Gbps","mac_address":"00:11:22:33:44:55","current_mtu":9000}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/fc_port.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"FC1","name":"FC-Port-0","appliance_id":"A1","node_id":"N1","is_link_up":true,"current_speed":"32_Gbps","wwn":"50:00:00:00:00:00:00:01"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/file_system.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"FS1","name":"nfs-share-01","nas_server_id":"NAS1","size_total":1099511627776,"size_used":549755813888,"filesystem_type":"Primary"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/hardware_all.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"F1","name":"Fan-1","type":"Fan","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"F2","name":"Fan-2","type":"Fan","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"F3","name":"Fan-3","type":"Fan","lifecycle_state":"Degraded","appliance_id":"A1"},{"id":"P1","name":"PSU-1","type":"Power_Supply","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"P2","name":"PSU-2","type":"Power_Supply","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"D1","name":"Drive-1","type":"Drive","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"D2","name":"Drive-2","type":"Drive","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"B1","name":"Battery-1","type":"Battery","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"N1","name":"Node-A","type":"Node","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"N2","name":"Node-B","type":"Node","lifecycle_state":"Healthy","appliance_id":"A1"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/hardware_battery.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"B1","name":"Battery-1","type":"Battery","lifecycle_state":"Healthy","appliance_id":"A1"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/hardware_drive.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"D1","name":"Drive-1","type":"Drive","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"D2","name":"Drive-2","type":"Drive","lifecycle_state":"Healthy","appliance_id":"A1"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/hardware_fan.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"F1","name":"Fan-1","type":"Fan","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"F2","name":"Fan-2","type":"Fan","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"F3","name":"Fan-3","type":"Fan","lifecycle_state":"Degraded","appliance_id":"A1"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/hardware_node.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"N1","name":"Node-A","type":"Node","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"N2","name":"Node-B","type":"Node","lifecycle_state":"Healthy","appliance_id":"A1"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/hardware_psu.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"P1","name":"PSU-1","type":"Power_Supply","lifecycle_state":"Healthy","appliance_id":"A1"},{"id":"P2","name":"PSU-2","type":"Power_Supply","lifecycle_state":"Healthy","appliance_id":"A1"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_copy_appliance.json new
+1
@@ -0,0 +1 @@
1 +[{"appliance_id":"A1","data_remaining":1073741824,"data_transferred":5368709120,"transfer_rate":52428800.0}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_perf_appliance.json new
+1
@@ -0,0 +1 @@
1 +[{"appliance_id":"A1","read_iops":1500.5,"write_iops":800.3,"total_iops":2300.8,"read_bandwidth":157286400,"write_bandwidth":83886080,"total_bandwidth":241172480,"avg_read_latency":250.5,"avg_write_latency":350.2,"avg_latency":290.1,"io_workload_cpu_utilization":35.5}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_perf_eth_port.json new
+1
@@ -0,0 +1 @@
1 +[{"fe_port_id":"ETH1","appliance_id":"A1","node_id":"N1","bytes_rx_ps":125000000.0,"bytes_tx_ps":62500000.0,"pkt_rx_ps":100000.0,"pkt_tx_ps":50000.0,"pkt_rx_crc_error_ps":0.5,"pkt_rx_no_buffer_error_ps":0.0,"pkt_tx_error_ps":0.0}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_perf_fc_port.json new
+1
@@ -0,0 +1 @@
1 +[{"fe_port_id":"FC1","appliance_id":"A1","node_id":"N1","read_iops":300.0,"write_iops":200.0,"total_iops":500.0,"read_bandwidth":31457280,"write_bandwidth":20971520,"total_bandwidth":52428800,"avg_read_latency":180.0,"avg_write_latency":250.0,"avg_latency":210.0}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_perf_filesystem.json new
+1
@@ -0,0 +1 @@
1 +[{"file_system_id":"FS1","read_iops":100.0,"write_iops":50.0,"total_iops":150.0,"read_bandwidth":10485760,"write_bandwidth":5242880,"total_bandwidth":15728640,"avg_read_latency":400.0,"avg_write_latency":500.0,"avg_latency":440.0}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_perf_node.json new
+1
@@ -0,0 +1 @@
1 +[{"node_id":"N1","appliance_id":"A1","read_iops":750.0,"write_iops":400.0,"total_iops":1150.0,"read_bandwidth":78643200,"write_bandwidth":41943040,"total_bandwidth":120586240,"avg_read_latency":230.0,"avg_write_latency":320.0,"avg_latency":270.0,"current_logins":5}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_perf_volume.json new
+1
@@ -0,0 +1 @@
1 +[{"volume_id":"V1","appliance_id":"A1","read_iops":500.2,"write_iops":300.1,"total_iops":800.3,"read_bandwidth":52428800,"write_bandwidth":31457280,"total_bandwidth":83886080,"avg_read_latency":200.0,"avg_write_latency":300.0,"avg_latency":240.0}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_space_appliance.json new
+1
@@ -0,0 +1 @@
1 +[{"appliance_id":"A1","physical_total":10995116277760,"physical_used":5497558138880,"logical_provisioned":21990232555520,"logical_used":8246337208320,"data_physical_used":4398046511104,"shared_logical_used":1099511627776,"efficiency_ratio":2.5,"data_reduction":1.8,"snapshot_savings":0.3,"thin_savings":0.7}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_space_cluster.json new
+1
@@ -0,0 +1 @@
1 +[{"cluster_id":"0","physical_total":10995116277760,"physical_used":5497558138880,"logical_provisioned":21990232555520,"logical_used":8246337208320,"data_physical_used":4398046511104,"shared_logical_used":1099511627776,"efficiency_ratio":2.5,"data_reduction":1.8,"snapshot_savings":0.3,"thin_savings":0.7}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_space_volume.json new
+1
@@ -0,0 +1 @@
1 +[{"volume_id":"V1","logical_provisioned":107374182400,"logical_used":53687091200,"thin_savings":0.5}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/metrics_wear_drive.json new
+1
@@ -0,0 +1 @@
1 +[{"drive_id":"D1","percent_endurance_remaining":98.5}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/nas_server.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"NAS1","name":"NAS-Server-1","operational_status":"Started","current_node_id":"N1"}]
\ No newline at end of file
src/go/plugin/go.d/collector/powerstore/testdata/volume.json new
+1
@@ -0,0 +1 @@
1 +[{"id":"V1","name":"prod-vol-01","size":107374182400,"logical_used":53687091200,"state":"Ready","type":"Primary","appliance_id":"A1"},{"id":"V2","name":"test-vol-01","size":53687091200,"logical_used":10737418240,"state":"Ready","type":"Primary","appliance_id":"A1"}]
\ No newline at end of file
src/go/plugin/go.d/config/go.d/powerstore.conf new
+16
@@ -0,0 +1,16 @@
1 +## All available configuration options, their descriptions and default values:
2 +## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/powerstore#readme
3 +
4 +#jobs:
5 +# - name : local
6 +# url : https://10.0.0.1
7 +# username : admin
8 +# password : password
9 +# tls_skip_verify: yes
10 +
11 +# - name : remote
12 +# url : https://10.0.0.2
13 +# username : admin
14 +# password : password
15 +# tls_skip_verify: yes
16 +# volume_selector: "prod-* !*-temp !*-test"