master
go 309 lines 8.09 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package client
4
5 import (
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "path"
12 "strconv"
13 "strings"
14 "sync"
15
16 "github.com/netdata/netdata/go/plugins/pkg/web"
17 )
18
19 /*
20 The REST API is served from the VxFlex OS Gateway.
21 The FxFlex Gateway connects to a single MDM and serves requests by querying the MDM
22 and reformatting the answers it receives from the MDM in s RESTful manner, back to a REST API.
23 The Gateway is stateless. It requires the MDM username and password for the login requests.
24 The login returns a token in the response, that is used for later authentication for other requests.
25
26 The token is valid for 8 hours from the time it was created, unless there has been no activity
27 for 10 minutes, of if the client has sent a logout request.
28
29 General URI:
30 - /api/login
31 - /api/logout
32 - /api/version
33 - /api/instances/ // GET all instances
34 - /api/types/{type}/instances // POST (create) / GET all objects for a given type
35 - /api/instances/{type::id} // GET by ID
36 - /api/instances/{type::id}/relationships/{Relationship name} // GET
37 - /api/instances/querySelectedStatistics // POST Query selected statistics
38 - /api/instances/{type::id}/action/{actionName} // POST a special action on an object
39 - /api/types/{type}/instances/action/{actionName} // POST a special action on a given type
40
41 Types:
42 - System
43 - Sds
44 - StoragePool
45 - ProtectionDomain
46 - Device
47 - Volume
48 - VTree
49 - Sdc
50 - User
51 - FaultSet
52 - RfcacheDevice
53 - Alerts
54
55 Actions:
56 - querySelectedStatistics // All types except Alarm and User
57 - querySystemLimits // System
58 - queryDisconnectedSdss // Sds
59 - querySdsNetworkLatencyMeters // Sds
60 - queryFailedDevices" // Device. Note: works strange!
61
62 Relationships:
63 - Statistics // All types except Alarm and User
64 - ProtectionDomain // System
65 - Sdc // System
66 - User // System
67 - StoragePool // ProtectionDomain
68 - FaultSet // ProtectionDomain
69 - Sds // ProtectionDomain
70 - RfcacheDevice // Sds
71 - Device // Sds, StoragePool
72 - Volume // Sdc, StoragePool
73 - VTree // StoragePool
74 */
75
76 // New creates new ScaleIO client.
77 func New(client web.ClientConfig, request web.RequestConfig) (*Client, error) {
78 httpClient, err := web.NewHTTPClient(client)
79 if err != nil {
80 return nil, err
81 }
82 return &Client{
83 Request: request,
84 httpClient: httpClient,
85 token: newToken(),
86 }, nil
87 }
88
89 // Client represents ScaleIO client.
90 type Client struct {
91 Request web.RequestConfig
92 httpClient *http.Client
93 token *token
94 }
95
96 // LoggedIn reports whether the client is logged in.
97 func (c *Client) LoggedIn() bool {
98 return c.token.isSet()
99 }
100
101 // Login connects to FxFlex Gateway to get the token that is used for later authentication for other requests.
102 func (c *Client) Login() error {
103 if c.LoggedIn() {
104 _ = c.Logout()
105 }
106 req := c.createLoginRequest()
107 resp, err := c.doOK(req)
108 defer web.CloseBody(resp)
109 if err != nil {
110 return err
111 }
112
113 token, err := decodeToken(resp.Body)
114 if err != nil {
115 return err
116 }
117
118 c.token.set(token)
119 return nil
120 }
121
122 // Logout sends logout request and unsets token.
123 func (c *Client) Logout() error {
124 if !c.LoggedIn() {
125 return nil
126 }
127 req := c.createLogoutRequest()
128 c.token.unset()
129
130 resp, err := c.do(req)
131 defer web.CloseBody(resp)
132 return err
133 }
134
135 // APIVersion returns FxFlex Gateway API version.
136 func (c *Client) APIVersion() (Version, error) {
137 req := c.createAPIVersionRequest()
138 resp, err := c.doOK(req)
139 defer web.CloseBody(resp)
140 if err != nil {
141 return Version{}, err
142 }
143 return decodeVersion(resp.Body)
144 }
145
146 // SelectedStatistics returns selected statistics.
147 func (c *Client) SelectedStatistics(query SelectedStatisticsQuery) (SelectedStatistics, error) {
148 b, _ := json.Marshal(query)
149 req := c.createSelectedStatisticsRequest(b)
150 var stats SelectedStatistics
151 err := c.doJSONWithRetry(&stats, req)
152 return stats, err
153 }
154
155 // Instances returns all instances.
156 func (c *Client) Instances() (Instances, error) {
157 req := c.createInstancesRequest()
158 var instances Instances
159 err := c.doJSONWithRetry(&instances, req)
160 return instances, err
161 }
162
163 func (c *Client) createLoginRequest() web.RequestConfig {
164 req := c.Request.Copy()
165 u, _ := url.Parse(req.URL)
166 u.Path = path.Join(u.Path, "/api/login")
167 req.URL = u.String()
168 return req
169 }
170
171 func (c *Client) createLogoutRequest() web.RequestConfig {
172 req := c.Request.Copy()
173 u, _ := url.Parse(req.URL)
174 u.Path = path.Join(u.Path, "/api/logout")
175 req.URL = u.String()
176 req.Password = c.token.get()
177 return req
178 }
179
180 func (c *Client) createAPIVersionRequest() web.RequestConfig {
181 req := c.Request.Copy()
182 u, _ := url.Parse(req.URL)
183 u.Path = path.Join(u.Path, "/api/version")
184 req.URL = u.String()
185 req.Password = c.token.get()
186 return req
187 }
188
189 func (c *Client) createSelectedStatisticsRequest(query []byte) web.RequestConfig {
190 req := c.Request.Copy()
191 u, _ := url.Parse(req.URL)
192 u.Path = path.Join(u.Path, "/api/instances/querySelectedStatistics")
193 req.URL = u.String()
194 req.Password = c.token.get()
195 req.Method = http.MethodPost
196 req.Headers = map[string]string{
197 "Content-Type": "application/json",
198 }
199 req.Body = string(query)
200 return req
201 }
202
203 func (c *Client) createInstancesRequest() web.RequestConfig {
204 req := c.Request.Copy()
205 u, _ := url.Parse(req.URL)
206 u.Path = path.Join(u.Path, "/api/instances")
207 req.URL = u.String()
208 req.Password = c.token.get()
209 return req
210 }
211
212 func (c *Client) do(req web.RequestConfig) (*http.Response, error) {
213 httpReq, err := web.NewHTTPRequest(req)
214 if err != nil {
215 return nil, fmt.Errorf("error on creating http request to %s: %v", req.URL, err)
216 }
217 return c.httpClient.Do(httpReq)
218 }
219
220 func (c *Client) doOK(req web.RequestConfig) (*http.Response, error) {
221 resp, err := c.do(req)
222 if err != nil {
223 return nil, err
224 }
225 if err = checkStatusCode(resp); err != nil {
226 err = fmt.Errorf("%s returned %v", req.URL, err)
227 }
228 return resp, err
229 }
230
231 func (c *Client) doOKWithRetry(req web.RequestConfig) (*http.Response, error) {
232 resp, err := c.do(req)
233 if err != nil {
234 return nil, err
235 }
236 if resp.StatusCode == http.StatusUnauthorized {
237 if err = c.Login(); err != nil {
238 return resp, err
239 }
240 req.Password = c.token.get()
241 return c.doOK(req)
242 }
243 if err = checkStatusCode(resp); err != nil {
244 err = fmt.Errorf("%s returned %v", req.URL, err)
245 }
246 return resp, err
247 }
248
249 func (c *Client) doJSONWithRetry(dst any, req web.RequestConfig) error {
250 resp, err := c.doOKWithRetry(req)
251 defer web.CloseBody(resp)
252 if err != nil {
253 return err
254 }
255 return json.NewDecoder(resp.Body).Decode(dst)
256 }
257
258 func checkStatusCode(resp *http.Response) error {
259 // For all 4xx and 5xx return codes, the body may contain an apiError
260 // instance with more specifics about the failure.
261 if resp.StatusCode >= 400 {
262 e := error(&apiError{})
263 if err := json.NewDecoder(resp.Body).Decode(e); err != nil {
264 e = err
265 }
266 return fmt.Errorf("HTTP status code %d : %v", resp.StatusCode, e)
267 }
268
269 // 200(OK), 201(Created), 202(Accepted), 204 (No Content).
270 if resp.StatusCode < 200 || resp.StatusCode > 299 {
271 return fmt.Errorf("HTTP status code %d", resp.StatusCode)
272 }
273 return nil
274 }
275
276 func decodeVersion(reader io.Reader) (ver Version, err error) {
277 bs, err := io.ReadAll(reader)
278 if err != nil {
279 return ver, err
280 }
281 parts := strings.Split(strings.Trim(string(bs), "\n "), ".")
282 if len(parts) != 2 {
283 return ver, fmt.Errorf("can't parse: %s", string(bs))
284 }
285 if ver.Major, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
286 return ver, err
287 }
288 ver.Minor, err = strconv.ParseInt(parts[1], 10, 64)
289 return ver, err
290 }
291
292 func decodeToken(reader io.Reader) (string, error) {
293 bs, err := io.ReadAll(reader)
294 if err != nil {
295 return "", err
296 }
297 return strings.Trim(string(bs), `"`), nil
298 }
299
300 type token struct {
301 mux *sync.RWMutex
302 value string
303 }
304
305 func newToken() *token { return &token{mux: &sync.RWMutex{}} }
306 func (t *token) get() string { t.mux.RLock(); defer t.mux.RUnlock(); return t.value }
307 func (t *token) set(v string) { t.mux.Lock(); defer t.mux.Unlock(); t.value = v }
308 func (t *token) unset() { t.set("") }
309 func (t *token) isSet() bool { return t.get() != "" }