diff --git a/internal/api/api.go b/internal/api/api.go index 54a0840..054146d 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -21,6 +21,9 @@ type VSphereProxyApi interface { // 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) } // DefaultVSphereProxyApi is the default API implementation 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/models.go b/internal/api/models.go index fca27fa..3090cda 100644 --- a/internal/api/models.go +++ b/internal/api/models.go @@ -22,3 +22,12 @@ type Host struct { 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"` +} 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_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/test/tools.go b/test/tools.go index 3776418..217eff5 100644 --- a/test/tools.go +++ b/test/tools.go @@ -36,7 +36,7 @@ func TestRequests(handler http.Handler, requests []*http.Request) *httptest.Resp 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{}} { + for _, endpoint := range []endpoints.Endpoint{&endpoints.VMSEndpoint{API: a}, &endpoints.HostsEndpoint{API: a}, &endpoints.StatusEndpoint{}, &endpoints.DataStoreEndpoint{API: a}} { endpoint.Register(g) }