Repository: vmware/govmomi
Pull request #1784 (2020-01-11 00:15:59)

vapi: Add cluster modules client and simulator


Patch:
diff --git a/vapi/cluster/cluster.go b/vapi/cluster/cluster.go
new file mode 100644
index 00000000..927c672b
--- /dev/null
+++ b/vapi/cluster/cluster.go
@@ -0,0 +1,112 @@
+/*
+Copyright (c) 2020 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cluster
+
+import (
+	"context"
+	"net/http"
+	"path"
+
+	"github.com/vmware/govmomi/vapi/rest"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+
+	"github.com/vmware/govmomi/vapi/cluster/internal"
+)
+
+// Manager extends rest.Client, adding cluster related methods.
+type Manager struct {
+	*rest.Client
+}
+
+// NewManager creates a new Manager instance with the given client.
+func NewManager(client *rest.Client) *Manager {
+	return &Manager{
+		Client: client,
+	}
+}
+
+// CreateModule creates a new module in a vCenter cluster.
+func (c *Manager) CreateModule(ctx context.Context, ref mo.Reference) (string, error) {
+	var s internal.CreateModule
+	s.Spec.ID = ref.Reference().Value
+
+	url := c.Resource(internal.ModulesPath)
+	var res string
+	return res, c.Do(ctx, url.Request(http.MethodPost, s), &res)
+}
+
+// DeleteModule deletes a specific module.
+func (c *Manager) DeleteModule(ctx context.Context, id string) error {
+	url := c.Resource(internal.ModulesPath + "/" + id)
+	return c.Do(ctx, url.Request(http.MethodDelete), nil)
+}
+
+// ModuleSummary contains commonly used information about a module in a vCenter cluster.
+type ModuleSummary struct {
+	Cluster string `json:"cluster"`
+	Module  string `json:"module"`
+}
+
+// ModuleSummaryList is used to JSON encode/decode a ModuleSummary.
+type ModuleSummaryList struct {
+	Summaries []ModuleSummary `json:"summaries"`
+}
+
+// ListModules returns information about the modules available in this vCenter server.
+func (c *Manager) ListModules(ctx context.Context) ([]ModuleSummary, error) {
+	var res ModuleSummaryList
+	url := c.Resource(internal.ModulesPath)
+	return res.Summaries, c.Do(ctx, url.Request(http.MethodGet), &res)
+}
+
+func memberPath(id string) string {
+	return path.Join(internal.ModulesVMPath, id, "members")
+}
+
+// ListModuleMembers returns the virtual machines that are members of the module.
+func (c *Manager) ListModuleMembers(ctx context.Context, id string) ([]types.ManagedObjectReference, error) {
+	var m internal.ModuleMembers
+	url := c.Resource(memberPath(id))
+	err := c.Do(ctx, url.Request(http.MethodGet), &m)
+	if err != nil {
+		return nil, err
+	}
+	return m.AsReferences(), err
+}
+
+func (c *Manager) moduleMembers(ctx context.Context, action string, id string, vms ...mo.Reference) (bool, error) {
+	url := c.Resource(memberPath(id)).WithParam("action", action)
+	var m internal.ModuleMembers
+	for i := range vms {
+		m.VMs = append(m.VMs, vms[i].Reference().Value)
+	}
+	var res internal.Status
+	return res.Success, c.Do(ctx, url.Request(http.MethodPost, m), &res)
+}
+
+// AddModuleMembers adds virtual machines to the module. These virtual machines are required to be in the same vCenter cluster.
+// Returns true if all vms are added, false if a vm is already a member of the module or not within the module's cluster.
+func (c *Manager) AddModuleMembers(ctx context.Context, id string, vms ...mo.Reference) (bool, error) {
+	return c.moduleMembers(ctx, "add", id, vms...)
+}
+
+// RemoveModuleMembers removes virtual machines from the module.
+// Returns true if all vms are removed, false if a vm is not a member of the module.
+func (c *Manager) RemoveModuleMembers(ctx context.Context, id string, vms ...mo.Reference) (bool, error) {
+	return c.moduleMembers(ctx, "remove", id, vms...)
+}
diff --git a/vapi/cluster/internal/internal.go b/vapi/cluster/internal/internal.go
new file mode 100644
index 00000000..9da4666f
--- /dev/null
+++ b/vapi/cluster/internal/internal.go
@@ -0,0 +1,87 @@
+/*
+Copyright (c) 2020 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package internal
+
+import (
+	"context"
+
+	"github.com/vmware/govmomi/view"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+const (
+	// ModulesPath is rest endpoint for the Cluster Modules API
+	ModulesPath = "/vcenter/cluster/modules"
+	// ModulesVMPath is rest endpoint for the Cluster Modules Members API
+	ModulesVMPath = "/vcenter/cluster/modules/vm"
+)
+
+// Status is used for JSON encode/decode
+type Status struct {
+	Success bool `json:"success"`
+}
+
+// CreateModule is used for JSON encode/decode
+type CreateModule struct {
+	Spec struct {
+		ID string `json:"cluster"`
+	} `json:"spec"`
+}
+
+// ModuleMembers is used for JSON encode/decode
+type ModuleMembers struct {
+	VMs []string `json:"vms"`
+}
+
+// AsReferences converts the ModuleMembers.VM field to morefs
+func (m *ModuleMembers) AsReferences() []types.ManagedObjectReference {
+	refs := make([]types.ManagedObjectReference, 0, len(m.VMs))
+	for _, id := range m.VMs {
+		refs = append(refs, types.ManagedObjectReference{
+			Type:  "VirtualMachine",
+			Value: id,
+		})
+	}
+	return refs
+}
+
+// ClusterVM returns all VM references in the given cluster
+func ClusterVM(c *vim25.Client, cluster mo.Reference) ([]mo.Reference, error) {
+	ctx := context.Background()
+	kind := []string{"VirtualMachine"}
+
+	m := view.NewManager(c)
+	v, err := m.CreateContainerView(ctx, cluster.Reference(), kind, true)
+	if err != nil {
+		return nil, err
+	}
+	defer func() { _ = v.Destroy(ctx) }()
+
+	refs, err := v.Find(ctx, kind, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	vms := make([]mo.Reference, 0, len(refs))
+	for i := range refs {
+		vms = append(vms, refs[i])
+	}
+
+	return vms, nil
+}
diff --git a/vapi/cluster/simulator/simulator.go b/vapi/cluster/simulator/simulator.go
new file mode 100644
index 00000000..77ab79e5
--- /dev/null
+++ b/vapi/cluster/simulator/simulator.go
@@ -0,0 +1,192 @@
+/*
+Copyright (c) 2020 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package simulator
+
+import (
+	"context"
+	"net/http"
+	"net/url"
+	"path"
+
+	"github.com/google/uuid"
+	"github.com/vmware/govmomi"
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/rest"
+	vapi "github.com/vmware/govmomi/vapi/simulator"
+	"github.com/vmware/govmomi/vim25/types"
+
+	"github.com/vmware/govmomi/vapi/cluster"
+	"github.com/vmware/govmomi/vapi/cluster/internal"
+)
+
+func init() {
+	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
+		New(s.Listen).Register(s, r)
+	})
+}
+
+type module struct {
+	cluster.ModuleSummary
+	members map[string]bool
+}
+
+// Handler implements the Cluster Modules API simulator
+type Handler struct {
+	Modules map[string]module
+	URL     *url.URL
+}
+
+// New creates a Handler instance
+func New(u *url.URL) *Handler {
+	return &Handler{
+		Modules: make(map[string]module),
+		URL:     u,
+	}
+}
+
+// Register Cluster Modules API paths with the vapi simulator's http.ServeMux
+func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) {
+	if r.IsVPX() {
+		s.HandleFunc(rest.Path+internal.ModulesPath, h.modules)
+		s.HandleFunc(rest.Path+internal.ModulesPath+"/", h.modules)
+		s.HandleFunc(rest.Path+internal.ModulesVMPath+"/", h.modulesVM)
+	}
+}
+
+func (h *Handler) modules(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodGet:
+		var modules cluster.ModuleSummaryList
+		for _, s := range h.Modules {
+			modules.Summaries = append(modules.Summaries, s.ModuleSummary)
+		}
+		vapi.OK(w, modules)
+	case http.MethodPost:
+		var m internal.CreateModule
+		if vapi.Decode(r, w, &m) {
+			ref := types.ManagedObjectReference{Type: "ClusterComputeResource", Value: m.Spec.ID}
+			if simulator.Map.Get(ref) == nil {
+				vapi.BadRequest(w, "com.vmware.vapi.std.errors.invalid_argument")
+				return
+			}
+
+			id := uuid.New().String()
+			h.Modules[id] = module{
+				cluster.ModuleSummary{
+					Cluster: m.Spec.ID,
+					Module:  id,
+				},
+				make(map[string]bool),
+			}
+			vapi.OK(w, id)
+		}
+	case http.MethodDelete:
+		id := path.Base(r.URL.Path)
+		_, ok := h.Modules[id]
+		if !ok {
+			http.NotFound(w, r)
+			return
+		}
+		delete(h.Modules, id)
+		vapi.OK(w)
+	}
+}
+
+func (*Handler) action(r *http.Request) string {
+	return r.URL.Query().Get("action")
+}
+
+func (h *Handler) addMembers(members internal.ModuleMembers, m module) bool {
+	cluster := types.ManagedObjectReference{Type: "ClusterComputeResource", Value: m.Cluster}
+	c, err := govmomi.NewClient(context.Background(), h.URL, true)
+	if err != nil {
+		panic(err)
+	}
+	vms, err := internal.ClusterVM(c.Client, cluster)
+	if err != nil {
+		panic(err)
+	}
+	_ = c.Logout(context.Background())
+
+	validVM := func(id string) bool {
+		for i := range vms {
+			if vms[i].Reference().Value == id {
+				return true
+			}
+		}
+		return false
+	}
+
+	for _, id := range members.VMs {
+		if m.members[id] {
+			return false
+		}
+		if !validVM(id) {
+			return false
+		}
+		m.members[id] = true
+	}
+	return true
+}
+
+func (h *Handler) removeMembers(members internal.ModuleMembers, m module) bool {
+	for _, id := range members.VMs {
+		if !m.members[id] {
+			return false
+		}
+		delete(m.members, id)
+	}
+	return true
+}
+
+func (h *Handler) modulesVM(w http.ResponseWriter, r *http.Request) {
+	p := path.Dir(r.URL.Path)
+	id := path.Base(p)
+
+	m, ok := h.Modules[id]
+	if !ok {
+		http.NotFound(w, r)
+		return
+	}
+
+	switch r.Method {
+	case http.MethodGet:
+		var members internal.ModuleMembers
+		for member := range m.members {
+			members.VMs = append(members.VMs, member)
+		}
+		vapi.OK(w, members)
+	case http.MethodPost:
+		action := h.addMembers
+
+		switch h.action(r) {
+		case "add":
+		case "remove":
+			action = h.removeMembers
+		default:
+			http.NotFound(w, r)
+			return
+		}
+
+		var status internal.Status
+		var members internal.ModuleMembers
+		if vapi.Decode(r, w, &members) {
+			status.Success = action(members, m)
+			vapi.OK(w, status)
+		}
+	}
+}
diff --git a/vcsim/main.go b/vcsim/main.go
index 246bda0a..9cc73d5a 100644
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -42,6 +42,7 @@ import (
 	_ "github.com/vmware/govmomi/lookup/simulator"
 	_ "github.com/vmware/govmomi/pbm/simulator"
 	_ "github.com/vmware/govmomi/sts/simulator"
+	_ "github.com/vmware/govmomi/vapi/cluster/simulator"
 	_ "github.com/vmware/govmomi/vapi/simulator"
 )
 


Test patch:
diff --git a/vapi/cluster/cluster_test.go b/vapi/cluster/cluster_test.go
new file mode 100644
index 00000000..278810f7
--- /dev/null
+++ b/vapi/cluster/cluster_test.go
@@ -0,0 +1,161 @@
+/*
+Copyright (c) 2020 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cluster_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/rest"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+
+	"github.com/vmware/govmomi/vapi/cluster"
+	"github.com/vmware/govmomi/vapi/cluster/internal"
+
+	_ "github.com/vmware/govmomi/vapi/cluster/simulator"
+	_ "github.com/vmware/govmomi/vapi/simulator"
+)
+
+var enoent = types.ManagedObjectReference{Value: "enoent"}
+
+func TestClusterModules(t *testing.T) {
+	simulator.Test(func(ctx context.Context, vc *vim25.Client) {
+		c := rest.NewClient(vc)
+
+		err := c.Login(ctx, simulator.DefaultLogin)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		m := cluster.NewManager(c)
+		modules, err := m.ListModules(ctx)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(modules) != 0 {
+			t.Errorf("expected 0 modules")
+		}
+
+		ccr := simulator.Map.Any("ClusterComputeResource")
+
+		_, err = m.CreateModule(ctx, enoent)
+		if err == nil {
+			t.Fatal("expected error")
+		}
+
+		id, err := m.CreateModule(ctx, ccr)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		modules, err = m.ListModules(ctx)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(modules) != 1 {
+			t.Errorf("expected 1 module")
+		}
+
+		err = m.DeleteModule(ctx, "enoent")
+		if err == nil {
+			t.Fatal("expected error")
+		}
+
+		err = m.DeleteModule(ctx, id)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		modules, err = m.ListModules(ctx)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if len(modules) != 0 {
+			t.Errorf("expected 0 modules")
+		}
+	})
+}
+
+func TestClusterModuleMembers(t *testing.T) {
+	simulator.Test(func(ctx context.Context, vc *vim25.Client) {
+		c := rest.NewClient(vc)
+
+		err := c.Login(ctx, simulator.DefaultLogin)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		m := cluster.NewManager(c)
+
+		_, err = m.ListModuleMembers(ctx, "enoent")
+		if err == nil {
+			t.Error("expected error")
+		}
+
+		ccr := simulator.Map.Any("ClusterComputeResource")
+
+		id, err := m.CreateModule(ctx, ccr)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		vms, err := internal.ClusterVM(vc, ccr)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		expect := []struct {
+			n       int
+			success bool
+			action  func(context.Context, string, ...mo.Reference) (bool, error)
+			ids     []mo.Reference
+		}{
+			{0, false, m.AddModuleMembers, []mo.Reference{enoent}},
+			{0, false, m.RemoveModuleMembers, []mo.Reference{enoent}},
+			{len(vms), true, m.AddModuleMembers, vms},
+			{len(vms), false, m.AddModuleMembers, vms},
+			{0, true, m.RemoveModuleMembers, vms},
+			{len(vms), false, m.AddModuleMembers, append(vms, enoent)},
+			{len(vms) - 1, false, m.RemoveModuleMembers, []mo.Reference{vms[0], enoent}},
+		}
+
+		for i, test := range expect {
+			ok, err := test.action(ctx, id, test.ids...)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if ok != test.success {
+				t.Errorf("%d: success=%t", i, ok)
+			}
+
+			members, err := m.ListModuleMembers(ctx, id)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if len(members) != test.n {
+				t.Errorf("%d: members=%d", i, len(members))
+			}
+		}
+	})
+}