summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/vcsa/client
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/go/collectors/go.d.plugin/modules/vcsa/client/client.go213
-rw-r--r--src/go/collectors/go.d.plugin/modules/vcsa/client/client_test.go288
2 files changed, 501 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/vcsa/client/client.go b/src/go/collectors/go.d.plugin/modules/vcsa/client/client.go
new file mode 100644
index 000000000..64f53ff44
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/vcsa/client/client.go
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "sync"
+
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
+)
+
+// Session: https://vmware.github.io/vsphere-automation-sdk-rest/vsphere/index.html#SVC_com.vmware.cis.session
+// Health: https://vmware.github.io/vsphere-automation-sdk-rest/vsphere/index.html#SVC_com.vmware.appliance.health
+
+const (
+ pathCISSession = "/rest/com/vmware/cis/session"
+ pathHealthSystem = "/rest/appliance/health/system"
+ pathHealthSwap = "/rest/appliance/health/swap"
+ pathHealthStorage = "/rest/appliance/health/storage"
+ pathHealthSoftwarePackager = "/rest/appliance/health/software-packages"
+ pathHealthMem = "/rest/appliance/health/mem"
+ pathHealthLoad = "/rest/appliance/health/load"
+ pathHealthDatabaseStorage = "/rest/appliance/health/database-storage"
+ pathHealthApplMgmt = "/rest/appliance/health/applmgmt"
+
+ apiSessIDKey = "vmware-api-session-id"
+)
+
+type sessionToken struct {
+ m *sync.RWMutex
+ id string
+}
+
+func (s *sessionToken) set(id string) {
+ s.m.Lock()
+ defer s.m.Unlock()
+ s.id = id
+}
+
+func (s *sessionToken) get() string {
+ s.m.RLock()
+ defer s.m.RUnlock()
+ return s.id
+}
+
+func New(httpClient *http.Client, url, username, password string) *Client {
+ if httpClient == nil {
+ httpClient = &http.Client{}
+ }
+ return &Client{
+ httpClient: httpClient,
+ url: url,
+ username: username,
+ password: password,
+ token: &sessionToken{m: new(sync.RWMutex)},
+ }
+}
+
+type Client struct {
+ httpClient *http.Client
+
+ url string
+ username string
+ password string
+
+ token *sessionToken
+}
+
+// Login creates a session with the API. This operation exchanges user credentials supplied in the security context
+// for a session identifier that is to be used for authenticating subsequent calls.
+func (c *Client) Login() error {
+ req := web.Request{
+ URL: fmt.Sprintf("%s%s", c.url, pathCISSession),
+ Username: c.username,
+ Password: c.password,
+ Method: http.MethodPost,
+ }
+ s := struct{ Value string }{}
+
+ err := c.doOKWithDecode(req, &s)
+ if err == nil {
+ c.token.set(s.Value)
+ }
+ return err
+}
+
+// Logout terminates the validity of a session token.
+func (c *Client) Logout() error {
+ req := web.Request{
+ URL: fmt.Sprintf("%s%s", c.url, pathCISSession),
+ Method: http.MethodDelete,
+ Headers: map[string]string{apiSessIDKey: c.token.get()},
+ }
+
+ resp, err := c.doOK(req)
+ closeBody(resp)
+ c.token.set("")
+ return err
+}
+
+// Ping sent a request to VCSA server to ensure the link is operating.
+// In case of 401 error Ping tries to re authenticate.
+func (c *Client) Ping() error {
+ req := web.Request{
+ URL: fmt.Sprintf("%s%s?~action=get", c.url, pathCISSession),
+ Method: http.MethodPost,
+ Headers: map[string]string{apiSessIDKey: c.token.get()},
+ }
+ resp, err := c.doOK(req)
+ defer closeBody(resp)
+ if resp != nil && resp.StatusCode == http.StatusUnauthorized {
+ return c.Login()
+ }
+ return err
+}
+
+func (c *Client) health(urlPath string) (string, error) {
+ req := web.Request{
+ URL: fmt.Sprintf("%s%s", c.url, urlPath),
+ Headers: map[string]string{apiSessIDKey: c.token.get()},
+ }
+ s := struct{ Value string }{}
+ err := c.doOKWithDecode(req, &s)
+ return s.Value, err
+}
+
+// ApplMgmt provides health status of applmgmt services.
+func (c *Client) ApplMgmt() (string, error) {
+ return c.health(pathHealthApplMgmt)
+}
+
+// DatabaseStorage provides health status of database storage health.
+func (c *Client) DatabaseStorage() (string, error) {
+ return c.health(pathHealthDatabaseStorage)
+}
+
+// Load provides health status of load health.
+func (c *Client) Load() (string, error) {
+ return c.health(pathHealthLoad)
+}
+
+// Mem provides health status of memory health.
+func (c *Client) Mem() (string, error) {
+ return c.health(pathHealthMem)
+}
+
+// SoftwarePackages provides information on available software updates available in remote VUM repository.
+// Red indicates that security updates are available.
+// Orange indicates that non-security updates are available.
+// Green indicates that there are no updates available.
+// Gray indicates that there was an error retrieving information on software updates.
+func (c *Client) SoftwarePackages() (string, error) {
+ return c.health(pathHealthSoftwarePackager)
+}
+
+// Storage provides health status of storage health.
+func (c *Client) Storage() (string, error) {
+ return c.health(pathHealthStorage)
+}
+
+// Swap provides health status of swap health.
+func (c *Client) Swap() (string, error) {
+ return c.health(pathHealthSwap)
+}
+
+// System provides overall health of system.
+func (c *Client) System() (string, error) {
+ return c.health(pathHealthSystem)
+}
+
+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 resp.StatusCode != http.StatusOK {
+ return resp, fmt.Errorf("%s returned %d", req.URL, resp.StatusCode)
+ }
+ return resp, nil
+}
+
+func (c *Client) doOKWithDecode(req web.Request, dst interface{}) error {
+ resp, err := c.doOK(req)
+ defer closeBody(resp)
+ if err != nil {
+ return err
+ }
+
+ err = json.NewDecoder(resp.Body).Decode(dst)
+ if err != nil {
+ return fmt.Errorf("error on decoding response from %s : %v", req.URL, err)
+ }
+ return nil
+}
+
+func closeBody(resp *http.Response) {
+ if resp != nil && resp.Body != nil {
+ _, _ = io.Copy(io.Discard, resp.Body)
+ _ = resp.Body.Close()
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/vcsa/client/client_test.go b/src/go/collectors/go.d.plugin/modules/vcsa/client/client_test.go
new file mode 100644
index 000000000..379644b89
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/vcsa/client/client_test.go
@@ -0,0 +1,288 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package client
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ testUser = "user"
+ testPass = "pass"
+ testSessToken = "sessToken"
+ testHealthValue = "green"
+)
+
+func newTestClient(srvURL string) *Client {
+ return New(nil, srvURL, testUser, testPass)
+}
+
+func TestClient_Login(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ assert.NoError(t, cl.Login())
+ assert.Equal(t, testSessToken, cl.token.get())
+}
+
+func TestClient_LoginWrongCredentials(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+ cl.username += "!"
+
+ assert.Error(t, cl.Login())
+}
+
+func TestClient_Logout(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ assert.NoError(t, cl.Login())
+ assert.NoError(t, cl.Logout())
+ assert.Zero(t, cl.token.get())
+}
+
+func TestClient_Ping(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ assert.NoError(t, cl.Ping())
+}
+
+func TestClient_PingWithReAuthentication(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ cl.token.set("")
+ assert.NoError(t, cl.Ping())
+ assert.Equal(t, testSessToken, cl.token.get())
+}
+
+func TestClient_ApplMgmt(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.ApplMgmt()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_DatabaseStorage(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.DatabaseStorage()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_Load(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.Load()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_Mem(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.Mem()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_SoftwarePackages(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.SoftwarePackages()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_Storage(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.Storage()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_Swap(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.Swap()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_System(t *testing.T) {
+ ts := newTestHTTPServer()
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ require.NoError(t, cl.Login())
+ v, err := cl.System()
+ assert.NoError(t, err)
+ assert.Equal(t, testHealthValue, v)
+}
+
+func TestClient_InvalidDataOnLogin(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ _, _ = w.Write([]byte("hello\n and goodbye!"))
+ }))
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ assert.Error(t, cl.Login())
+}
+
+func TestClient_404OnLogin(t *testing.T) {
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(404)
+ }))
+ defer ts.Close()
+ cl := newTestClient(ts.URL)
+
+ assert.Error(t, cl.Login())
+}
+
+func newTestHTTPServer() *httptest.Server {
+ return httptest.NewServer(&mockVCSAServer{
+ username: testUser,
+ password: testPass,
+ sessionID: testSessToken,
+ })
+}
+
+type mockVCSAServer struct {
+ username string
+ password string
+ sessionID string
+}
+
+func (m mockVCSAServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ default:
+ w.WriteHeader(http.StatusNotFound)
+ case pathCISSession:
+ m.handleSession(w, r)
+ case
+ pathHealthApplMgmt,
+ pathHealthDatabaseStorage,
+ pathHealthLoad,
+ pathHealthMem,
+ pathHealthSoftwarePackager,
+ pathHealthStorage,
+ pathHealthSwap,
+ pathHealthSystem:
+ m.handleHealth(w, r)
+ }
+}
+
+func (m mockVCSAServer) handleHealth(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ if !m.isSessionAuthenticated(r) {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ s := struct{ Value string }{Value: testHealthValue}
+ b, _ := json.Marshal(s)
+ _, _ = w.Write(b)
+}
+
+func (m mockVCSAServer) handleSession(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ default:
+ w.WriteHeader(http.StatusBadRequest)
+ case http.MethodDelete:
+ m.handleSessionDelete(w, r)
+ case http.MethodPost:
+ if r.URL.RawQuery == "" {
+ m.handleSessionCreate(w, r)
+ } else {
+ m.handleSessionGet(w, r)
+ }
+ }
+}
+
+func (m mockVCSAServer) handleSessionCreate(w http.ResponseWriter, r *http.Request) {
+ if !m.isReqAuthenticated(r) {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ s := struct{ Value string }{Value: m.sessionID}
+ b, _ := json.Marshal(s)
+ _, _ = w.Write(b)
+}
+
+func (m mockVCSAServer) handleSessionGet(w http.ResponseWriter, r *http.Request) {
+ if !m.isSessionAuthenticated(r) {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ s := struct{ Value struct{ User string } }{Value: struct{ User string }{User: m.username}}
+ b, _ := json.Marshal(s)
+ _, _ = w.Write(b)
+}
+
+func (m mockVCSAServer) handleSessionDelete(w http.ResponseWriter, r *http.Request) {
+ if !m.isSessionAuthenticated(r) {
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+}
+
+func (m mockVCSAServer) isReqAuthenticated(r *http.Request) bool {
+ u, p, ok := r.BasicAuth()
+ return ok && m.username == u && p == m.password
+}
+
+func (m mockVCSAServer) isSessionAuthenticated(r *http.Request) bool {
+ return r.Header.Get(apiSessIDKey) == m.sessionID
+}