master
go 395 lines 12.6 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package powervault
4
5 import (
6 "context"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "strings"
11 "testing"
12
13 "github.com/netdata/netdata/go/plugins/pkg/metrix"
14 "github.com/netdata/netdata/go/plugins/pkg/web"
15 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
16 "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
17 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
18 "github.com/stretchr/testify/assert"
19 "github.com/stretchr/testify/require"
20 )
21
22 func TestCollector_Init(t *testing.T) {
23 tests := map[string]struct {
24 config Config
25 wantFail bool
26 }{
27 "success with valid config": {
28 config: Config{
29 HTTPConfig: web.HTTPConfig{
30 RequestConfig: web.RequestConfig{
31 URL: "https://127.0.0.1",
32 Username: "admin",
33 Password: "password",
34 },
35 },
36 },
37 },
38 "success with md5 digest": {
39 config: Config{
40 HTTPConfig: web.HTTPConfig{
41 RequestConfig: web.RequestConfig{
42 URL: "https://127.0.0.1",
43 Username: "admin",
44 Password: "password",
45 },
46 },
47 AuthDigest: "md5",
48 },
49 },
50 "fail without username": {
51 wantFail: true,
52 config: Config{
53 HTTPConfig: web.HTTPConfig{
54 RequestConfig: web.RequestConfig{
55 URL: "https://127.0.0.1",
56 Password: "password",
57 },
58 },
59 },
60 },
61 "fail without password": {
62 wantFail: true,
63 config: Config{
64 HTTPConfig: web.HTTPConfig{
65 RequestConfig: web.RequestConfig{
66 URL: "https://127.0.0.1",
67 Username: "admin",
68 },
69 },
70 },
71 },
72 "fail with invalid digest": {
73 wantFail: true,
74 config: Config{
75 HTTPConfig: web.HTTPConfig{
76 RequestConfig: web.RequestConfig{
77 URL: "https://127.0.0.1",
78 Username: "admin",
79 Password: "password",
80 },
81 },
82 AuthDigest: "sha1",
83 },
84 },
85 }
86
87 for name, test := range tests {
88 t.Run(name, func(t *testing.T) {
89 collr := New()
90 collr.Config = test.config
91
92 if test.wantFail {
93 assert.Error(t, collr.Init(context.Background()))
94 } else {
95 assert.NoError(t, collr.Init(context.Background()))
96 }
97 })
98 }
99 }
100
101 func TestCollector_Check(t *testing.T) {
102 srv := newMockPowerVaultServer()
103 defer srv.Close()
104
105 collr := New()
106 collr.Config = Config{
107 HTTPConfig: web.HTTPConfig{
108 RequestConfig: web.RequestConfig{
109 URL: srv.URL,
110 Username: "admin",
111 Password: "password",
112 },
113 },
114 }
115
116 require.NoError(t, collr.Init(context.Background()))
117 assert.NoError(t, collr.Check(context.Background()))
118 }
119
120 func TestCollector_Collect(t *testing.T) {
121 srv := newMockPowerVaultServer()
122 defer srv.Close()
123
124 collr := New()
125 collr.Config = Config{
126 HTTPConfig: web.HTTPConfig{
127 RequestConfig: web.RequestConfig{
128 URL: srv.URL,
129 Username: "admin",
130 Password: "password",
131 },
132 },
133 }
134
135 require.NoError(t, collr.Init(context.Background()))
136 require.NoError(t, collr.Check(context.Background()))
137
138 cc := mustCycleController(t, collr.MetricStore())
139 cc.BeginCycle()
140 require.NoError(t, collr.Collect(context.Background()))
141 cc.CommitCycleSuccess()
142
143 r := collr.MetricStore().Read(metrix.ReadRaw())
144
145 // System health
146 assertValue(t, r, "system_health", nil, 0)
147
148 // Hardware health: 2 controllers OK, 3 drives (2 OK + 1 Global SP→OK), 2 fans OK, 2 PSUs OK
149 assertValue(t, r, "hw_controller_ok", nil, 2)
150 assertValue(t, r, "hw_controller_degraded", nil, 0)
151 assertValue(t, r, "hw_controller_fault", nil, 0)
152 assertValue(t, r, "hw_controller_unknown", nil, 0)
153
154 assertValue(t, r, "hw_drive_ok", nil, 3) // 2 healthy + 1 global spare treated as OK
155 assertValue(t, r, "hw_drive_degraded", nil, 0)
156 assertValue(t, r, "hw_drive_fault", nil, 0)
157 assertValue(t, r, "hw_drive_unknown", nil, 0)
158
159 assertValue(t, r, "hw_fan_ok", nil, 2)
160 assertValue(t, r, "hw_psu_ok", nil, 2)
161
162 // FRUs: 2 OK + 1 N/A (status=5 → OK)
163 assertValue(t, r, "hw_fru_ok", nil, 3)
164 assertValue(t, r, "hw_fru_degraded", nil, 0)
165
166 // Ports: 2 OK, 1 with health=4 (N/A) excluded
167 assertValue(t, r, "hw_port_ok", nil, 2)
168 assertValue(t, r, "hw_port_unknown", nil, 0)
169
170 // Controller stats
171 ctrlA := metrix.Labels{"controller": "controller_a"}
172 ctrlB := metrix.Labels{"controller": "controller_b"}
173 assertValue(t, r, "controller_iops", ctrlA, 1250)
174 assertValue(t, r, "controller_throughput", ctrlA, 52428800)
175 assertValue(t, r, "controller_cpu_load", ctrlA, 23.5)
176 assertValue(t, r, "controller_write_cache_used", ctrlA, 45)
177 assertValue(t, r, "controller_forwarded_cmds", ctrlA, 12500)
178 assertValue(t, r, "controller_data_read", ctrlA, 107374182400)
179 assertValue(t, r, "controller_data_written", ctrlA, 53687091200)
180 assertValue(t, r, "controller_read_ops", ctrlA, 5000000)
181 assertValue(t, r, "controller_write_ops", ctrlA, 2500000)
182 assertValue(t, r, "controller_write_cache_hits", ctrlA, 1800000)
183 assertValue(t, r, "controller_write_cache_misses", ctrlA, 200000)
184 assertValue(t, r, "controller_read_cache_hits", ctrlA, 3500000)
185 assertValue(t, r, "controller_read_cache_misses", ctrlA, 500000)
186 assertHasValue(t, r, "controller_iops", ctrlB)
187
188 // Volume stats
189 v1Labels := metrix.Labels{"volume": "prod-db-01"}
190 v2Labels := metrix.Labels{"volume": "prod-app-01"}
191 v3Labels := metrix.Labels{"volume": "test-backup"}
192 assertValue(t, r, "volume_iops", v1Labels, 850)
193 assertValue(t, r, "volume_throughput", v1Labels, 35651584)
194 assertValue(t, r, "volume_write_cache_percent", v1Labels, 52)
195 assertValue(t, r, "volume_tier_ssd", v1Labels, 60)
196 assertValue(t, r, "volume_tier_sas", v1Labels, 30)
197 assertValue(t, r, "volume_tier_sata", v1Labels, 10)
198 assertHasValue(t, r, "volume_iops", v2Labels)
199 assertHasValue(t, r, "volume_iops", v3Labels)
200
201 // Port stats
202 pA0 := metrix.Labels{"port": "hostport_A0"}
203 pA1 := metrix.Labels{"port": "hostport_A1"}
204 assertValue(t, r, "port_read_ops", pA0, 8500000)
205 assertValue(t, r, "port_write_ops", pA0, 4200000)
206 assertValue(t, r, "port_data_read", pA0, 180388626432)
207 assertValue(t, r, "port_data_written", pA0, 90194313216)
208 assertHasValue(t, r, "port_read_ops", pA1)
209
210 // PHY errors (aggregated per port: hostport_A0 has phy0+phy1, hostport_A1 has phy0)
211 phyA0 := metrix.Labels{"port": "hostport_A0"}
212 phyA1 := metrix.Labels{"port": "hostport_A1"}
213 assertValue(t, r, "phy_disparity_errors", phyA0, 8) // 5+3
214 assertValue(t, r, "phy_lost_dwords", phyA0, 2) // 2+0
215 assertValue(t, r, "phy_invalid_dwords", phyA0, 1) // 1+0
216 assertValue(t, r, "phy_disparity_errors", phyA1, 0)
217 assertValue(t, r, "phy_lost_dwords", phyA1, 1)
218
219 // Pool capacity (values × 512 to convert from blocks to bytes)
220 poolA := metrix.Labels{"pool": "Pool-A"}
221 poolB := metrix.Labels{"pool": "Pool-B"}
222 assertValue(t, r, "pool_total_bytes", poolA, 19531250000*512)
223 assertValue(t, r, "pool_available_bytes", poolA, 8789062500*512)
224 assertHasValue(t, r, "pool_total_bytes", poolB)
225
226 // Drive metrics
227 d00 := metrix.Labels{"drive": "0.0"}
228 d01 := metrix.Labels{"drive": "0.1"}
229 assertValue(t, r, "drive_temperature", d00, 32)
230 assertValue(t, r, "drive_power_on_hours", d00, 18750)
231 // HDD: ssd-life-left=255 → not reported
232 _, hasSSDLife := r.Value("drive_ssd_life_left", d00)
233 assert.False(t, hasSSDLife, "HDD should not report SSD life left")
234 // SSD: ssd-life-left=97 → reported
235 assertValue(t, r, "drive_ssd_life_left", d01, 97)
236
237 // Sensor metrics
238 tempSensor := metrix.Labels{"sensor": "sensor_temp_ctrl_A.1"}
239 assertValue(t, r, "sensor_temperature", tempSensor, 42)
240
241 voltSensor := metrix.Labels{"sensor": "sensor_volt_ctrl_A.1"}
242 assertValue(t, r, "sensor_voltage", voltSensor, 3300) // 3.30V → 3300mV
243
244 currSensor := metrix.Labels{"sensor": "sensor_curr_ctrl_A.1"}
245 assertValue(t, r, "sensor_current", currSensor, 1250) // 1.25A → 1250mA
246
247 chrgSensor := metrix.Labels{"sensor": "sensor_chrg_ctrl_A.1"}
248 assertValue(t, r, "sensor_charge_capacity", chrgSensor, 95)
249
250 // Chart coverage
251 collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{})
252 }
253
254 func TestCollector_CollectWithVolumeSelector(t *testing.T) {
255 srv := newMockPowerVaultServer()
256 defer srv.Close()
257
258 collr := New()
259 collr.Config = Config{
260 HTTPConfig: web.HTTPConfig{
261 RequestConfig: web.RequestConfig{
262 URL: srv.URL,
263 Username: "admin",
264 Password: "password",
265 },
266 },
267 VolumeSelector: "prod-*",
268 }
269
270 require.NoError(t, collr.Init(context.Background()))
271 require.NoError(t, collr.Check(context.Background()))
272
273 cc := mustCycleController(t, collr.MetricStore())
274 cc.BeginCycle()
275 require.NoError(t, collr.Collect(context.Background()))
276 cc.CommitCycleSuccess()
277
278 r := collr.MetricStore().Read(metrix.ReadRaw())
279
280 // prod-* volumes should be included
281 assertHasValue(t, r, "volume_iops", metrix.Labels{"volume": "prod-db-01"})
282 assertHasValue(t, r, "volume_iops", metrix.Labels{"volume": "prod-app-01"})
283 // test-backup should be excluded
284 _, ok := r.Value("volume_iops", metrix.Labels{"volume": "test-backup"})
285 assert.False(t, ok, "test-backup should be excluded by volume selector")
286 }
287
288 func TestCollector_Cleanup(t *testing.T) {
289 collr := New()
290 assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
291
292 srv := newMockPowerVaultServer()
293 defer srv.Close()
294
295 collr.Config = Config{
296 HTTPConfig: web.HTTPConfig{
297 RequestConfig: web.RequestConfig{
298 URL: srv.URL,
299 Username: "admin",
300 Password: "password",
301 },
302 },
303 }
304
305 require.NoError(t, collr.Init(context.Background()))
306 require.NoError(t, collr.Check(context.Background()))
307 assert.NotPanics(t, func() { collr.Cleanup(context.Background()) })
308 }
309
310 func TestCollector_ChartTemplateYAML(t *testing.T) {
311 templateYAML := New().ChartTemplateYAML()
312 collecttest.AssertChartTemplateSchema(t, templateYAML)
313
314 spec, err := charttpl.DecodeYAML([]byte(templateYAML))
315 require.NoError(t, err)
316 require.NoError(t, spec.Validate())
317
318 _, err = chartengine.Compile(spec, 1)
319 require.NoError(t, err)
320 }
321
322 func mustCycleController(t *testing.T, store metrix.CollectorStore) metrix.CycleController {
323 t.Helper()
324 managed, ok := metrix.AsCycleManagedStore(store)
325 require.True(t, ok, "store does not expose cycle control")
326 return managed.CycleController()
327 }
328
329 func assertValue(t *testing.T, r metrix.Reader, name string, labels metrix.Labels, want float64) {
330 t.Helper()
331 got, ok := r.Value(name, labels)
332 require.Truef(t, ok, "expected metric %s labels=%v", name, labels)
333 assert.InDeltaf(t, want, got, 1e-9, "unexpected value for %s labels=%v: got %v want %v", name, labels, got, want)
334 }
335
336 func assertHasValue(t *testing.T, r metrix.Reader, name string, labels metrix.Labels) {
337 t.Helper()
338 _, ok := r.Value(name, labels)
339 assert.Truef(t, ok, "expected metric %s labels=%v to exist", name, labels)
340 }
341
342 func newMockPowerVaultServer() *httptest.Server {
343 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
344 w.Header().Set("Content-Type", "application/json")
345
346 p := r.URL.Path
347
348 switch {
349 case strings.Contains(p, "/api/login/"):
350 writeTestData(w, "testdata/login.json")
351 case strings.HasSuffix(p, "/api/set/cli-parameters/locale/English"):
352 w.WriteHeader(http.StatusOK)
353 _, _ = w.Write([]byte(`{"status":[{"response-type":"Success","response":"Command completed successfully.","return-code":0,"response-type-numeric":0}]}`))
354 case strings.HasSuffix(p, "/api/show/system"):
355 writeTestData(w, "testdata/system.json")
356 case strings.HasSuffix(p, "/api/show/controllers"):
357 writeTestData(w, "testdata/controllers.json")
358 case strings.HasSuffix(p, "/api/show/disks"):
359 writeTestData(w, "testdata/drives.json")
360 case strings.HasSuffix(p, "/api/show/fans"):
361 writeTestData(w, "testdata/fans.json")
362 case strings.HasSuffix(p, "/api/show/power-supplies"):
363 writeTestData(w, "testdata/power_supplies.json")
364 case strings.HasSuffix(p, "/api/show/sensor-status"):
365 writeTestData(w, "testdata/sensors.json")
366 case strings.HasSuffix(p, "/api/show/frus"):
367 writeTestData(w, "testdata/frus.json")
368 case strings.HasSuffix(p, "/api/show/volumes"):
369 writeTestData(w, "testdata/volumes.json")
370 case strings.HasSuffix(p, "/api/show/pools"):
371 writeTestData(w, "testdata/pools.json")
372 case strings.HasSuffix(p, "/api/show/ports"):
373 writeTestData(w, "testdata/ports.json")
374 case strings.HasSuffix(p, "/api/show/controller-statistics"):
375 writeTestData(w, "testdata/controller_statistics.json")
376 case strings.HasSuffix(p, "/api/show/volume-statistics"):
377 writeTestData(w, "testdata/volume_statistics.json")
378 case strings.HasSuffix(p, "/api/show/host-port-statistics"):
379 writeTestData(w, "testdata/port_statistics.json")
380 case strings.HasSuffix(p, "/api/show/host-phy-statistics"):
381 writeTestData(w, "testdata/phy_statistics.json")
382 default:
383 w.WriteHeader(http.StatusNotFound)
384 }
385 }))
386 }
387
388 func writeTestData(w http.ResponseWriter, path string) {
389 data, err := os.ReadFile(path)
390 if err != nil {
391 w.WriteHeader(http.StatusInternalServerError)
392 return
393 }
394 _, _ = w.Write(data)
395 }