diff --git a/go.mod b/go.mod index daf8fc3..9f41dd4 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/thoas/go-funk v0.9.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect diff --git a/go.sum b/go.sum index 6a11366..827740f 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -62,6 +63,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= +github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= @@ -124,6 +127,7 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/api.go b/internal/api/api.go index 6292e8a..d31922f 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,26 +1,35 @@ package api -//go:generate mockgen -source api.go -package test -destination ../../test/api.go - import ( "github.com/go-resty/resty/v2" ) +// The VSphereProxyApi interface describes the API available to the endpoints type VSphereProxyApi interface { // GetSession returns the vmware session id to be used by other requests GetSession(username string, password string) (string, error) // GetVMs returns all VMs from the VM endpoint - GetVMs(username string, password string) ([]VMResponse, error) + GetVMs(username string, password string) ([]VM, error) // GetVMTags retrieves a list of tags associated with the given vm GetVMTags(username string, password string, VMID string) ([]VMTag, error) // GetFQDN uses the VMware guest tools to get the fqdn of a VM (if possible) GetFQDN(username string, password string, VMID string) (string, error) + + // GetHosts retrieves a list of ESXi hosts from the vCenter + GetHosts(username string, password string) ([]Host, error) + + // GetDatastores retrieves a list of datastores from the vCenter + GetDatastores(username string, password string) ([]Datastore, error) + + // GetVMInfo creates a statistical overview of the given vm + GetVMInfo(username string, password string, VMID string) (VMInfo, error) } +// DefaultVSphereProxyApi is the default API implementation type DefaultVSphereProxyApi struct { Resty *resty.Client } diff --git a/internal/api/datastores.go b/internal/api/datastores.go new file mode 100644 index 0000000..4efd3e3 --- /dev/null +++ b/internal/api/datastores.go @@ -0,0 +1,30 @@ +package api + +import ( + "fmt" + "github.com/sirupsen/logrus" +) + +func (d DefaultVSphereProxyApi) GetDatastores(username string, password string) ([]Datastore, error) { + if s, err := d.GetSession(username, password); err != nil { + return []Datastore{}, err + } else { + logrus.Debugf("Fetching all datastores from %s for %s", d.Resty.BaseURL, username) + var datastores []Datastore + if r, err := d.Resty. + R(). + SetHeader("vmware-api-session-id", s). + SetResult(&datastores). + Get("/api/vcenter/datastore"); err != nil { + logrus.Errorf("Error fetching datastores: %s", err) + return []Datastore{}, err + } else { + if r.IsError() { + err := fmt.Errorf("error getting datastores (%s): %s", r.Status(), r.Body()) + logrus.Error(err) + return []Datastore{}, err + } + return datastores, nil + } + } +} diff --git a/internal/api/hosts.go b/internal/api/hosts.go new file mode 100644 index 0000000..a494b34 --- /dev/null +++ b/internal/api/hosts.go @@ -0,0 +1,30 @@ +package api + +import ( + "fmt" + "github.com/sirupsen/logrus" +) + +func (d DefaultVSphereProxyApi) GetHosts(username string, password string) ([]Host, error) { + if s, err := d.GetSession(username, password); err != nil { + return []Host{}, err + } else { + logrus.Debugf("Fetching all hosts from %s for %s", d.Resty.BaseURL, username) + var hostsResponse []Host + if r, err := d.Resty. + R(). + SetHeader("vmware-api-session-id", s). + SetResult(&hostsResponse). + Get("/api/vcenter/host"); err != nil { + logrus.Errorf("Error fetching hosts: %s", err) + return []Host{}, err + } else { + if r.IsError() { + err := fmt.Errorf("error getting hosts (%s): %s", r.Status(), r.Body()) + logrus.Error(err) + return []Host{}, err + } + return hostsResponse, nil + } + } +} diff --git a/internal/api/models.go b/internal/api/models.go new file mode 100644 index 0000000..46dc5d8 --- /dev/null +++ b/internal/api/models.go @@ -0,0 +1,41 @@ +package api + +// VM represents a virtual machine in the vCenter as described in https://developer.vmware.com/apis/vsphere-automation/v8.0U1/vcenter/data-structures/VM/Summary/ +type VM struct { + VM string `json:"vm"` + Name string `json:"name"` + PowerState string `json:"power_state"` +} + +// VMTag holds a tag from vSphere +type VMTag struct { + // Value holds the value of the tag + Value string `json:"value"` + // Category holds the tag category + Category string `json:"category"` +} + +// Host represents a host in the vCenter as described in https://developer.vmware.com/apis/vsphere-automation/v8.0U1/vcenter/data-structures/Host/Summary/ +type Host struct { + Host string `json:"host"` + Name string `json:"name"` + PowerState string `json:"power_state"` + ConnectionState string `json:"connection_state"` +} + +// Datastore represents a host in the vCenter as described in https://developer.vmware.com/apis/vsphere-automation/v8.0U1/vcenter/data-structures/Datastore/Summary/ +type Datastore struct { + Datastore string `json:"datastore"` + Name string `json:"name"` + Type string `json:"type"` + Capacity int `json:"capacity"` + FreeSpace int `json:"free_space"` +} + +type VMInfo struct { + Name string `json:"name"` + CPUCores int `json:"cpu_cores"` + ProvisionedRAM int `json:"provisioned_ram"` + ProvisionedStorage int `json:"provisioned_storage"` + UsedStorage int `json:"used_storage"` +} diff --git a/internal/api/vms.go b/internal/api/vms.go index 60246f4..7f8e536 100644 --- a/internal/api/vms.go +++ b/internal/api/vms.go @@ -4,45 +4,34 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/thoas/go-funk" ) -// VMResponse is the value in response from the VM endpoint -type VMResponse struct { - VM string `json:"vm"` - Name string `json:"name"` -} - // GetVMs returns all VMs from the VM endpoint -func (d DefaultVSphereProxyApi) GetVMs(username string, password string) ([]VMResponse, error) { +func (d DefaultVSphereProxyApi) GetVMs(username string, password string) ([]VM, error) { if s, err := d.GetSession(username, password); err != nil { - return []VMResponse{}, err + return []VM{}, err } else { logrus.Debugf("Fetching all VMs from %s for %s", d.Resty.BaseURL, username) - var vmsResponse []VMResponse + var vmsResponse []VM if r, err := d.Resty. R(). SetHeader("vmware-api-session-id", s). SetResult(&vmsResponse). Get("/api/vcenter/vm"); err != nil { logrus.Errorf("Error fetching VMs: %s", err) - return []VMResponse{}, err + return []VM{}, err } else { if r.IsError() { err := fmt.Errorf("error getting vms (%s): %s", r.Status(), r.Body()) logrus.Error(err) - return []VMResponse{}, err + return []VM{}, err } return vmsResponse, nil } } } -// VMTag holds a tag from vSphere -type VMTag struct { - Value string `json:"value"` - Category string `json:"category"` -} - // GetVMTags retrieves a list of tags associated with the given vm func (d DefaultVSphereProxyApi) GetVMTags(username string, password string, VMID string) ([]VMTag, error) { var tags []VMTag @@ -157,3 +146,61 @@ func (d DefaultVSphereProxyApi) GetFQDN(username string, password string, VMID s } } } + +func (d DefaultVSphereProxyApi) GetVMInfo(username string, password string, VMID string) (VMInfo, error) { + v := VMInfo{} + if s, err := d.GetSession(username, password); err != nil { + return v, err + } else { + logrus.Debugf("Getting basic information for vm %s from %s for %s", VMID, d.Resty.BaseURL, username) + var vR struct { + Name string `json:"name"` + CPU struct { + CoresPerSocket int `json:"cores_per_socket"` + Count int `json:"count"` + } `json:"cpu"` + Memory struct { + SizeMiB int `json:"size_MiB"` + } `json:"memory"` + } + if r, err := d.Resty. + R(). + SetHeader("vmware-api-session-id", s). + SetResult(&vR). + SetPathParam("vm", VMID). + Get("/api/vcenter/vm/{vm}"); err != nil { + logrus.Error(err) + return v, err + } else { + if r.IsError() { + return v, fmt.Errorf("can not get vm information (%s): %s", r.Status(), r.Body()) + } + v.Name = vR.Name + v.CPUCores = vR.CPU.CoresPerSocket * vR.CPU.Count + v.ProvisionedRAM = vR.Memory.SizeMiB + + logrus.Debugf("Getting local filesystem information for vm %s from %s for %s", VMID, d.Resty.BaseURL, username) + type fs struct { + FreeSpace int `json:"free_space"` + Capacity int `json:"capacity"` + } + var fR map[string]fs + if r, err := d.Resty. + R(). + SetHeader("vmware-api-session-id", s). + SetResult(&fR). + SetPathParam("vm", VMID). + Get("/api/vcenter/vm/{vm}/guest/local-filesystem"); err != nil { + logrus.Error(err) + return v, err + } else { + if r.IsError() { + return v, fmt.Errorf("can not get info about local filesystems (%s): %s", r.Status(), r.Body()) + } + v.ProvisionedStorage = funk.Reduce(funk.Values(fR), func(acc int, f fs) int { return acc + f.Capacity }, 0).(int) + v.UsedStorage = funk.Reduce(funk.Values(fR), func(acc int, f fs) int { return acc + f.Capacity - f.FreeSpace }, 0).(int) + return v, nil + } + } + } +} diff --git a/internal/endpoints/datastores.go b/internal/endpoints/datastores.go new file mode 100644 index 0000000..2c43531 --- /dev/null +++ b/internal/endpoints/datastores.go @@ -0,0 +1,32 @@ +package endpoints + +import ( + "fmt" + "github.com/gin-gonic/gin" + "vmware-rest-proxy/internal/api" +) + +type DataStoreEndpoint struct { + API api.VSphereProxyApi +} + +func (d DataStoreEndpoint) Register(engine *gin.Engine) { + engine.GET("/datastores", d.getDatastores) +} + +func (d DataStoreEndpoint) getDatastores(context *gin.Context) { + if r, ok := HandleRequest(context); ok { + if datastores, err := d.API.GetDatastores(r.Username, r.Password); err != nil { + context.AbortWithStatusJSON(500, gin.H{ + "error": fmt.Sprintf("Error getting datastores: %s", err), + }) + } else { + context.JSON(200, gin.H{ + "datastores": gin.H{ + "count": len(datastores), + "datastores": datastores, + }, + }) + } + } +} diff --git a/internal/endpoints/hosts.go b/internal/endpoints/hosts.go new file mode 100644 index 0000000..59a5e52 --- /dev/null +++ b/internal/endpoints/hosts.go @@ -0,0 +1,32 @@ +package endpoints + +import ( + "fmt" + "github.com/gin-gonic/gin" + "vmware-rest-proxy/internal/api" +) + +type HostsEndpoint struct { + API api.VSphereProxyApi +} + +func (H HostsEndpoint) Register(engine *gin.Engine) { + engine.GET("/hosts", H.getHosts) +} + +func (H HostsEndpoint) getHosts(context *gin.Context) { + if r, ok := HandleRequest(context); ok { + if hosts, err := H.API.GetHosts(r.Username, r.Password); err != nil { + context.AbortWithStatusJSON(500, gin.H{ + "error": fmt.Sprintf("Error getting hosts: %s", err), + }) + } else { + context.JSON(200, gin.H{ + "hosts": gin.H{ + "count": len(hosts), + "hosts": hosts, + }, + }) + } + } +} diff --git a/internal/endpoints/vms.go b/internal/endpoints/vms.go index 8dd6090..31f094d 100644 --- a/internal/endpoints/vms.go +++ b/internal/endpoints/vms.go @@ -22,6 +22,7 @@ func (V *VMSEndpoint) Register(engine *gin.Engine) { engine.GET("/vms", V.getVMS) engine.GET("/vms/:vm/tags", V.getVMTags) engine.GET("/vms/:vm/fqdn", V.getFQDN) + engine.GET("/vms/:vm/info", V.getVMInfo) } // getVMS exposes all VMS of the vCenter at /VMS @@ -87,3 +88,22 @@ func (V *VMSEndpoint) getFQDN(context *gin.Context) { } } } + +func (V *VMSEndpoint) getVMInfo(context *gin.Context) { + if r, ok := HandleRequest(context); ok { + var vm VMBinding + if err := context.ShouldBindUri(&vm); err != nil { + context.AbortWithStatusJSON(400, gin.H{ + "error": fmt.Sprintf("Missing VM id in path: %s", err), + }) + return + } + if vmInfo, err := V.API.GetVMInfo(r.Username, r.Password, vm.ID); err != nil { + context.AbortWithStatusJSON(500, gin.H{ + "error": fmt.Sprintf("Error getting vm info: %s", err), + }) + } else { + context.JSON(200, vmInfo) + } + } +} diff --git a/internal/endpoints_test/datastores_test.go b/internal/endpoints_test/datastores_test.go new file mode 100644 index 0000000..3b5d012 --- /dev/null +++ b/internal/endpoints_test/datastores_test.go @@ -0,0 +1,56 @@ +package endpoints_test + +import ( + "encoding/json" + "github.com/dodevops/golang-handlerinspector/pkg/builder" + "github.com/dodevops/golang-handlerinspector/pkg/inspector" + "github.com/go-playground/assert/v2" + "net/http" + "testing" + "vmware-rest-proxy/internal/api" + "vmware-rest-proxy/test" +) + +func TestDatastoresEndpoint_GetDatastores(t *testing.T) { + b := builder.NewBuilder(). + WithRule(test.SessionRule). + WithRule( + builder.NewRule("datastores"). + WithCondition(builder.HasPath("/api/vcenter/datastore")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). + ReturnBody(`[{"datastore": "1", "name": "test1", "type": "VMFS", "capacity": 100, "free_space": 0}, {"datastore": "2", "name": "test2", "type": "NFS", "capacity": 1000, "free_space": 10}]`). + ReturnHeader("Content-Type", "application/json"). + Build(), + ) + req, _ := http.NewRequest("GET", "/datastores", nil) + req.SetBasicAuth("test", "test") + w := test.TestRequests(b.Build(), []*http.Request{req}) + + type resp struct { + Datastores struct { + Count int `json:"count"` + Datastores []api.Datastore `json:"datastores"` + } `json:"datastores"` + } + var r resp + err := json.NewDecoder(w.Body).Decode(&r) + assert.Equal(t, err, nil) + + i := inspector.NewInspector(b) + assert.Equal(t, i.Failed(), false) + assert.Equal(t, i.AllWereCalled(), true) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, r.Datastores.Count, 2) + assert.Equal(t, len(r.Datastores.Datastores), 2) + assert.Equal(t, r.Datastores.Datastores[0].Datastore, "1") + assert.Equal(t, r.Datastores.Datastores[0].Name, "test1") + assert.Equal(t, r.Datastores.Datastores[0].Type, "VMFS") + assert.Equal(t, r.Datastores.Datastores[0].Capacity, 100) + assert.Equal(t, r.Datastores.Datastores[0].FreeSpace, 0) + assert.Equal(t, r.Datastores.Datastores[1].Datastore, "2") + assert.Equal(t, r.Datastores.Datastores[1].Name, "test2") + assert.Equal(t, r.Datastores.Datastores[1].Type, "NFS") + assert.Equal(t, r.Datastores.Datastores[1].Capacity, 1000) + assert.Equal(t, r.Datastores.Datastores[1].FreeSpace, 10) +} diff --git a/internal/endpoints_test/hosts_test.go b/internal/endpoints_test/hosts_test.go new file mode 100644 index 0000000..4011575 --- /dev/null +++ b/internal/endpoints_test/hosts_test.go @@ -0,0 +1,55 @@ +package endpoints_test + +import ( + "encoding/json" + "github.com/dodevops/golang-handlerinspector/pkg/builder" + "github.com/dodevops/golang-handlerinspector/pkg/inspector" + "github.com/go-playground/assert/v2" + "net/http" + "testing" + "vmware-rest-proxy/internal/api" + "vmware-rest-proxy/test" +) + +// TestHostsEndpoint_GetVMS checks the hosts endpoint +func TestHostsEndpoint_GetHosts(t *testing.T) { + b := builder.NewBuilder(). + WithRule(test.SessionRule). + WithRule( + builder.NewRule("hosts"). + WithCondition(builder.HasPath("/api/vcenter/host")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). + ReturnBody(`[{"host": "1", "name": "test1", "power_state": "POWERED_OFF", "connection_state": "CONNECTED"}, {"host": "2", "name": "test2", "power_state": "POWERED_ON", "connection_state": "DISCONNECTED"}]`). + ReturnHeader("Content-Type", "application/json"). + Build(), + ) + req, _ := http.NewRequest("GET", "/hosts", nil) + req.SetBasicAuth("test", "test") + w := test.TestRequests(b.Build(), []*http.Request{req}) + + type resp struct { + Hosts struct { + Count int `json:"count"` + Hosts []api.Host `json:"hosts"` + } `json:"hosts"` + } + var r resp + err := json.NewDecoder(w.Body).Decode(&r) + assert.Equal(t, err, nil) + + i := inspector.NewInspector(b) + assert.Equal(t, i.Failed(), false) + assert.Equal(t, i.AllWereCalled(), true) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, r.Hosts.Count, 2) + assert.Equal(t, len(r.Hosts.Hosts), 2) + assert.Equal(t, r.Hosts.Hosts[0].Host, "1") + assert.Equal(t, r.Hosts.Hosts[0].Name, "test1") + assert.Equal(t, r.Hosts.Hosts[0].PowerState, "POWERED_OFF") + assert.Equal(t, r.Hosts.Hosts[0].ConnectionState, "CONNECTED") + assert.Equal(t, r.Hosts.Hosts[1].Host, "2") + assert.Equal(t, r.Hosts.Hosts[1].Name, "test2") + assert.Equal(t, r.Hosts.Hosts[1].PowerState, "POWERED_ON") + assert.Equal(t, r.Hosts.Hosts[1].ConnectionState, "DISCONNECTED") +} diff --git a/internal/endpoints/status_test.go b/internal/endpoints_test/status_test.go similarity index 87% rename from internal/endpoints/status_test.go rename to internal/endpoints_test/status_test.go index 42ceb1d..3bf35a5 100644 --- a/internal/endpoints/status_test.go +++ b/internal/endpoints_test/status_test.go @@ -7,12 +7,13 @@ import ( "net/http" "net/http/httptest" "testing" + "vmware-rest-proxy/internal/endpoints" ) // TestStatusEndpoint tests the status endpoint func TestStatusEndpoint(t *testing.T) { r := gin.Default() - s := StatusEndpoint{} + s := endpoints.StatusEndpoint{} s.Register(r) req, _ := http.NewRequest("GET", "/status", nil) w := httptest.NewRecorder() diff --git a/internal/endpoints/vms_test.go b/internal/endpoints_test/vms_test.go similarity index 63% rename from internal/endpoints/vms_test.go rename to internal/endpoints_test/vms_test.go index 86ab618..a9eb9e8 100644 --- a/internal/endpoints/vms_test.go +++ b/internal/endpoints_test/vms_test.go @@ -2,64 +2,24 @@ package endpoints import ( "encoding/json" - "fmt" "github.com/dodevops/golang-handlerinspector/pkg/builder" "github.com/dodevops/golang-handlerinspector/pkg/inspector" - "github.com/gin-gonic/gin" "github.com/go-playground/assert/v2" - "github.com/go-resty/resty/v2" "net/http" - "net/http/httptest" "testing" "vmware-rest-proxy/internal/api" + "vmware-rest-proxy/test" ) -// AUTHTOKEN holds a test token that should be issued and used in all tests -const AUTHTOKEN = "testtoken" - -// sessionRule holds a builder Rule for the session api -var sessionRule = builder.NewRule("session"). - WithCondition(builder.HasPath("/api/session")). - ReturnBodyFromFunction(func(r *http.Request) string { - if r.Method == "POST" { - return fmt.Sprintf(`"%s"`, AUTHTOKEN) - } else { - return `{"user": "test"}` - } - }). - ReturnHeader("Content-Type", "application/json"). - Build() - -// testRequests is a short helper function to call requests on the build-up endpoints and mock server -// requires a http.Handler and a list of http.Request objects -func testRequests(handler http.Handler, requests []*http.Request) *httptest.ResponseRecorder { - s := httptest.NewServer(handler) - defer s.Close() - - r := resty.New().SetBaseURL(s.URL).SetBasicAuth("test", "test") - a := api.DefaultVSphereProxyApi{Resty: r} - sub := VMSEndpoint{API: a} - g := gin.Default() - sub.Register(g) - - w := httptest.NewRecorder() - - for _, request := range requests { - g.ServeHTTP(w, request) - - } - return w -} - // TestVMSEndpoint_GetSession checks if the session endpoint is called func TestVMSEndpoint_GetSession(t *testing.T) { b := builder.NewBuilder(). - WithRule(sessionRule). + WithRule(test.SessionRule). WithRule( builder.NewRule("vms"). WithCondition(builder.HasPath("/api/vcenter/vm")). WithCondition(builder.HasMethod("GET")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). ReturnBody("[]"). ReturnHeader("Content-Type", "application/json"). Build(), @@ -67,7 +27,7 @@ func TestVMSEndpoint_GetSession(t *testing.T) { req, _ := http.NewRequest("GET", "/vms", nil) req.SetBasicAuth("test", "test") - w := testRequests(b.Build(), []*http.Request{req}) + w := test.TestRequests(b.Build(), []*http.Request{req}) i := inspector.NewInspector(b) assert.Equal(t, i.Failed(), false) @@ -78,24 +38,24 @@ func TestVMSEndpoint_GetSession(t *testing.T) { // TestVMSEndpoint_GetVMS checks the vms endpoint func TestVMSEndpoint_GetVMS(t *testing.T) { b := builder.NewBuilder(). - WithRule(sessionRule). + WithRule(test.SessionRule). WithRule( builder.NewRule("vms"). WithCondition(builder.HasPath("/api/vcenter/vm")). WithCondition(builder.HasMethod("GET")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). - ReturnBody(`[{"VM": "1", "Name": "test1"}, {"VM": "2", "Name": "test2"}]`). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). + ReturnBody(`[{"vm": "1", "name": "test1", "power_state": "POWERED_OFF"}, {"vm": "2", "name": "test2", "power_state": "POWERED_ON"}]`). ReturnHeader("Content-Type", "application/json"). Build(), ) req, _ := http.NewRequest("GET", "/vms", nil) req.SetBasicAuth("test", "test") - w := testRequests(b.Build(), []*http.Request{req}) + w := test.TestRequests(b.Build(), []*http.Request{req}) type resp struct { VMS struct { - Count int `json:"count"` - VMS []api.VMResponse `json:"vms"` + Count int `json:"count"` + VMS []api.VM `json:"vms"` } `json:"vms"` } var r resp @@ -110,19 +70,21 @@ func TestVMSEndpoint_GetVMS(t *testing.T) { assert.Equal(t, len(r.VMS.VMS), 2) assert.Equal(t, r.VMS.VMS[0].VM, "1") assert.Equal(t, r.VMS.VMS[0].Name, "test1") + assert.Equal(t, r.VMS.VMS[0].PowerState, "POWERED_OFF") assert.Equal(t, r.VMS.VMS[1].VM, "2") assert.Equal(t, r.VMS.VMS[1].Name, "test2") + assert.Equal(t, r.VMS.VMS[1].PowerState, "POWERED_ON") } // TestVMSEndpoint_GetVMTags checks the /vms/tags endpoint func TestVMSEndpoint_GetVMTags(t *testing.T) { b := builder.NewBuilder(). - WithRule(sessionRule). + WithRule(test.SessionRule). WithRule( builder.NewRule("list-associated-tags"). WithCondition(builder.HasPath("/api/cis/tagging/tag-association")). WithCondition(builder.HasMethod("POST")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). WithCondition(builder.HasQueryParam("action", "list-attached-tags")). WithCondition(builder.HasBody(`{"object_id":{"id":"1","type":"VirtualMachine"}}`)). ReturnBody(`["1", "2"]`). @@ -133,7 +95,7 @@ func TestVMSEndpoint_GetVMTags(t *testing.T) { builder.NewRule("tag-data-1"). WithCondition(builder.HasPath("/api/cis/tagging/tag/1")). WithCondition(builder.HasMethod("GET")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). ReturnBody(`{"category_id": "1", "name": "testtag1"}`). ReturnHeader("Content-Type", "application/json"). Build(), @@ -142,7 +104,7 @@ func TestVMSEndpoint_GetVMTags(t *testing.T) { builder.NewRule("tag-data-2"). WithCondition(builder.HasPath("/api/cis/tagging/tag/2")). WithCondition(builder.HasMethod("GET")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). ReturnBody(`{"category_id": "2", "name": "testtag2"}`). ReturnHeader("Content-Type", "application/json"). Build(), @@ -151,7 +113,7 @@ func TestVMSEndpoint_GetVMTags(t *testing.T) { builder.NewRule("tag-category-1"). WithCondition(builder.HasPath("/api/cis/tagging/category/1")). WithCondition(builder.HasMethod("GET")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). ReturnBody(`{"name": "testcategory1"}`). ReturnHeader("Content-Type", "application/json"). Build(), @@ -160,14 +122,14 @@ func TestVMSEndpoint_GetVMTags(t *testing.T) { builder.NewRule("tag-category-2"). WithCondition(builder.HasPath("/api/cis/tagging/category/2")). WithCondition(builder.HasMethod("GET")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). ReturnBody(`{"name": "testcategory2"}`). ReturnHeader("Content-Type", "application/json"). Build(), ) req, _ := http.NewRequest("GET", "/vms/1/tags", nil) req.SetBasicAuth("test", "test") - w := testRequests(b.Build(), []*http.Request{req}) + w := test.TestRequests(b.Build(), []*http.Request{req}) type resp struct { Tags struct { @@ -194,19 +156,19 @@ func TestVMSEndpoint_GetVMTags(t *testing.T) { // TestVMSEndpoint_GetFQDN checks the vm/fqdn endpoint func TestVMSEndpoint_GetFQDN(t *testing.T) { b := builder.NewBuilder(). - WithRule(sessionRule). + WithRule(test.SessionRule). WithRule( - builder.NewRule("get-fqdm"). + builder.NewRule("get-fqdn"). WithCondition(builder.HasPath("/api/vcenter/vm/1/guest/networking")). WithCondition(builder.HasMethod("GET")). - WithCondition(builder.HasHeader("Vmware-Api-Session-Id", AUTHTOKEN)). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). ReturnBody(`{"dns_values":{"domain_name":"example.com","host_name":"test"}}`). ReturnHeader("Content-Type", "application/json"). Build(), ) req, _ := http.NewRequest("GET", "/vms/1/fqdn", nil) req.SetBasicAuth("test", "test") - w := testRequests(b.Build(), []*http.Request{req}) + w := test.TestRequests(b.Build(), []*http.Request{req}) type resp struct { FQDN string @@ -221,3 +183,44 @@ func TestVMSEndpoint_GetFQDN(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, r.FQDN, "test.example.com") } + +func TestVMSEndpoint_GetVMInfo(t *testing.T) { + b := builder.NewBuilder(). + WithRule(test.SessionRule). + WithRule( + builder.NewRule("get-vm"). + WithCondition(builder.HasPath("/api/vcenter/vm/1")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). + ReturnBody(`{"name":"test", "cpu": {"cores_per_socket": 2, "count": 2}, "memory": {"size_MiB": 200}}`). + ReturnHeader("Content-Type", "application/json"). + Build(), + ). + WithRule( + builder.NewRule("get-vm"). + WithCondition(builder.HasPath("/api/vcenter/vm/1/guest/local-filesystem")). + WithCondition(builder.HasMethod("GET")). + WithCondition(builder.HasHeader("Vmware-Api-Session-Id", test.AUTHTOKEN)). + ReturnBody(`{"/": {"capacity": 100, "free_space": 0}, "/opt": {"capacity": 2000, "free_space": 20}}`). + ReturnHeader("Content-Type", "application/json"). + Build(), + ) + + req, _ := http.NewRequest("GET", "/vms/1/info", nil) + req.SetBasicAuth("test", "test") + w := test.TestRequests(b.Build(), []*http.Request{req}) + + var r api.VMInfo + err := json.NewDecoder(w.Body).Decode(&r) + assert.Equal(t, err, nil) + + i := inspector.NewInspector(b) + assert.Equal(t, i.Failed(), false) + assert.Equal(t, i.AllWereCalled(), true) + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, r.Name, "test") + assert.Equal(t, r.CPUCores, 4) + assert.Equal(t, r.ProvisionedRAM, 200) + assert.Equal(t, r.ProvisionedStorage, 100+2000) + assert.Equal(t, r.UsedStorage, 100+2000-20) +} diff --git a/test/tools.go b/test/tools.go new file mode 100644 index 0000000..217eff5 --- /dev/null +++ b/test/tools.go @@ -0,0 +1,49 @@ +package test + +import ( + "fmt" + "github.com/dodevops/golang-handlerinspector/pkg/builder" + "github.com/gin-gonic/gin" + "github.com/go-resty/resty/v2" + "net/http" + "net/http/httptest" + "vmware-rest-proxy/internal/api" + "vmware-rest-proxy/internal/endpoints" +) + +// AUTHTOKEN holds a test token that should be issued and used in all tests +const AUTHTOKEN = "testtoken" + +// SessionRule holds a builder Rule for the session api +var SessionRule = builder.NewRule("session"). + WithCondition(builder.HasPath("/api/session")). + ReturnBodyFromFunction(func(r *http.Request) string { + if r.Method == "POST" { + return fmt.Sprintf(`"%s"`, AUTHTOKEN) + } else { + return `{"user": "test"}` + } + }). + ReturnHeader("Content-Type", "application/json"). + Build() + +// TestRequests is a short helper function to call requests on the build-up endpoints and mock server +// requires a http.Handler and a list of http.Request objects +func TestRequests(handler http.Handler, requests []*http.Request) *httptest.ResponseRecorder { + s := httptest.NewServer(handler) + defer s.Close() + + r := resty.New().SetBaseURL(s.URL).SetBasicAuth("test", "test") + a := api.DefaultVSphereProxyApi{Resty: r} + g := gin.Default() + for _, endpoint := range []endpoints.Endpoint{&endpoints.VMSEndpoint{API: a}, &endpoints.HostsEndpoint{API: a}, &endpoints.StatusEndpoint{}, &endpoints.DataStoreEndpoint{API: a}} { + endpoint.Register(g) + } + + w := httptest.NewRecorder() + + for _, request := range requests { + g.ServeHTTP(w, request) + } + return w +}