diff options
Diffstat (limited to '')
-rw-r--r-- | src/go/collectors/go.d.plugin/modules/vcsa/client/client.go | 213 | ||||
-rw-r--r-- | src/go/collectors/go.d.plugin/modules/vcsa/client/client_test.go | 288 |
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 +} |