master
go 363 lines 9.99 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package client
4
5 import (
6 "encoding/json"
7 "fmt"
8 "maps"
9 "net/http"
10 "net/http/cookiejar"
11 "net/url"
12 "path"
13 "strconv"
14 "sync"
15
16 "github.com/netdata/netdata/go/plugins/pkg/web"
17 )
18
19 const (
20 apiBasePath = "/api/rest"
21 dellEMCToken = "DELL-EMC-TOKEN"
22 defaultLimit = "2000"
23 )
24
25 // New creates a new PowerStore REST API client.
26 func New(client web.ClientConfig, request web.RequestConfig) (*Client, error) {
27 httpClient, err := web.NewHTTPClient(client)
28 if err != nil {
29 return nil, err
30 }
31
32 jar, err := cookiejar.New(nil)
33 if err != nil {
34 return nil, fmt.Errorf("error creating cookie jar: %v", err)
35 }
36 httpClient.Jar = jar
37
38 return &Client{
39 Request: request,
40 httpClient: httpClient,
41 csrf: &csrfToken{},
42 }, nil
43 }
44
45 // Client represents a Dell PowerStore REST API client.
46 type Client struct {
47 Request web.RequestConfig
48 httpClient *http.Client
49 csrf *csrfToken
50 }
51
52 // Login authenticates with the PowerStore API.
53 // GET /api/rest/login_session with Basic Auth.
54 // Caches auth_cookie (via cookiejar) and DELL-EMC-TOKEN (from response header).
55 func (c *Client) Login() error {
56 req := c.createRequest("/login_session")
57
58 resp, err := c.doOK(req)
59 defer web.CloseBody(resp)
60 if err != nil {
61 return fmt.Errorf("login failed: %v", err)
62 }
63
64 c.cacheCSRFToken(resp)
65 return nil
66 }
67
68 // Logout clears the client session state.
69 func (c *Client) Logout() {
70 c.csrf.unset()
71 }
72
73 // Clusters returns all clusters.
74 func (c *Client) Clusters() ([]Cluster, error) {
75 return doGetAllPages[Cluster](c, "/cluster", nil)
76 }
77
78 // Appliances returns all appliances.
79 func (c *Client) Appliances() ([]Appliance, error) {
80 return doGetAllPages[Appliance](c, "/appliance", nil)
81 }
82
83 // Volumes returns all volumes.
84 func (c *Client) Volumes() ([]Volume, error) {
85 return doGetAllPages[Volume](c, "/volume", nil)
86 }
87
88 // AllHardware returns all hardware components.
89 func (c *Client) AllHardware() ([]Hardware, error) {
90 return doGetAllPages[Hardware](c, "/hardware", nil)
91 }
92
93 // Alerts returns alerts filtered by state.
94 func (c *Client) Alerts(state string) ([]Alert, error) {
95 return doGetAllPages[Alert](c, "/alert", url.Values{"state": {"eq." + state}})
96 }
97
98 // FcPorts returns all Fibre Channel ports.
99 func (c *Client) FcPorts() ([]FcPort, error) {
100 return doGetAllPages[FcPort](c, "/fc_port", nil)
101 }
102
103 // EthPorts returns all Ethernet ports.
104 func (c *Client) EthPorts() ([]EthPort, error) {
105 return doGetAllPages[EthPort](c, "/eth_port", nil)
106 }
107
108 // FileSystems returns all file systems.
109 func (c *Client) FileSystems() ([]FileSystem, error) {
110 return doGetAllPages[FileSystem](c, "/file_system", nil)
111 }
112
113 // NASServers returns all NAS servers.
114 func (c *Client) NASServers() ([]NAS, error) {
115 return doGetAllPages[NAS](c, "/nas_server", nil)
116 }
117
118 // PerformanceMetricsByAppliance returns performance metrics for an appliance.
119 func (c *Client) PerformanceMetricsByAppliance(id string) ([]PerformanceMetrics, error) {
120 return doMetrics[PerformanceMetrics](c, "performance_metrics_by_appliance", id, "Five_Mins")
121 }
122
123 // PerformanceMetricsByVolume returns performance metrics for a volume.
124 func (c *Client) PerformanceMetricsByVolume(id string) ([]PerformanceMetrics, error) {
125 return doMetrics[PerformanceMetrics](c, "performance_metrics_by_volume", id, "Five_Mins")
126 }
127
128 // PerformanceMetricsByNode returns performance metrics for a node.
129 func (c *Client) PerformanceMetricsByNode(id string) ([]PerformanceMetrics, error) {
130 return doMetrics[PerformanceMetrics](c, "performance_metrics_by_node", id, "Five_Mins")
131 }
132
133 // PerformanceMetricsByFcPort returns performance metrics for an FC port.
134 func (c *Client) PerformanceMetricsByFcPort(id string) ([]PerformanceMetrics, error) {
135 return doMetrics[PerformanceMetrics](c, "performance_metrics_by_fe_fc_port", id, "Five_Mins")
136 }
137
138 // EthPortPerformanceMetrics returns performance metrics for an Ethernet port.
139 func (c *Client) EthPortPerformanceMetrics(id string) ([]EthPortMetrics, error) {
140 return doMetrics[EthPortMetrics](c, "performance_metrics_by_fe_eth_port", id, "Five_Mins")
141 }
142
143 // PerformanceMetricsByFileSystem returns performance metrics for a file system.
144 func (c *Client) PerformanceMetricsByFileSystem(id string) ([]FileSystemMetrics, error) {
145 return doMetrics[FileSystemMetrics](c, "performance_metrics_by_file_system", id, "Five_Mins")
146 }
147
148 // SpaceMetricsByCluster returns space metrics for a cluster.
149 func (c *Client) SpaceMetricsByCluster(id string) ([]SpaceMetrics, error) {
150 return doMetrics[SpaceMetrics](c, "space_metrics_by_cluster", id, "One_Day")
151 }
152
153 // SpaceMetricsByAppliance returns space metrics for an appliance.
154 func (c *Client) SpaceMetricsByAppliance(id string) ([]SpaceMetrics, error) {
155 return doMetrics[SpaceMetrics](c, "space_metrics_by_appliance", id, "One_Day")
156 }
157
158 // SpaceMetricsByVolume returns space metrics for a volume.
159 func (c *Client) SpaceMetricsByVolume(id string) ([]SpaceMetrics, error) {
160 return doMetrics[SpaceMetrics](c, "space_metrics_by_volume", id, "Five_Mins")
161 }
162
163 // WearMetricsByDrive returns wear metrics for a drive.
164 func (c *Client) WearMetricsByDrive(id string) ([]WearMetrics, error) {
165 return doMetrics[WearMetrics](c, "wear_metrics_by_drive", id, "Five_Mins")
166 }
167
168 // CopyMetricsByAppliance returns copy/replication metrics for an appliance.
169 func (c *Client) CopyMetricsByAppliance(id string) ([]CopyMetrics, error) {
170 return doMetrics[CopyMetrics](c, "copy_metrics_by_appliance", id, "Five_Mins")
171 }
172
173 func doMetrics[T any](c *Client, entity, entityID, interval string) ([]T, error) {
174 body := MetricsRequest{Entity: entity, EntityID: entityID, Interval: interval}
175 var v []T
176 if err := c.doPostWithRetry(&v, "/metrics/generate", body); err != nil {
177 return nil, err
178 }
179 return v, nil
180 }
181
182 func (c *Client) createRequest(urlPath string) web.RequestConfig {
183 req := c.Request.Copy()
184 u, _ := url.Parse(req.URL)
185 u.Path = path.Join(u.Path, apiBasePath, urlPath)
186 req.URL = u.String()
187 return req
188 }
189
190 func (c *Client) createGetRequest(urlPath string, params url.Values) web.RequestConfig {
191 req := c.createRequest(urlPath)
192 u, _ := url.Parse(req.URL)
193 q := u.Query()
194 q.Set("select", "*")
195 q.Set("limit", defaultLimit)
196 for k, vals := range params {
197 for _, v := range vals {
198 q.Set(k, v)
199 }
200 }
201 u.RawQuery = q.Encode()
202 req.URL = u.String()
203
204 if tok := c.csrf.get(); tok != "" {
205 if req.Headers == nil {
206 req.Headers = make(map[string]string)
207 }
208 req.Headers[dellEMCToken] = tok
209 }
210 return req
211 }
212
213 func (c *Client) createPostRequest(urlPath string, body any) (web.RequestConfig, error) {
214 req := c.createRequest(urlPath)
215
216 b, err := json.Marshal(body)
217 if err != nil {
218 return req, fmt.Errorf("error marshaling request body: %v", err)
219 }
220
221 if req.Headers == nil {
222 req.Headers = make(map[string]string)
223 }
224 req.Headers["Content-Type"] = "application/json"
225 if tok := c.csrf.get(); tok != "" {
226 req.Headers[dellEMCToken] = tok
227 }
228 req.Method = http.MethodPost
229 req.Body = string(b)
230 return req, nil
231 }
232
233 func (c *Client) do(req web.RequestConfig) (*http.Response, error) {
234 httpReq, err := web.NewHTTPRequest(req)
235 if err != nil {
236 return nil, fmt.Errorf("error creating http request to %s: %v", req.URL, err)
237 }
238 return c.httpClient.Do(httpReq)
239 }
240
241 func (c *Client) doOK(req web.RequestConfig) (*http.Response, error) {
242 resp, err := c.do(req)
243 if err != nil {
244 return nil, err
245 }
246 if err = checkStatusCode(resp); err != nil {
247 err = fmt.Errorf("%s returned %v", req.URL, err)
248 }
249 return resp, err
250 }
251
252 func (c *Client) doOKWithRetry(req web.RequestConfig) (*http.Response, error) {
253 resp, err := c.do(req)
254 if err != nil {
255 return nil, err
256 }
257 // PowerStore returns 403 when the session/token is stale (not 401)
258 if resp.StatusCode == http.StatusForbidden {
259 web.CloseBody(resp)
260 if err = c.Login(); err != nil {
261 return nil, fmt.Errorf("re-login after 403 failed: %v", err)
262 }
263 req = c.applyCSRF(req)
264 return c.doOK(req)
265 }
266 if err = checkStatusCode(resp); err != nil {
267 err = fmt.Errorf("%s returned %v", req.URL, err)
268 }
269 return resp, err
270 }
271
272 // doGetAllPages fetches all pages from a paginated GET endpoint.
273 // PowerStore returns HTTP 206 (Partial Content) when more pages are available,
274 // with a server-enforced maximum of 2000 items per page.
275 func doGetAllPages[T any](c *Client, urlPath string, params url.Values) ([]T, error) {
276 var all []T
277 offset := 0
278
279 for {
280 reqParams := make(url.Values)
281 maps.Copy(reqParams, params)
282 if offset > 0 {
283 reqParams.Set("offset", strconv.Itoa(offset))
284 }
285
286 req := c.createGetRequest(urlPath, reqParams)
287
288 resp, err := c.doOKWithRetry(req)
289 if err != nil {
290 web.CloseBody(resp)
291 return nil, err
292 }
293
294 var page []T
295 err = json.NewDecoder(resp.Body).Decode(&page)
296 c.cacheCSRFToken(resp)
297 partial := resp.StatusCode == http.StatusPartialContent
298 web.CloseBody(resp)
299
300 if err != nil {
301 return nil, fmt.Errorf("error decoding %s response: %v", urlPath, err)
302 }
303
304 all = append(all, page...)
305
306 if !partial || len(page) == 0 {
307 break
308 }
309 offset += len(page)
310 }
311
312 return all, nil
313 }
314
315 func (c *Client) doPostWithRetry(dst any, urlPath string, body any) error {
316 req, err := c.createPostRequest(urlPath, body)
317 if err != nil {
318 return err
319 }
320 resp, err := c.doOKWithRetry(req)
321 defer web.CloseBody(resp)
322 if err != nil {
323 return err
324 }
325 c.cacheCSRFToken(resp)
326 return json.NewDecoder(resp.Body).Decode(dst)
327 }
328
329 func (c *Client) cacheCSRFToken(resp *http.Response) {
330 if resp == nil {
331 return
332 }
333 if tok := resp.Header.Get(dellEMCToken); tok != "" {
334 c.csrf.set(tok)
335 }
336 }
337
338 func (c *Client) applyCSRF(req web.RequestConfig) web.RequestConfig {
339 if tok := c.csrf.get(); tok != "" {
340 if req.Headers == nil {
341 req.Headers = make(map[string]string)
342 }
343 req.Headers[dellEMCToken] = tok
344 }
345 return req
346 }
347
348 func checkStatusCode(resp *http.Response) error {
349 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
350 return fmt.Errorf("HTTP status code %d", resp.StatusCode)
351 }
352 return nil
353 }
354
355 // csrfToken safely stores the DELL-EMC-TOKEN value.
356 type csrfToken struct {
357 mux sync.RWMutex
358 value string
359 }
360
361 func (t *csrfToken) get() string { t.mux.RLock(); defer t.mux.RUnlock(); return t.value }
362 func (t *csrfToken) set(v string) { t.mux.Lock(); defer t.mux.Unlock(); t.value = v }
363 func (t *csrfToken) unset() { t.set("") }