Repository: vmware/govmomi
Pull request #2808 (2022-04-06 18:02:42)

govc: Add CLI command cluster.module
## Description

Add govc cluster.module command

This is a little easier than trying to remember the vapi HTTP calls. Note that modules.ls does output the MoIDs since that is what the API returns: a later improvement might be to resolve those to their friendlier inventory names.

Closes: #(issue-number)

## Type of change

Please mark options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to
  not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

Against real VC, and added bats test

## Checklist:

- [ ] My code follows the CONTRIBUTION [guidelines](./../CONTRIBUTING.md) of
  this project
- [ ] I have commented my code, particularly in hard-to-understand areas
- [X] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged


Patch:
diff --git a/govc/USAGE.md b/govc/USAGE.md
index 694aef5c..ff7d1747 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -44,6 +44,11 @@ but appear via `govc $cmd -h`:
  - [cluster.group.create](#clustergroupcreate)
  - [cluster.group.ls](#clustergroupls)
  - [cluster.group.remove](#clustergroupremove)
+ - [cluster.module.create](#clustermodulecreate)
+ - [cluster.module.ls](#clustermodulels)
+ - [cluster.module.rm](#clustermodulerm)
+ - [cluster.module.vm.add](#clustermodulevmadd)
+ - [cluster.module.vm.rm](#clustermodulevmrm)
  - [cluster.override.change](#clusteroverridechange)
  - [cluster.override.info](#clusteroverrideinfo)
  - [cluster.override.remove](#clusteroverrideremove)
@@ -523,6 +528,81 @@ Options:
   -name=                 Cluster group name
 ```
 
+## cluster.module.create
+
+```
+Usage: govc cluster.module.create [OPTIONS]
+
+Create cluster module.
+
+This command will output the ID of the new module.
+
+Examples:
+  govc cluster.module.create -cluster my_cluster
+
+Options:
+  -cluster=              Cluster [GOVC_CLUSTER]
+```
+
+## cluster.module.ls
+
+```
+Usage: govc cluster.module.ls [OPTIONS]
+
+List cluster modules.
+
+When -id is specified, that module's members are listed.
+
+Examples:
+  govc cluster.module.ls
+  govc cluster.module.ls -json | jq .
+  govc cluster.module.ls -id module_id
+
+Options:
+  -id=                   Module ID
+```
+
+## cluster.module.rm
+
+```
+Usage: govc cluster.module.rm [OPTIONS] ID
+
+Delete cluster module ID.
+
+Examples:
+  govc cluster.module.rm module_id
+
+Options:
+```
+
+## cluster.module.vm.add
+
+```
+Usage: govc cluster.module.vm.add [OPTIONS] VM...
+
+Add VM(s) to a cluster module.
+
+Examples:
+  govc cluster.module.vm.add -id module_id $vm...
+
+Options:
+  -id=                   Module ID
+```
+
+## cluster.module.vm.rm
+
+```
+Usage: govc cluster.module.vm.rm [OPTIONS] VM...
+
+Remove VM(s) from a cluster module.
+
+Examples:
+  govc cluster.module.vm.rm -id module_id $vm...
+
+Options:
+  -id=                   Module ID
+```
+
 ## cluster.override.change
 
 ```
diff --git a/govc/cluster/module/create.go b/govc/cluster/module/create.go
new file mode 100644
index 00000000..63e3bb57
--- /dev/null
+++ b/govc/cluster/module/create.go
@@ -0,0 +1,77 @@
+/*
+Copyright (c) 2022 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 module
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	vapicluster "github.com/vmware/govmomi/vapi/cluster"
+)
+
+type create struct {
+	*flags.ClusterFlag
+}
+
+func init() {
+	cli.Register("cluster.module.create", &create{})
+}
+
+func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClusterFlag, ctx = flags.NewClusterFlag(ctx)
+	cmd.ClusterFlag.Register(ctx, f)
+}
+
+func (cmd *create) Process(ctx context.Context) error {
+	return cmd.ClusterFlag.Process(ctx)
+}
+
+func (cmd *create) Description() string {
+	return `Create cluster module.
+
+This command will output the ID of the new module.
+
+Examples:
+  govc cluster.module.create -cluster my_cluster`
+}
+
+func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 0 {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	cluster, err := cmd.Cluster()
+	if err != nil {
+		return err
+	}
+
+	id, err := vapicluster.NewManager(c).CreateModule(ctx, cluster.Reference())
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(id)
+	return nil
+}
diff --git a/govc/cluster/module/ls.go b/govc/cluster/module/ls.go
new file mode 100644
index 00000000..a5f7f38a
--- /dev/null
+++ b/govc/cluster/module/ls.go
@@ -0,0 +1,130 @@
+/*
+Copyright (c) 2022 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 module
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"text/tabwriter"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/cluster"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type ls struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+	moduleID string
+}
+
+func init() {
+	cli.Register("cluster.module.ls", &ls{})
+}
+
+func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.moduleID, "id", "", "Module ID")
+}
+
+func (cmd *ls) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *ls) Description() string {
+	return `List cluster modules.
+
+When -id is specified, that module's members are listed.
+
+Examples:
+  govc cluster.module.ls
+  govc cluster.module.ls -json | jq .
+  govc cluster.module.ls -id module_id`
+}
+
+type lsResult []cluster.ModuleSummary
+
+func (r lsResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	for _, i := range r {
+		fmt.Fprintf(tw, "%s\t%s\n", i.Cluster, i.Module)
+	}
+
+	return tw.Flush()
+}
+
+func (cmd *ls) List(ctx context.Context, m *cluster.Manager) error {
+	var res lsResult
+
+	res, err := m.ListModules(ctx)
+	if err != nil {
+		return err
+	}
+
+	return cmd.WriteResult(res)
+}
+
+type lsMemberResult []types.ManagedObjectReference
+
+func (r lsMemberResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	for _, i := range r {
+		fmt.Fprintf(tw, "%s\t%s\n", i.Reference().Type, i.Reference().Value)
+	}
+
+	return tw.Flush()
+}
+
+func (cmd *ls) ListMembers(ctx context.Context, m *cluster.Manager, moduleID string) error {
+	var res lsMemberResult
+
+	res, err := m.ListModuleMembers(ctx, moduleID)
+	if err != nil {
+		return err
+	}
+
+	return cmd.WriteResult(res)
+}
+
+func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 0 {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	m := cluster.NewManager(c)
+
+	if cmd.moduleID == "" {
+		return cmd.List(ctx, m)
+	}
+
+	return cmd.ListMembers(ctx, m, cmd.moduleID)
+}
diff --git a/govc/cluster/module/rm.go b/govc/cluster/module/rm.go
new file mode 100644
index 00000000..7938c1ec
--- /dev/null
+++ b/govc/cluster/module/rm.go
@@ -0,0 +1,65 @@
+/*
+Copyright (c) 2022 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 module
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/cluster"
+)
+
+type rm struct {
+	*flags.ClientFlag
+}
+
+func init() {
+	cli.Register("cluster.module.rm", &rm{})
+}
+
+func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+}
+
+func (cmd *rm) Usage() string {
+	return "ID"
+}
+
+func (cmd *rm) Description() string {
+	return `Delete cluster module ID.
+
+Examples:
+  govc cluster.module.rm module_id`
+}
+
+func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	moduleID := f.Arg(0)
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	return cluster.NewManager(c).DeleteModule(ctx, moduleID)
+}
diff --git a/govc/cluster/module/vm_add.go b/govc/cluster/module/vm_add.go
new file mode 100644
index 00000000..3a0f5094
--- /dev/null
+++ b/govc/cluster/module/vm_add.go
@@ -0,0 +1,91 @@
+/*
+Copyright (c) 2022 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 module
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/cluster"
+	"github.com/vmware/govmomi/vim25/mo"
+)
+
+type addVMs struct {
+	*flags.SearchFlag
+	moduleID string
+}
+
+func init() {
+	cli.Register("cluster.module.vm.add", &addVMs{})
+}
+
+func (cmd *addVMs) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.SearchFlag, ctx = flags.NewSearchFlag(ctx, flags.SearchVirtualMachines)
+	cmd.SearchFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.moduleID, "id", "", "Module ID")
+}
+
+func (cmd *addVMs) Process(ctx context.Context) error {
+	return cmd.SearchFlag.Process(ctx)
+}
+
+func (cmd *addVMs) Usage() string {
+	return `VM...`
+}
+
+func (cmd *addVMs) Description() string {
+	return `Add VM(s) to a cluster module.
+
+Examples:
+  govc cluster.module.vm.add -id module_id $vm...`
+}
+
+func (cmd *addVMs) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() < 1 {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	vms, err := cmd.VirtualMachines(f.Args())
+	if err != nil {
+		return err
+	}
+
+	refs := make([]mo.Reference, 0, len(vms))
+	for _, vm := range vms {
+		refs = append(refs, vm.Reference())
+	}
+
+	allAdded, err := cluster.NewManager(c).AddModuleMembers(ctx, cmd.moduleID, refs...)
+	if err != nil {
+		return err
+	}
+
+	if !allAdded {
+		return fmt.Errorf("a VM is already a member of the module or not within the module's cluster")
+	}
+
+	return nil
+}
diff --git a/govc/cluster/module/vm_rm.go b/govc/cluster/module/vm_rm.go
new file mode 100644
index 00000000..b837db90
--- /dev/null
+++ b/govc/cluster/module/vm_rm.go
@@ -0,0 +1,91 @@
+/*
+Copyright (c) 2022 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 module
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/cluster"
+	"github.com/vmware/govmomi/vim25/mo"
+)
+
+type rmVMs struct {
+	*flags.SearchFlag
+	moduleID string
+}
+
+func init() {
+	cli.Register("cluster.module.vm.rm", &rmVMs{})
+}
+
+func (cmd *rmVMs) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.SearchFlag, ctx = flags.NewSearchFlag(ctx, flags.SearchVirtualMachines)
+	cmd.SearchFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.moduleID, "id", "", "Module ID")
+}
+
+func (cmd *rmVMs) Process(ctx context.Context) error {
+	return cmd.SearchFlag.Process(ctx)
+}
+
+func (cmd *rmVMs) Usage() string {
+	return `VM...`
+}
+
+func (cmd *rmVMs) Description() string {
+	return `Remove VM(s) from a cluster module.
+
+Examples:
+  govc cluster.module.vm.rm -id module_id $vm...`
+}
+
+func (cmd *rmVMs) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() < 1 {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	vms, err := cmd.VirtualMachines(f.Args())
+	if err != nil {
+		return err
+	}
+
+	refs := make([]mo.Reference, 0, len(vms))
+	for _, vm := range vms {
+		refs = append(refs, vm.Reference())
+	}
+
+	allRemoved, err := cluster.NewManager(c).RemoveModuleMembers(ctx, cmd.moduleID, refs...)
+	if err != nil {
+		return err
+	}
+
+	if !allRemoved {
+		return fmt.Errorf("a VM was not a member of the module")
+	}
+
+	return nil
+}
diff --git a/govc/main.go b/govc/main.go
index f0d82bec..ec91ae7d 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -23,6 +23,7 @@ import (
 	"github.com/vmware/govmomi/govc/cli"
 	_ "github.com/vmware/govmomi/govc/cluster"
 	_ "github.com/vmware/govmomi/govc/cluster/group"
+	_ "github.com/vmware/govmomi/govc/cluster/module"
 	_ "github.com/vmware/govmomi/govc/cluster/override"
 	_ "github.com/vmware/govmomi/govc/cluster/rule"
 	_ "github.com/vmware/govmomi/govc/datacenter"


Test patch:
diff --git a/govc/test/cluster.bats b/govc/test/cluster.bats
index 1bb76e29..6e6fa7e6 100755
--- a/govc/test/cluster.bats
+++ b/govc/test/cluster.bats
@@ -282,3 +282,48 @@ _EOF_
   run govc cluster.stretch -witness DC0_H0 -first-fault-domain-hosts=DC0_C0_H1,DC0_C0_H2 -second-fault-domain-hosts DC0_C0_H2,DC0_C0_H3 DC0_C0
   assert_success
 }
+
+@test "cluster.module" {
+  vcsim_env
+  local output
+
+  run govc cluster.module.ls
+  assert_success # no tags defined yet
+
+  run govc cluster.module.ls -k=false
+  assert_failure
+
+  run govc cluster.module.ls -id enoent
+  assert_failure # specific module does not exist
+
+  run govc cluster.module.create -cluster DC0_C0
+  assert_success
+
+  id="$output"
+
+  run govc cluster.module.ls -id $id
+  assert_success
+
+  vm="/DC0/vm/DC0_C0_RP0_VM0"
+
+  run govc cluster.module.vm.add -id $id $vm
+  assert_success
+
+  count=$(govc cluster.module.ls -id $id | grep -c VirtualMachine)
+  [ "$count" = "1" ]
+
+  run govc cluster.module.vm.add -id $id $vm
+  assert_failure # already a member
+
+  run govc cluster.module.vm.rm -id $id $vm
+  assert_success
+
+  run govc cluster.module.ls -id $id
+  [ -z "$output" ]
+
+  run govc cluster.module.rm $id
+  assert_success
+
+  run govc cluster.module.ls -id $id
+  assert_failure
+}