Skip to content

Commit

Permalink
feat: Support for vminfo endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
dploeger committed Dec 22, 2023
1 parent 0758124 commit 3c9567c
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,16 @@ 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=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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=
Expand Down Expand Up @@ -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=
Expand Down
3 changes: 3 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type VSphereProxyApi interface {

// 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
Expand Down
8 changes: 8 additions & 0 deletions internal/api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ type Datastore struct {
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"`
}
59 changes: 59 additions & 0 deletions internal/api/vms.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/thoas/go-funk"
)

// GetVMs returns all VMs from the VM endpoint
Expand Down Expand Up @@ -145,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
}
}
}
}
20 changes: 20 additions & 0 deletions internal/endpoints/vms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
43 changes: 42 additions & 1 deletion internal/endpoints_test/vms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func TestVMSEndpoint_GetFQDN(t *testing.T) {
b := builder.NewBuilder().
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", test.AUTHTOKEN)).
Expand All @@ -183,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)
}

0 comments on commit 3c9567c

Please sign in to comment.