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