| 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() != "" } |