github.com/netdata/go.d.plugin@v0.58.1/modules/scaleio/client/client.go (about) 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/go.d.plugin/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.Client, request web.Request) (*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.Request 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 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 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 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.Request { 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.Request { 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.Request { 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.Request { 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.Request { 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.Request) (*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.Request) (*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.Request) (*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 interface{}, req web.Request) error { 250 resp, err := c.doOKWithRetry(req) 251 defer closeBody(resp) 252 if err != nil { 253 return err 254 } 255 return json.NewDecoder(resp.Body).Decode(dst) 256 } 257 258 func closeBody(resp *http.Response) { 259 if resp != nil && resp.Body != nil { 260 _, _ = io.Copy(io.Discard, resp.Body) 261 _ = resp.Body.Close() 262 } 263 } 264 265 func checkStatusCode(resp *http.Response) error { 266 // For all 4xx and 5xx return codes, the body may contain an apiError 267 // instance with more specifics about the failure. 268 if resp.StatusCode >= 400 { 269 e := error(&apiError{}) 270 if err := json.NewDecoder(resp.Body).Decode(e); err != nil { 271 e = err 272 } 273 return fmt.Errorf("HTTP status code %d : %v", resp.StatusCode, e) 274 } 275 276 // 200(OK), 201(Created), 202(Accepted), 204 (No Content). 277 if resp.StatusCode < 200 || resp.StatusCode > 299 { 278 return fmt.Errorf("HTTP status code %d", resp.StatusCode) 279 } 280 return nil 281 } 282 283 func decodeVersion(reader io.Reader) (ver Version, err error) { 284 bs, err := io.ReadAll(reader) 285 if err != nil { 286 return ver, err 287 } 288 parts := strings.Split(strings.Trim(string(bs), "\n "), ".") 289 if len(parts) != 2 { 290 return ver, fmt.Errorf("can't parse: %s", string(bs)) 291 } 292 if ver.Major, err = strconv.ParseInt(parts[0], 10, 64); err != nil { 293 return ver, err 294 } 295 ver.Minor, err = strconv.ParseInt(parts[1], 10, 64) 296 return ver, err 297 } 298 299 func decodeToken(reader io.Reader) (string, error) { 300 bs, err := io.ReadAll(reader) 301 if err != nil { 302 return "", err 303 } 304 return strings.Trim(string(bs), `"`), nil 305 } 306 307 type token struct { 308 mux *sync.RWMutex 309 value string 310 } 311 312 func newToken() *token { return &token{mux: &sync.RWMutex{}} } 313 func (t *token) get() string { t.mux.RLock(); defer t.mux.RUnlock(); return t.value } 314 func (t *token) set(v string) { t.mux.Lock(); defer t.mux.Unlock(); t.value = v } 315 func (t *token) unset() { t.set("") } 316 func (t *token) isSet() bool { return t.get() != "" }