summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/scaleio/client/client.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/go/collectors/go.d.plugin/modules/scaleio/client/client.go316
1 files changed, 316 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/scaleio/client/client.go b/src/go/collectors/go.d.plugin/modules/scaleio/client/client.go
new file mode 100644
index 000000000..c75f79aa7
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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/go.d.plugin/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() != "" }