master
go 332 lines 10.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package ceph
4
5 import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "io"
10 "net/http"
11 "net/http/httptest"
12 "os"
13 "sync/atomic"
14 "testing"
15
16 "github.com/stretchr/testify/assert"
17
18 "github.com/netdata/netdata/go/plugins/pkg/web"
19 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
20
21 "github.com/stretchr/testify/require"
22 )
23
24 var (
25 dataConfigJSON, _ = os.ReadFile("testdata/config.json")
26 dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
27
28 dataVer16ApiHealthMinimal, _ = os.ReadFile("testdata/v16.2.15/api_health_minimal.json")
29 dataVer16ApiOsd, _ = os.ReadFile("testdata/v16.2.15/api_osd.json")
30 dataVer16ApiPoolStats, _ = os.ReadFile("testdata/v16.2.15/api_pool_stats.json")
31 dataVer16ApiMonitor, _ = os.ReadFile("testdata/v16.2.15/api_monitor.json")
32 )
33
34 func Test_testDataIsValid(t *testing.T) {
35 for name, data := range map[string][]byte{
36 "dataConfigJSON": dataConfigJSON,
37 "dataConfigYAML": dataConfigYAML,
38 "dataVer16ApiHealthMinimal": dataVer16ApiHealthMinimal,
39 "dataVer16ApiOsd": dataVer16ApiOsd,
40 "dataVer16ApiPoolStats": dataVer16ApiPoolStats,
41 "dataVer16ApiMonitor": dataVer16ApiMonitor,
42 } {
43 require.NotNil(t, data, name)
44 }
45 }
46
47 func TestCollector_Configuration(t *testing.T) {
48 collecttest.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
49 }
50
51 func TestCollector_Init(t *testing.T) {
52 tests := map[string]struct {
53 wantFail bool
54 config Config
55 }{
56 "fails with default": {
57 wantFail: true,
58 config: New().Config,
59 },
60 "fail when URL not set": {
61 wantFail: true,
62 config: Config{
63 HTTPConfig: web.HTTPConfig{
64 RequestConfig: web.RequestConfig{URL: ""},
65 },
66 },
67 },
68 }
69
70 for name, test := range tests {
71 t.Run(name, func(t *testing.T) {
72 collr := New()
73 collr.Config = test.config
74
75 if test.wantFail {
76 assert.Error(t, collr.Init(context.Background()))
77 } else {
78 assert.NoError(t, collr.Init(context.Background()))
79 }
80 })
81 }
82 }
83
84 func TestCollector_Check(t *testing.T) {
85 tests := map[string]struct {
86 wantFail bool
87 prepare func(t *testing.T) (collr *Collector, cleanup func())
88 }{
89 "success with valid API key": {
90 wantFail: false,
91 prepare: caseOk,
92 },
93 "fail on connection refused": {
94 wantFail: true,
95 prepare: caseConnectionRefused,
96 },
97 "fail on 404 response": {
98 wantFail: true,
99 prepare: case404,
100 },
101 }
102
103 for name, test := range tests {
104 t.Run(name, func(t *testing.T) {
105 collr, cleanup := test.prepare(t)
106 defer cleanup()
107
108 if test.wantFail {
109 assert.Error(t, collr.Check(context.Background()))
110 } else {
111 assert.NoError(t, collr.Check(context.Background()))
112 }
113 })
114 }
115 }
116
117 func TestCollector_Charts(t *testing.T) {
118 assert.NotNil(t, New().Charts())
119 }
120
121 func TestCollector_Collect(t *testing.T) {
122 tests := map[string]struct {
123 prepare func(t *testing.T) (collr *Collector, cleanup func())
124 wantNumOfCharts int
125 wantMetrics map[string]int64
126 }{
127 "success with valid API key": {
128 prepare: caseOk,
129 wantNumOfCharts: len(clusterCharts) + len(osdChartsTmpl)*2 + len(poolChartsTmpl)*2,
130 wantMetrics: map[string]int64{
131 "client_perf_read_bytes_sec": 1,
132 "client_perf_read_op_per_sec": 1,
133 "client_perf_recovering_bytes_per_sec": 1,
134 "client_perf_write_bytes_sec": 1,
135 "client_perf_write_op_per_sec": 1,
136 "health_err": 0,
137 "health_ok": 0,
138 "health_warn": 1,
139 "hosts_num": 1,
140 "iscsi_daemons_down_num": 1,
141 "iscsi_daemons_num": 2,
142 "iscsi_daemons_up_num": 1,
143 "mgr_active_num": 1,
144 "mgr_standby_num": 1,
145 "monitors_num": 1,
146 "objects_degraded_num": 1,
147 "objects_healthy_num": 3,
148 "objects_misplaced_num": 1,
149 "objects_num": 6,
150 "objects_unfound_num": 1,
151 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_apply_latency_ms": 1,
152 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_commit_latency_ms": 1,
153 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_read_bytes": 1,
154 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_read_ops": 1,
155 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_size_bytes": 68715282432,
156 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_space_avail_bytes": 68410753024,
157 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_space_used_bytes": 304529408,
158 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_status_down": 0,
159 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_status_in": 1,
160 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_status_out": 0,
161 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_status_up": 1,
162 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_write_ops": 1,
163 "osd_f5bbbe9d-e85b-419c-af5a-a57e2527cad3_written_bytes": 1,
164 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_apply_latency_ms": 1,
165 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_commit_latency_ms": 1,
166 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_read_bytes": 1,
167 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_read_ops": 1,
168 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_size_bytes": 107369988096,
169 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_space_avail_bytes": 107065458688,
170 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_space_used_bytes": 304529408,
171 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_status_down": 0,
172 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_status_in": 1,
173 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_status_out": 0,
174 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_status_up": 1,
175 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_write_ops": 1,
176 "osd_f78537db-9b18-4c62-a24f-a4344fc28de7_written_bytes": 1,
177 "osds_down_num": 0,
178 "osds_in_num": 2,
179 "osds_num": 2,
180 "osds_out_num": 0,
181 "osds_up_num": 2,
182 "pg_status_category_clean": 1,
183 "pg_status_category_unknown": 0,
184 "pg_status_category_warning": 1,
185 "pg_status_category_working": 0,
186 "pgs_num": 2,
187 "pgs_per_osd": 2,
188 "pool_device_health_metrics_objects": 3,
189 "pool_device_health_metrics_read_bytes": 1,
190 "pool_device_health_metrics_read_ops": 1,
191 "pool_device_health_metrics_size": 166530172973,
192 "pool_device_health_metrics_space_avail_bytes": 166530172972,
193 "pool_device_health_metrics_space_used_bytes": 1,
194 "pool_device_health_metrics_space_utilization": 1000,
195 "pool_device_health_metrics_write_ops": 3,
196 "pool_device_health_metrics_written_bytes": 6144,
197 "pool_mySuperPool_objects": 1,
198 "pool_mySuperPool_read_bytes": 1,
199 "pool_mySuperPool_read_ops": 1,
200 "pool_mySuperPool_size": 166530172973,
201 "pool_mySuperPool_space_avail_bytes": 166530172972,
202 "pool_mySuperPool_space_used_bytes": 1,
203 "pool_mySuperPool_space_utilization": 1000,
204 "pool_mySuperPool_write_ops": 1,
205 "pool_mySuperPool_written_bytes": 1,
206 "pools_num": 2,
207 "raw_capacity_avail_bytes": 175476178944,
208 "raw_capacity_used_bytes": 609091584,
209 "raw_capacity_utilization": 345,
210 "rgw_num": 1,
211 "scrub_status_active": 0,
212 "scrub_status_disabled": 0,
213 "scrub_status_inactive": 1,
214 },
215 },
216 "fail on connection refused": {
217 prepare: caseConnectionRefused,
218 wantMetrics: nil,
219 },
220 "fail on 404 response": {
221 prepare: case404,
222 wantMetrics: nil,
223 },
224 }
225
226 for name, test := range tests {
227 t.Run(name, func(t *testing.T) {
228 collr, cleanup := test.prepare(t)
229 defer cleanup()
230
231 _ = collr.Check(context.Background())
232
233 mx := collr.Collect(context.Background())
234
235 require.Equal(t, test.wantMetrics, mx)
236
237 if len(test.wantMetrics) > 0 {
238 assert.Equal(t, test.wantNumOfCharts, len(*collr.Charts()), "want charts")
239
240 collecttest.TestMetricsHasAllChartsDims(t, collr.Charts(), mx)
241 }
242 })
243 }
244 }
245
246 func caseOk(t *testing.T) (*Collector, func()) {
247 t.Helper()
248
249 loginResp, _ := json.Marshal(authLoginResp{Token: "secret_token"})
250 checkResp, _ := json.Marshal(authCheckResp{Username: "username"})
251 var loggedIn atomic.Bool
252
253 srv := httptest.NewServer(http.HandlerFunc(
254 func(w http.ResponseWriter, r *http.Request) {
255 switch r.Method {
256 case http.MethodPost:
257 switch r.URL.Path {
258 case urlPathApiAuth:
259 _, _ = w.Write(loginResp)
260 w.WriteHeader(http.StatusCreated)
261 loggedIn.Store(true)
262 case urlPathApiAuthCheck:
263 bs, _ := io.ReadAll(r.Body)
264 if bytes.Equal(bs, loginResp) {
265 _, _ = w.Write(checkResp)
266 } else {
267 w.WriteHeader(http.StatusNotFound)
268 }
269 case urlPathApiAuthLogout:
270 w.WriteHeader(http.StatusOK)
271 loggedIn.Store(false)
272 default:
273 w.WriteHeader(http.StatusNotFound)
274 }
275 case http.MethodGet:
276 if !loggedIn.Load() {
277 w.WriteHeader(http.StatusUnauthorized)
278 return
279 }
280 switch r.URL.Path {
281 case urlPathApiHealthMinimal:
282 _, _ = w.Write(dataVer16ApiHealthMinimal)
283 case urlPathApiOsd:
284 _, _ = w.Write(dataVer16ApiOsd)
285 case urlPathApiPool:
286 if r.URL.RawQuery != urlQueryApiPool {
287 w.WriteHeader(http.StatusNotFound)
288 } else {
289 _, _ = w.Write(dataVer16ApiPoolStats)
290 }
291 case urlPathApiMonitor:
292 _, _ = w.Write(dataVer16ApiMonitor)
293 default:
294 w.WriteHeader(http.StatusNotFound)
295 }
296 }
297 }))
298
299 collr := New()
300 collr.URL = srv.URL
301 collr.Username = "user"
302 collr.Password = "password"
303 require.NoError(t, collr.Init(context.Background()))
304
305 return collr, srv.Close
306 }
307
308 func caseConnectionRefused(t *testing.T) (*Collector, func()) {
309 t.Helper()
310 collr := New()
311 collr.URL = "http://127.0.0.1:65001"
312 collr.Username = "user"
313 collr.Password = "password"
314 require.NoError(t, collr.Init(context.Background()))
315
316 return collr, func() {}
317 }
318
319 func case404(t *testing.T) (*Collector, func()) {
320 t.Helper()
321 srv := httptest.NewServer(http.HandlerFunc(
322 func(w http.ResponseWriter, r *http.Request) {
323 w.WriteHeader(http.StatusNotFound)
324 }))
325 collr := New()
326 collr.URL = srv.URL
327 collr.Username = "user"
328 collr.Password = "password"
329 require.NoError(t, collr.Init(context.Background()))
330
331 return collr, srv.Close
332 }