Repository: vmware/govmomi
Pull request #3609 (2024-11-03 16:43:08)

api: support MOID conversion in Finder methods
With this change, finder methods can convert ManagedObjectReference or MOID to an object instance. For example:

```console
# `-i` outputs MOR
% govc find -l -i -type m / -name nginx
VirtualMachine:vm-1100  /test-vpx-1730432486-649974-wcp.wcp-sanity/vm/nginx

% govc vm.info VirtualMachine:vm-1100
Name:           nginx
  Path:         /test-vpx-1730432486-649974-wcp.wcp-sanity/vm/nginx
  UUID:         421b8fd2-3006-0a69-1f16-7d088f90145f
  Guest name:   Other (32-bit)
   ...

# `-I` outputs MOID
% govc find -l -I -type m / -name nginx
vm-1100  /test-vpx-1730432486-649974-wcp.wcp-sanity/vm/nginx

% govc vm.info vm-1100
Name:           nginx
  Path:         /test-vpx-1730432486-649974-wcp.wcp-sanity/vm/nginx
  UUID:         421b8fd2-3006-0a69-1f16-7d088f90145f
  Guest name:   Other (32-bit)
...

% govc collect -s vm-1100 runtime.host | cut -d: -f2
host-14

% govc host.info host-14
Name:              10.192.85.11
  Path:            /test-vpx-1730432486-649974-wcp.wcp-sanity/host/test-vpx-1730432486-649974-wcp.wcp-sanity-cluster/10.192.85.11
  Manufacturer:    VMware, Inc.
  Logical CPUs:    4 CPUs @ 2100MHz
 ...
```


Patch:
diff --git a/find/doc.go b/find/doc.go
index d22e8835..a42a910a 100644
--- a/find/doc.go
+++ b/find/doc.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2014-2017 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-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
+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,
@@ -32,6 +32,9 @@ otherwise "find" mode is used.
 The exception is to use a "..." wildcard with a path to find all objects recursively underneath any root object.
 For example: VirtualMachineList("/DC1/...")
 
+Finder methods can also convert a managed object reference (aka MOID) to an object instance.
+For example: VirtualMachine("VirtualMachine:vm-123") or VirtualMachine("vm-123")
+
 See also: https://github.com/vmware/govmomi/blob/main/govc/README.md#usage
 */
 package find
