From 15630b906cf1bed9ae9cdbfa844a886d372fe8d3 Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Fri, 10 Jan 2020 16:14:30 -0800 Subject: [PATCH] vapi: Add cluster modules client and simulator --- vapi/cluster/cluster.go | 112 ++++++++++++++++ vapi/cluster/cluster_test.go | 161 +++++++++++++++++++++++ vapi/cluster/internal/internal.go | 87 +++++++++++++ vapi/cluster/simulator/simulator.go | 192 ++++++++++++++++++++++++++++ vcsim/main.go | 1 + 5 files changed, 553 insertions(+) create mode 100644 vapi/cluster/cluster.go create mode 100644 vapi/cluster/cluster_test.go create mode 100644 vapi/cluster/internal/internal.go create mode 100644 vapi/cluster/simulator/simulator.go diff --git a/vapi/cluster/cluster.go b/vapi/cluster/cluster.go new file mode 100644 index 000000000..927c672ba --- /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/cluster_test.go b/vapi/cluster/cluster_test.go new file mode 100644 index 000000000..278810f76 --- /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)) + } + } + }) +} diff --git a/vapi/cluster/internal/internal.go b/vapi/cluster/internal/internal.go new file mode 100644 index 000000000..9da4666f7 --- /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 000000000..77ab79e58 --- /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 246bda0a7..9cc73d5a8 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" )