diff options
Diffstat (limited to '')
-rw-r--r-- | src/go/plugin/go.d/modules/scaleio/client/client.go | 316 |
1 files changed, 316 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/scaleio/client/client.go b/src/go/plugin/go.d/modules/scaleio/client/client.go new file mode 100644 index 000000000..698b2d174 --- /dev/null +++ b/src/go/plugin/go.d/modules/scaleio/client/client.go @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package client + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strconv" + "strings" + "sync" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" +) + +/* +The REST API is served from the VxFlex OS Gateway. +The FxFlex Gateway connects to a single MDM and serves requests by querying the MDM +and reformatting the answers it receives from the MDM in s RESTful manner, back to a REST API. +The Gateway is stateless. It requires the MDM username and password for the login requests. +The login returns a token in the response, that is used for later authentication for other requests. + +The token is valid for 8 hours from the time it was created, unless there has been no activity +for 10 minutes, of if the client has sent a logout request. + +General URI: +- /api/login +- /api/logout +- /api/version +- /api/instances/ // GET all instances +- /api/types/{type}/instances // POST (create) / GET all objects for a given type +- /api/instances/{type::id} // GET by ID +- /api/instances/{type::id}/relationships/{Relationship name} // GET +- /api/instances/querySelectedStatistics // POST Query selected statistics +- /api/instances/{type::id}/action/{actionName} // POST a special action on an object +- /api/types/{type}/instances/action/{actionName} // POST a special action on a given type + +Types: +- System +- Sds +- StoragePool +- ProtectionDomain +- Device +- Volume +- VTree +- Sdc +- User +- FaultSet +- RfcacheDevice +- Alerts + +Actions: +- querySelectedStatistics // All types except Alarm and User +- querySystemLimits // System +- queryDisconnectedSdss // Sds +- querySdsNetworkLatencyMeters // Sds +- queryFailedDevices" // Device. Note: works strange! + +Relationships: +- Statistics // All types except Alarm and User +- ProtectionDomain // System +- Sdc // System +- User // System +- StoragePool // ProtectionDomain +- FaultSet // ProtectionDomain +- Sds // ProtectionDomain +- RfcacheDevice // Sds +- Device // Sds, StoragePool +- Volume // Sdc, StoragePool +- VTree // StoragePool +*/ + +// New creates new ScaleIO client. +func New(client web.Client, request web.Request) (*Client, error) { + httpClient, err := web.NewHTTPClient(client) + if err != nil { + return nil, err + } + return &Client{ + Request: request, + httpClient: httpClient, + token: newToken(), + }, nil +} + +// Client represents ScaleIO client. +type Client struct { + Request web.Request + httpClient *http.Client + token *token +} + +// LoggedIn reports whether the client is logged in. +func (c *Client) LoggedIn() bool { + return c.token.isSet() +} + +// Login connects to FxFlex Gateway to get the token that is used for later authentication for other requests. +func (c *Client) Login() error { + if c.LoggedIn() { + _ = c.Logout() + } + req := c.createLoginRequest() + resp, err := c.doOK(req) + defer closeBody(resp) + if err != nil { + return err + } + + token, err := decodeToken(resp.Body) + if err != nil { + return err + } + + c.token.set(token) + return nil +} + +// Logout sends logout request and unsets token. +func (c *Client) Logout() error { + if !c.LoggedIn() { + return nil + } + req := c.createLogoutRequest() + c.token.unset() + + resp, err := c.do(req) + defer closeBody(resp) + return err +} + +// APIVersion returns FxFlex Gateway API version. +func (c *Client) APIVersion() (Version, error) { + req := c.createAPIVersionRequest() + resp, err := c.doOK(req) + defer closeBody(resp) + if err != nil { + return Version{}, err + } + return decodeVersion(resp.Body) +} + +// SelectedStatistics returns selected statistics. +func (c *Client) SelectedStatistics(query SelectedStatisticsQuery) (SelectedStatistics, error) { + b, _ := json.Marshal(query) + req := c.createSelectedStatisticsRequest(b) + var stats SelectedStatistics + err := c.doJSONWithRetry(&stats, req) + return stats, err +} + +// Instances returns all instances. +func (c *Client) Instances() (Instances, error) { + req := c.createInstancesRequest() + var instances Instances + err := c.doJSONWithRetry(&instances, req) + return instances, err +} + +func (c *Client) createLoginRequest() web.Request { + req := c.Request.Copy() + u, _ := url.Parse(req.URL) + u.Path = path.Join(u.Path, "/api/login") + req.URL = u.String() + return req +} + +func (c *Client) createLogoutRequest() web.Request { + req := c.Request.Copy() + u, _ := url.Parse(req.URL) + u.Path = path.Join(u.Path, "/api/logout") + req.URL = u.String() + req.Password = c.token.get() + return req +} + +func (c *Client) createAPIVersionRequest() web.Request { + req := c.Request.Copy() + u, _ := url.Parse(req.URL) + u.Path = path.Join(u.Path, "/api/version") + req.URL = u.String() + req.Password = c.token.get() + return req +} + +func (c *Client) createSelectedStatisticsRequest(query []byte) web.Request { + req := c.Request.Copy() + u, _ := url.Parse(req.URL) + u.Path = path.Join(u.Path, "/api/instances/querySelectedStatistics") + req.URL = u.String() + req.Password = c.token.get() + req.Method = http.MethodPost + req.Headers = map[string]string{ + "Content-Type": "application/json", + } + req.Body = string(query) + return req +} + +func (c *Client) createInstancesRequest() web.Request { + req := c.Request.Copy() + u, _ := url.Parse(req.URL) + u.Path = path.Join(u.Path, "/api/instances") + req.URL = u.String() + req.Password = c.token.get() + return req +} + +func (c *Client) do(req web.Request) (*http.Response, error) { + httpReq, err := web.NewHTTPRequest(req) + if err != nil { + return nil, fmt.Errorf("error on creating http request to %s: %v", req.URL, err) + } + return c.httpClient.Do(httpReq) +} + +func (c *Client) doOK(req web.Request) (*http.Response, error) { + resp, err := c.do(req) + if err != nil { + return nil, err + } + if err = checkStatusCode(resp); err != nil { + err = fmt.Errorf("%s returned %v", req.URL, err) + } + return resp, err +} + +func (c *Client) doOKWithRetry(req web.Request) (*http.Response, error) { + resp, err := c.do(req) + if err != nil { + return nil, err + } + if resp.StatusCode == http.StatusUnauthorized { + if err = c.Login(); err != nil { + return resp, err + } + req.Password = c.token.get() + return c.doOK(req) + } + if err = checkStatusCode(resp); err != nil { + err = fmt.Errorf("%s returned %v", req.URL, err) + } + return resp, err +} + +func (c *Client) doJSONWithRetry(dst interface{}, req web.Request) error { + resp, err := c.doOKWithRetry(req) + defer closeBody(resp) + if err != nil { + return err + } + return json.NewDecoder(resp.Body).Decode(dst) +} + +func closeBody(resp *http.Response) { + if resp != nil && resp.Body != nil { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } +} + +func checkStatusCode(resp *http.Response) error { + // For all 4xx and 5xx return codes, the body may contain an apiError + // instance with more specifics about the failure. + if resp.StatusCode >= 400 { + e := error(&apiError{}) + if err := json.NewDecoder(resp.Body).Decode(e); err != nil { + e = err + } + return fmt.Errorf("HTTP status code %d : %v", resp.StatusCode, e) + } + + // 200(OK), 201(Created), 202(Accepted), 204 (No Content). + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("HTTP status code %d", resp.StatusCode) + } + return nil +} + +func decodeVersion(reader io.Reader) (ver Version, err error) { + bs, err := io.ReadAll(reader) + if err != nil { + return ver, err + } + parts := strings.Split(strings.Trim(string(bs), "\n "), ".") + if len(parts) != 2 { + return ver, fmt.Errorf("can't parse: %s", string(bs)) + } + if ver.Major, err = strconv.ParseInt(parts[0], 10, 64); err != nil { + return ver, err + } + ver.Minor, err = strconv.ParseInt(parts[1], 10, 64) + return ver, err +} + +func decodeToken(reader io.Reader) (string, error) { + bs, err := io.ReadAll(reader) + if err != nil { + return "", err + } + return strings.Trim(string(bs), `"`), nil +} + +type token struct { + mux *sync.RWMutex + value string +} + +func newToken() *token { return &token{mux: &sync.RWMutex{}} } +func (t *token) get() string { t.mux.RLock(); defer t.mux.RUnlock(); return t.value } +func (t *token) set(v string) { t.mux.Lock(); defer t.mux.Unlock(); t.value = v } +func (t *token) unset() { t.set("") } +func (t *token) isSet() bool { return t.get() != "" } |