diff --git a/find/finder.go b/find/finder.go
index 887ddd5c..b4949597 100644
--- a/find/finder.go
+++ b/find/finder.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2014-2023 VMware, Inc. All Rights Reserved.
+Copyright (c) 2014-2024 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
+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,
@@ -22,6 +22,7 @@ import (
 	"path"
 	"strings"
 
+	"github.com/vmware/govmomi/fault"
 	"github.com/vmware/govmomi/internal"
 	"github.com/vmware/govmomi/list"
 	"github.com/vmware/govmomi/object"
@@ -116,6 +117,19 @@ func (f *Finder) findRoot(ctx context.Context, root *list.Element, parts []strin
 func (f *Finder) find(ctx context.Context, arg string, s *spec) ([]list.Element, error) {
 	isPath := strings.Contains(arg, "/")
 
+	if !isPath {
+		if ref := object.ReferenceFromString(arg); ref != nil {
+			p, err := InventoryPath(ctx, f.client, *ref)
+			if err == nil {
+				if t, ok := mo.Value(*ref); ok {
+					return []list.Element{{Object: t, Path: p}}, nil
+				}
+			} else if !fault.Is(err, &types.ManagedObjectNotFound{}) {
+				return nil, err
+			} // else fall through to name based lookup
+		}
+	}
+
 	root := list.Element{
 		Object: object.NewRootFolder(f.client),
 		Path:   "/",
@@ -821,11 +835,6 @@ func (f *Finder) Network(ctx context.Context, path string) (object.NetworkRefere
 }
 
 func (f *Finder) networkByID(ctx context.Context, path string) (object.NetworkReference, error) {
-	if ref := object.ReferenceFromString(path); ref != nil {
-		// This is a MOID
-		return object.NewReference(f.client, *ref).(object.NetworkReference), nil
-	}
-
 	kind := []string{"DistributedVirtualPortgroup"}
 
 	m := view.NewManager(f.client)
diff --git a/govc/USAGE.md b/govc/USAGE.md
index ab83f3a6..eb2f514e 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -2600,6 +2600,7 @@ The '-type' flag value can be a managed entity type or one of the following alia
 Examples:
   govc find
   govc find -l / # include object type in output
+  govc find -l -I / # include MOID in output
   govc find /dc1 -type c
   govc find vm -name my-vm-*
   govc find . -type n
@@ -2611,6 +2612,7 @@ Examples:
   govc find . -type h -hardware.cpuInfo.numCpuCores 16
 
 Options:
+  -I=false               Print the managed object ID
   -i=false               Print the managed object reference
   -l=false               Long listing format
   -maxdepth=-1           Max depth
@@ -4388,6 +4390,7 @@ Examples:
   govc ls -t Datastore host/ClusterA/* | grep -v local | xargs -n1 basename | sort | uniq
 
 Options:
+  -I=false               Print the managed object ID
   -L=false               Follow managed object references
   -i=false               Print the managed object reference
   -l=false               Long listing format
diff --git a/govc/flags/datacenter.go b/govc/flags/datacenter.go
index 7cfcef3b..9d5e03ac 100644
--- a/govc/flags/datacenter.go
+++ b/govc/flags/datacenter.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2014-2016 VMware, Inc. All Rights Reserved.
+Copyright (c) 2014-2024 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
+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,
@@ -197,16 +197,6 @@ func (flag *DatacenterFlag) ManagedObjects(ctx context.Context, args []string) (
 	}
 
 	for _, arg := range args {
-		if ref := object.ReferenceFromString(arg); ref != nil {
-			// e.g. output from object.collect
-			refs = append(refs, *ref)
-			continue
-		}
-
-		if !strings.Contains(arg, "/") {
-			return nil, fmt.Errorf("%q must be qualified with a path", arg)
-		}
-
 		elements, err := finder.ManagedObjectList(ctx, arg)
 		if err != nil {
 			return nil, err
@@ -216,6 +206,10 @@ func (flag *DatacenterFlag) ManagedObjects(ctx context.Context, args []string) (
 			return nil, fmt.Errorf("object '%s' not found", arg)
 		}
 
+		if len(elements) > 1 && !strings.Contains(arg, "/") {
+			return nil, fmt.Errorf("%q must be qualified with a path", arg)
+		}
+
 		for _, e := range elements {
 			refs = append(refs, e.Object.Reference())
 		}
diff --git a/govc/ls/command.go b/govc/ls/command.go
index 6be82b2f..d5555c7d 100644
--- a/govc/ls/command.go
+++ b/govc/ls/command.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
+Copyright (c) 2014-2024 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
+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,
@@ -36,6 +36,7 @@ type ls struct {
 	Long  bool
 	Type  string
 	ToRef bool
+	ToID  bool
 	DeRef bool
 }
 
@@ -49,6 +50,7 @@ func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) {
 
 	f.BoolVar(&cmd.Long, "l", false, "Long listing format")
 	f.BoolVar(&cmd.ToRef, "i", false, "Print the managed object reference")
+	f.BoolVar(&cmd.ToID, "I", false, "Print the managed object ID")
 	f.BoolVar(&cmd.DeRef, "L", false, "Follow managed object references")
 	f.StringVar(&cmd.Type, "t", "", "Object type")
 }
@@ -145,8 +147,13 @@ func (l listResult) Write(w io.Writer) error {
 	var err error
 
 	for _, e := range l.Elements {
-		if l.ToRef {
-			fmt.Fprint(w, e.Object.Reference().String())
+		if l.ToRef || l.ToID {
+			ref := e.Object.Reference()
+			id := ref.String()
+			if l.ToID {
+				id = ref.Value
+			}
+			fmt.Fprint(w, id)
 			if l.Long {
 				fmt.Fprintf(w, " %s", e.Path)
 			}
diff --git a/govc/object/find.go b/govc/object/find.go
index e19abed9..94ba1fab 100644
--- a/govc/object/find.go
+++ b/govc/object/find.go
@@ -41,6 +41,7 @@ type find struct {
 	*flags.DatacenterFlag
 
 	ref      bool
+	id       bool
 	long     bool
 	parent   bool
 	kind     kinds
@@ -128,6 +129,7 @@ func (cmd *find) Register(ctx context.Context, f *flag.FlagSet) {
 	f.StringVar(&cmd.name, "name", "*", "Resource name")
 	f.IntVar(&cmd.maxdepth, "maxdepth", -1, "Max depth")
 	f.BoolVar(&cmd.ref, "i", false, "Print the managed object reference")
+	f.BoolVar(&cmd.id, "I", false, "Print the managed object ID")
 	f.BoolVar(&cmd.long, "l", false, "Long listing format")
 	f.BoolVar(&cmd.parent, "p", false, "Find parent objects")
 }
@@ -153,6 +155,7 @@ The '-type' flag value can be a managed entity type or one of the following alia
 Examples:
   govc find
   govc find -l / # include object type in output
+  govc find -l -I / # include MOID in output
   govc find /dc1 -type c
   govc find vm -name my-vm-*
   govc find . -type n
@@ -214,6 +217,13 @@ func (cmd *find) writeResult(paths []string) error {
 	return cmd.WriteResult(findResult(paths))
 }
 
+func (cmd *find) mo(o types.ManagedObjectReference) string {
+	if cmd.id {
+		return o.Value
+	}
+	return o.String()
+}
+
 func (cmd *find) Run(ctx context.Context, f *flag.FlagSet) error {
 	client, err := cmd.Client()
 	if err != nil {
@@ -225,6 +235,10 @@ func (cmd *find) Run(ctx context.Context, f *flag.FlagSet) error {
 		return err
 	}
 
+	if cmd.id {
+		cmd.ref = true
+	}
+
 	root := client.ServiceContent.RootFolder
 	rootPath := "/"
 
@@ -316,7 +330,7 @@ func (cmd *find) Run(ctx context.Context, f *flag.FlagSet) error {
 
 	printPath := func(o types.ManagedObjectReference, p string) {
 		if cmd.ref && !cmd.long {
-			paths = append(paths, o.String())
+			paths = append(paths, cmd.mo(o))
 			return
 		}
 
@@ -324,7 +338,7 @@ func (cmd *find) Run(ctx context.Context, f *flag.FlagSet) error {
 		if cmd.long {
 			id := strings.TrimPrefix(o.Type, "Vmware")
 			if cmd.ref {
-				id = o.String()
+				id = cmd.mo(o)
 			}
 
 			path = id + "\t" + path
diff --git a/object/common.go b/object/common.go
index 88ce78fc..11064ac0 100644
--- a/object/common.go
+++ b/object/common.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"path"
+	"strings"
 
 	"github.com/vmware/govmomi/property"
 	"github.com/vmware/govmomi/vim25"
@@ -136,13 +137,58 @@ func (c Common) SetCustomValue(ctx context.Context, key string, value string) er
 	return err
 }
 
+var refTypeMap = map[string]string{
+	"datacenter":  "Datacenter",
+	"datastore":   "Datastore",
+	"domain":      "ComputeResource",
+	"dvportgroup": "DistributedVirtualPortgroup",
+	"dvs":         "DistributedVirtualSwitch",
+	"group":       "Folder",
+	"host":        "HostSystem",
+	"network":     "Network",
+	"resgroup":    "ResourcePool",
+	"vm":          "VirtualMachine",
+}
+
+// sub types
+var prefixTypeMap = map[string]struct{ prefix, kind string }{
+	"domain":   {"c", "ClusterComputeResource"}, // extends ComputeResource
+	"group":    {"p", "StoragePod"},             // extends Folder
+	"resgroup": {"v", "VirtualApp"},             // extends ResourcePool
+}
+
+// ReferenceFromString converts a string to ManagedObjectReference.
+// First checks for ManagedObjectReference (MOR), in the format of:
+// "$Type:$ID", e.g. "Datacenter:datacenter-3"
+// Next checks for Managed Object ID (MOID), where type is derived from the ID.
+// For example, "datacenter-3" is converted to a MOR "Datacenter:datacenter-3"
+// Returns nil if string is not in either format.
 func ReferenceFromString(s string) *types.ManagedObjectReference {
 	var ref types.ManagedObjectReference
-	if !ref.FromString(s) {
+	if ref.FromString(s) && mo.IsManagedObjectType(ref.Type) {
+		return &ref
+	}
+
+	id := strings.SplitN(s, "-", 2)
+	if len(id) != 2 {
 		return nil
 	}
-	if mo.IsManagedObjectType(ref.Type) {
-		return &ref
+
+	if kind, ok := refTypeMap[id[0]]; ok {
+		if p, ok := prefixTypeMap[id[0]]; ok {
+			if strings.HasPrefix(id[1], p.prefix) {
+				return &types.ManagedObjectReference{
+					Type:  p.kind,
+					Value: s,
+				}
+			}
+		}
+
+		return &types.ManagedObjectReference{
+			Type:  kind,
+			Value: s,
+		}
 	}
+
 	return nil
 }
diff --git a/simulator/registry.go b/simulator/registry.go
index 8fbfea18..b40b755c 100644
--- a/simulator/registry.go
+++ b/simulator/registry.go
@@ -41,11 +41,13 @@ var refValueMap = map[string]string{
 	"EnvironmentBrowser":             "envbrowser",
 	"HostSystem":                     "host",
 	"ResourcePool":                   "resgroup",
+	"VirtualApp":                     "resgroup-v",
 	"VirtualMachine":                 "vm",
 	"VirtualMachineSnapshot":         "snapshot",
 	"VmwareDistributedVirtualSwitch": "dvs",
 	"DistributedVirtualSwitch":       "dvs",
 	"ClusterComputeResource":         "domain-c",
+	"ComputeResource":                "domain-s",
 	"Folder":                         "group",
 	"StoragePod":                     "group-p",
 }
diff --git a/vim25/mo/type_info.go b/vim25/mo/type_info.go
index 003b56c2..bb2d2e4a 100644
--- a/vim25/mo/type_info.go
+++ b/vim25/mo/type_info.go
@@ -337,6 +337,16 @@ func IsManagedObjectType(kind string) bool {
 	return ok
 }
 
+// Value returns a new mo instance of the given ref Type.
+func Value(ref types.ManagedObjectReference) (Reference, bool) {
+	if rt, ok := t[ref.Type]; ok {
+		val := reflect.New(rt)
+		val.Interface().(Entity).Entity().Self = ref
+		return val.Elem().Interface().(Reference), true
+	}
+	return nil, false
+}
+
 // Field of a ManagedObject in string form.
 type Field struct {
 	Path string


Test patch:
diff --git a/find/finder_test.go b/find/finder_test.go
index 050ca6bd..7d3b5476 100644
--- a/find/finder_test.go
+++ b/find/finder_test.go
@@ -5,7 +5,7 @@ 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
+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,
@@ -105,3 +105,48 @@ func TestFindNetwork(t *testing.T) {
 		}
 	}, model)
 }
+
+func TestFindByID(t *testing.T) {
+	simulator.Test(func(ctx context.Context, c *vim25.Client) {
+		find := find.NewFinder(c)
+
+		vms, err := find.VirtualMachineList(ctx, "*")
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		for _, vm := range vms {
+			ref := vm.Reference()
+			byRef, err := find.VirtualMachine(ctx, ref.String())
+			if err != nil {
+				t.Fatal(err)
+			}
+			if byRef.InventoryPath != vm.InventoryPath {
+				t.Errorf("InventoryPath=%q", byRef.InventoryPath)
+			}
+			if byRef.Reference() != ref {
+				t.Error(byRef.Reference())
+			}
+			_, err = find.VirtualMachine(ctx, ref.String()+"invalid")
+			if err == nil {
+				t.Error("expected error")
+			}
+
+			byID, err := find.VirtualMachine(ctx, ref.Value)
+			if err != nil {
+				t.Error(err)
+			}
+			if byID.InventoryPath != vm.InventoryPath {
+				t.Errorf("InventoryPath=%q", byID.InventoryPath)
+			}
+			if byID.Reference() != ref {
+				t.Error(byID.Reference())
+			}
+			_, err = find.VirtualMachine(ctx, ref.Value+"invalid")
+			if err == nil {
+				t.Error("expected error")
+			}
+
+		}
+	})
+}
diff --git a/govc/test/object.bats b/govc/test/object.bats
index 0fa3928d..1b6ea7fd 100755
--- a/govc/test/object.bats
+++ b/govc/test/object.bats
@@ -493,6 +493,42 @@ EOF
   assert_success 2
 }
 
+@test "collect by id" {
+  vcsim_env -standalone-host 0 -pod 1 -app 1
+
+  run govc find -i /
+  assert_success
+
+  for ref in "${lines[@]}" ; do
+    run govc collect "$ref" name
+    assert_success
+  done
+
+  run govc find -I /
+  assert_success
+
+  for id in "${lines[@]}" ; do
+    run govc collect "$id" name
+    assert_success
+  done
+
+  run govc find -I -type m /
+  assert_success
+
+  for id in "${lines[@]}" ; do
+    run govc vm.info "$id"
+    assert_success
+  done
+
+  run govc find -I -type h /
+  assert_success
+
+  for id in "${lines[@]}" ; do
+    run govc host.info "$id"
+    assert_success
+  done
+}
+
 @test "object.find" {
   vcsim_env -ds 2
 
diff --git a/object/common_test.go b/object/common_test.go
index fe4239fa..eba9a16d 100644
--- a/object/common_test.go
+++ b/object/common_test.go
@@ -18,11 +18,13 @@ package object_test
 
 import (
 	"context"
+	"reflect"
 	"testing"
 
 	"github.com/vmware/govmomi/object"
 	"github.com/vmware/govmomi/simulator"
 	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/types"
 )
 
 func TestCommonName(t *testing.T) {
@@ -63,3 +65,29 @@ func TestObjectName(t *testing.T) {
 		}
 	})
 }
+
+func TestReferenceFromString(t *testing.T) {
+	tests := []struct {
+		in  string
+		out *types.ManagedObjectReference
+	}{
+		{"no:no", nil},
+		{"Datacenter:yes", &types.ManagedObjectReference{Type: "Datacenter", Value: "yes"}},
+		{"datacenter-yes", &types.ManagedObjectReference{Type: "Datacenter", Value: "datacenter-yes"}},
+		{"VirtualMachine:vm-2", &types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-2"}},
+		{"vm-2", &types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-2"}},
+		{"domain-s2", &types.ManagedObjectReference{Type: "ComputeResource", Value: "domain-s2"}},
+		{"domain-c2", &types.ManagedObjectReference{Type: "ClusterComputeResource", Value: "domain-c2"}},
+		{"group-d1", &types.ManagedObjectReference{Type: "Folder", Value: "group-d1"}},
+		{"group-p2", &types.ManagedObjectReference{Type: "StoragePod", Value: "group-p2"}},
+		{"resgroup-42", &types.ManagedObjectReference{Type: "ResourcePool", Value: "resgroup-42"}},
+		{"resgroup-v32", &types.ManagedObjectReference{Type: "VirtualApp", Value: "resgroup-v32"}},
+	}
+
+	for _, test := range tests {
+		ref := object.ReferenceFromString(test.in)
+		if !reflect.DeepEqual(test.out, ref) {
+			t.Errorf("%s: expected %v, got %v", test.in, test.out, ref)
+		}
+	}
+}