master
go 293 lines 8.03 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package client
4
5 import (
6 "crypto/md5"
7 "crypto/sha256"
8 "encoding/json"
9 "fmt"
10 "net/http"
11 "net/url"
12 "path"
13 "sync"
14
15 "github.com/netdata/netdata/go/plugins/pkg/web"
16 )
17
18 // New creates a new PowerVault MCI REST API client.
19 func New(client web.ClientConfig, request web.RequestConfig, digest string) (*Client, error) {
20 httpClient, err := web.NewHTTPClient(client)
21 if err != nil {
22 return nil, err
23 }
24
25 return &Client{
26 request: request,
27 httpClient: httpClient,
28 digest: digest,
29 }, nil
30 }
31
32 // Client represents a Dell PowerVault MCI API client.
33 type Client struct {
34 request web.RequestConfig
35 httpClient *http.Client
36 digest string // "sha256" or "md5"
37
38 mu sync.Mutex // protects sessionKey reads/writes
39 sessionKey string
40 authMu sync.Mutex // serializes re-authentication
41 }
42
43 // Login authenticates with the PowerVault API.
44 // Hashes "username_password" with SHA-256 or MD5, then GET /api/login/<hash>.
45 func (c *Client) Login() error {
46 key, err := c.login()
47 if err != nil {
48 return err
49 }
50 c.setSessionKey(key)
51 return nil
52 }
53
54 // login authenticates and returns the session key without publishing it.
55 func (c *Client) login() (string, error) {
56 hash := c.authHash()
57
58 req := c.newRequest("/api/login/" + hash)
59
60 resp, err := c.doOK(req)
61 defer web.CloseBody(resp)
62 if err != nil {
63 return "", fmt.Errorf("login failed: %v", err)
64 }
65
66 var result struct {
67 Status []StatusResponse `json:"status"`
68 }
69 if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
70 return "", fmt.Errorf("login: error decoding response: %v", err)
71 }
72 if len(result.Status) == 0 || result.Status[0].ResponseType != "Success" {
73 if len(result.Status) > 0 {
74 return "", fmt.Errorf("login: authentication failed: %s (rc=%d)", result.Status[0].Response, result.Status[0].ReturnCode)
75 }
76 return "", fmt.Errorf("login: authentication failed: empty status response")
77 }
78
79 return result.Status[0].Response, nil
80 }
81
82 func (c *Client) setSessionKey(key string) {
83 c.mu.Lock()
84 c.sessionKey = key
85 c.mu.Unlock()
86 }
87
88 // SetLocale sets the CLI output to English to prevent locale-dependent parsing.
89 func (c *Client) SetLocale() error {
90 req := c.newSessionRequest("/api/set/cli-parameters/locale/English")
91 resp, err := c.doOK(req)
92 web.CloseBody(resp)
93 return err
94 }
95
96 // setLocale sets the CLI locale to English using the given session key.
97 func (c *Client) setLocale(sessionKey string) error {
98 req := c.newRequest("/api/set/cli-parameters/locale/English")
99 if sessionKey != "" {
100 req.Headers["sessionKey"] = sessionKey
101 }
102 resp, err := c.doOK(req)
103 web.CloseBody(resp)
104 return err
105 }
106
107 // System returns system information.
108 func (c *Client) System() ([]SystemInfo, error) {
109 return doShow[SystemInfo](c, "/api/show/system", "system")
110 }
111
112 // Controllers returns all controllers.
113 func (c *Client) Controllers() ([]Controller, error) {
114 return doShow[Controller](c, "/api/show/controllers", "controllers")
115 }
116
117 // Drives returns all drives.
118 func (c *Client) Drives() ([]Drive, error) {
119 return doShow[Drive](c, "/api/show/disks", "drives")
120 }
121
122 // Fans returns all fans.
123 func (c *Client) Fans() ([]Fan, error) {
124 return doShow[Fan](c, "/api/show/fans", "fan")
125 }
126
127 // PowerSupplies returns all power supplies.
128 func (c *Client) PowerSupplies() ([]PowerSupply, error) {
129 return doShow[PowerSupply](c, "/api/show/power-supplies", "power-supplies")
130 }
131
132 // Sensors returns all sensor readings.
133 func (c *Client) Sensors() ([]Sensor, error) {
134 return doShow[Sensor](c, "/api/show/sensor-status", "sensors")
135 }
136
137 // FRUs returns all Field Replaceable Units.
138 func (c *Client) FRUs() ([]FRU, error) {
139 return doShow[FRU](c, "/api/show/frus", "enclosure-fru")
140 }
141
142 // Volumes returns all volumes.
143 func (c *Client) Volumes() ([]Volume, error) {
144 return doShow[Volume](c, "/api/show/volumes", "volumes")
145 }
146
147 // Pools returns all storage pools.
148 func (c *Client) Pools() ([]Pool, error) {
149 return doShow[Pool](c, "/api/show/pools", "pools")
150 }
151
152 // Ports returns all host ports.
153 func (c *Client) Ports() ([]Port, error) {
154 return doShow[Port](c, "/api/show/ports", "port")
155 }
156
157 // ControllerStatistics returns performance stats for all controllers.
158 func (c *Client) ControllerStatistics() ([]ControllerStats, error) {
159 return doShow[ControllerStats](c, "/api/show/controller-statistics", "controller-statistics")
160 }
161
162 // VolumeStatistics returns performance stats for all volumes.
163 func (c *Client) VolumeStatistics() ([]VolumeStats, error) {
164 return doShow[VolumeStats](c, "/api/show/volume-statistics", "volume-statistics")
165 }
166
167 // PortStatistics returns I/O stats for all host ports.
168 func (c *Client) PortStatistics() ([]PortStats, error) {
169 return doShow[PortStats](c, "/api/show/host-port-statistics", "host-port-statistics")
170 }
171
172 // PhyStatistics returns SAS PHY error stats.
173 func (c *Client) PhyStatistics() ([]PhyStats, error) {
174 return doShow[PhyStats](c, "/api/show/host-phy-statistics", "sas-host-phy-statistics")
175 }
176
177 // doShow fetches a /api/show/<command> endpoint and extracts the named array from the response.
178 // Re-authenticates once on 401 (session expiry), including locale reset.
179 func doShow[T any](c *Client, urlPath, key string) ([]T, error) {
180 req := c.newSessionRequest(urlPath)
181
182 resp, err := c.doOK(req)
183 if err != nil && resp != nil && resp.StatusCode == http.StatusUnauthorized {
184 if loginErr := c.reAuth(); loginErr != nil {
185 return nil, fmt.Errorf("%s: session expired and re-auth failed: %v", urlPath, loginErr)
186 }
187 req = c.newSessionRequest(urlPath)
188 resp, err = c.doOK(req)
189 }
190 defer web.CloseBody(resp)
191 if err != nil {
192 return nil, err
193 }
194
195 var raw map[string]json.RawMessage
196 if err = json.NewDecoder(resp.Body).Decode(&raw); err != nil {
197 return nil, fmt.Errorf("%s: error decoding response: %v", urlPath, err)
198 }
199
200 // Check for API-level errors in the status envelope.
201 if statusData, ok := raw["status"]; ok {
202 var statuses []StatusResponse
203 if json.Unmarshal(statusData, &statuses) == nil {
204 for _, s := range statuses {
205 if s.ResponseType == "Error" {
206 return nil, fmt.Errorf("%s: API error: %s (rc=%d)", urlPath, s.Response, s.ReturnCode)
207 }
208 }
209 }
210 }
211
212 data, ok := raw[key]
213 if !ok {
214 return nil, nil
215 }
216
217 var result []T
218 if err = json.Unmarshal(data, &result); err != nil {
219 return nil, fmt.Errorf("%s: error decoding %q: %v", urlPath, key, err)
220 }
221 return result, nil
222 }
223
224 // reAuth re-authenticates and restores session locale.
225 // Serialized so concurrent 401 retries don't race on session state.
226 // The new session key is published only after locale setup, preventing
227 // concurrent requests from seeing a session with non-English locale.
228 func (c *Client) reAuth() error {
229 c.authMu.Lock()
230 defer c.authMu.Unlock()
231 key, err := c.login()
232 if err != nil {
233 return err
234 }
235 if err := c.setLocale(key); err != nil {
236 return err
237 }
238 c.setSessionKey(key)
239 return nil
240 }
241
242 func (c *Client) authHash() string {
243 cred := c.request.Username + "_" + c.request.Password
244 if c.digest == "md5" {
245 return fmt.Sprintf("%x", md5.Sum([]byte(cred)))
246 }
247 return fmt.Sprintf("%x", sha256.Sum256([]byte(cred)))
248 }
249
250 func (c *Client) newRequest(urlPath string) web.RequestConfig {
251 req := c.request.Copy()
252 u, _ := url.Parse(req.URL)
253 u.Path = path.Join(u.Path, urlPath)
254 req.URL = u.String()
255 // Clear basic auth — MCI API uses hash-based auth, not HTTP Basic.
256 req.Username = ""
257 req.Password = ""
258 if req.Headers == nil {
259 req.Headers = make(map[string]string)
260 }
261 req.Headers["datatype"] = "json"
262 return req
263 }
264
265 func (c *Client) newSessionRequest(urlPath string) web.RequestConfig {
266 req := c.newRequest(urlPath)
267
268 c.mu.Lock()
269 key := c.sessionKey
270 c.mu.Unlock()
271
272 if key != "" {
273 req.Headers["sessionKey"] = key
274 }
275 return req
276 }
277
278 func (c *Client) doOK(req web.RequestConfig) (*http.Response, error) {
279 httpReq, err := web.NewHTTPRequest(req)
280 if err != nil {
281 return nil, fmt.Errorf("error creating request to %s: %v", req.URL, err)
282 }
283 resp, err := c.httpClient.Do(httpReq)
284 if err != nil {
285 return resp, err
286 }
287 if resp.StatusCode < 200 || resp.StatusCode >= 400 {
288 web.CloseBody(resp)
289 // Return resp (body closed) so callers can inspect StatusCode.
290 return resp, fmt.Errorf("%s: HTTP %d", req.URL, resp.StatusCode)
291 }
292 return resp, nil
293 }