Repository: vmware/govmomi
Pull request #3466 (2024-06-11 19:08:52)

Add storage profile related api and govc helpers
api: add pbm.DatastoreMap for finding compatible Datastores
- add pbm.ProfileMap for finding storage profiles by name

govc: add vm.create '-profile' and '-place' flags

api: add storage_specs field to Namespace related structures

govc: add '-storage' flag to namespace.create and namespace.update commands
- The '-cluster', '-library' and '-storage' flags can be specified by name, rather than ID
- The namespace.info command outputs cluster, library and storage by name, rather than ID


dougm: Example of using `-place` to see which Datastore is recommended:
```console
% govc vm.create -place -net "VM Network" -cluster $cluster -iso library:/govc/ubuntu-14.04.6-server-amd64/*.iso ubuntu
Target:       /test-vpx-1717673857-26202-wcp.wcp-sanity/host/test-vpx-1717673857-26202-wcp.wcp-sanity-cluster
  Datastore:  /test-vpx-1717673857-26202-wcp.wcp-sanity/datastore/sharedVmfs-0
  Pool:       /test-vpx-1717673857-26202-wcp.wcp-sanity/host/test-vpx-1717673857-26202-wcp.wcp-sanity-cluster/Resources
  Host:       /test-vpx-1717673857-26202-wcp.wcp-sanity/host/test-vpx-1717673857-26202-wcp.wcp-sanity-cluster/10.182.172.85
```
Note that adding `-xml` or `-dump` flags will output the complete spec input and recommendations response.

Patch:
diff --git a/govc/USAGE.md b/govc/USAGE.md
index 09f59fb2..b48a0c94 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -4311,20 +4311,21 @@ Usage: govc namespace.create [OPTIONS] NAME
 
 Creates a new vSphere Namespace on a Supervisor.
 
-The '-library' and '-vm-class' flags can each be specified multiple times.
+The '-library', '-vmclass' and '-storage' flags can each be specified multiple times.
 
 Examples:
-  govc namespace.create -supervisor=domain-c1 test-namespace
-  govc namespace.create -supervisor=domain-c1 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 test-namespace
-  govc namespace.create -supervisor=domain-c1 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 test-namespace
-  govc namespace.create -supervisor=domain-c1 -vm-class=best-effort-2xlarge test-namespace
-  govc namespace.create -supervisor=domain-c1 -vm-class=best-effort-2xlarge -vm-class best-effort-4xlarge test-namespace
-  govc namespace.create -supervisor=domain-c1 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -vm-class=best-effort-2xlarge -vm-class=best-effort-4xlarge test-namespace
+  govc namespace.create -cluster C1 test-namespace
+  govc namespace.create -cluster C1 -library vmsvc test-namespace
+  govc namespace.create -cluster C1 -library vmsvc -library tkgs test-namespace -storage wcp-policy
+  govc namespace.create -cluster C1 -vmclass best-effort-2xlarge test-namespace
+  govc namespace.create -cluster C1 -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace
+  govc namespace.create -cluster C1 -library vmsvc -library tkgs -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace
 
 Options:
+  -cluster=              Cluster [GOVC_CLUSTER]
   -library=[]            Content library IDs to associate with the vSphere Namespace.
-  -supervisor=           The identifier of the Supervisor.
-  -vm-class=[]           Virtual machine class IDs to associate with the vSphere Namespace.
+  -storage=[]            Storage profile IDs to associate with the vSphere Namespace.
+  -vmclass=[]            Virtual machine class IDs to associate with the vSphere Namespace.
 ```
 
 ## namespace.info
@@ -4476,15 +4477,16 @@ Usage: govc namespace.update [OPTIONS] NAME
 Modifies an existing vSphere Namespace on a Supervisor.
 
 Examples:
-  govc namespace.update -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 test-namespace
-  govc namespace.update -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=617a3ee3-a2ff-4311-9a7c-0016ccf958bd test-namespace
-  govc namespace.update -vm-class=best-effort-2xlarge test-namespace
-  govc namespace.update -vm-class=best-effort-2xlarge -vm-class=best-effort-4xlarge test-namespace
-  govc namespace.update -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=617a3ee3-a2ff-4311-9a7c-0016ccf958bd -vm-class=best-effort-2xlarge -vm-class=best-effort-4xlarge test-namespace
+  govc namespace.update -library vmsvc test-namespace
+  govc namespace.update -library vmsvc -library tkgs -storage wcp-policy test-namespace
+  govc namespace.update -vmclass best-effort-2xlarge test-namespace
+  govc namespace.update -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace
+  govc namespace.update -library vmsvc -library tkgs -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace
 
 Options:
   -library=[]            Content library IDs to associate with the vSphere Namespace.
-  -vm-class=[]           Virtual machine class IDs to associate with the vSphere Namespace.
+  -storage=[]            Storage profile IDs to associate with the vSphere Namespace.
+  -vmclass=[]            Virtual machine class IDs to associate with the vSphere Namespace.
 ```
 
 ## namespace.vmclass.create
@@ -6318,6 +6320,7 @@ Options:
   -net.protocol=         Network device protocol. Applicable to vmxnet3vrdma. Default to 'rocev2'
   -on=true               Power on VM
   -pool=                 Resource pool [GOVC_RESOURCE_POOL]
+  -profile=              Storage profile name or ID
   -version=              ESXi hardware version [2|3|4|5.0|5.1|5.5|6.0|6.5|6.7|6.7.2|7.0|7.0.1|7.0.2|8.0|8.0.1|8.0.2]
 ```
 
diff --git a/govc/namespace/create.go b/govc/namespace/create.go
index d9caa06e..e4e99737 100644
--- a/govc/namespace/create.go
+++ b/govc/namespace/create.go
@@ -26,11 +26,10 @@ import (
 )
 
 type create struct {
-	*flags.ClientFlag
+	*flags.ClusterFlag
+	*namespaceFlag
 
-	libraries flags.StringList
-	vmClasses flags.StringList
-	spec      namespace.NamespacesInstanceCreateSpec
+	spec namespace.NamespacesInstanceCreateSpec
 }
 
 func init() {
@@ -38,37 +37,49 @@ func init() {
 }
 
 func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
-	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
-	cmd.ClientFlag.Register(ctx, f)
+	cmd.ClusterFlag, ctx = flags.NewClusterFlag(ctx)
+	cmd.ClusterFlag.Register(ctx, f)
 
-	f.StringVar(&cmd.spec.Cluster, "supervisor", "", "The identifier of the Supervisor.")
-	f.Var(&cmd.libraries, "library", "Content library IDs to associate with the vSphere Namespace.")
-	f.Var(&cmd.vmClasses, "vm-class", "Virtual machine class IDs to associate with the vSphere Namespace.")
-}
-
-func (*create) Usage() string {
-	return "NAME"
+	cmd.namespaceFlag = &namespaceFlag{}
+	cmd.namespaceFlag.Register(ctx, f)
 }
 
 func (cmd *create) Process(ctx context.Context) error {
-	cmd.spec.VmServiceSpec.ContentLibraries = cmd.libraries
-	cmd.spec.VmServiceSpec.VmClasses = cmd.vmClasses
+	if err := cmd.ClusterFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.namespaceFlag.Process(ctx); err != nil {
+		return err
+	}
 
-	return cmd.ClientFlag.Process(ctx)
+	cluster, err := cmd.Cluster()
+	if err != nil {
+		return err
+	}
+
+	cmd.spec.Cluster = cluster.Reference().Value
+	cmd.spec.StorageSpecs = cmd.storageSpec()
+	cmd.spec.VmServiceSpec = cmd.vmServiceSpec()
+
+	return nil
+}
+
+func (*create) Usage() string {
+	return "NAME"
 }
 
 func (cmd *create) Description() string {
 	return `Creates a new vSphere Namespace on a Supervisor.
 
-The '-library' and '-vm-class' flags can each be specified multiple times.
+The '-library', '-vmclass' and '-storage' flags can each be specified multiple times.
 
 Examples:
-  govc namespace.create -supervisor=domain-c1 test-namespace
-  govc namespace.create -supervisor=domain-c1 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 test-namespace
-  govc namespace.create -supervisor=domain-c1 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 test-namespace
-  govc namespace.create -supervisor=domain-c1 -vm-class=best-effort-2xlarge test-namespace
-  govc namespace.create -supervisor=domain-c1 -vm-class=best-effort-2xlarge -vm-class best-effort-4xlarge test-namespace
-  govc namespace.create -supervisor=domain-c1 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -vm-class=best-effort-2xlarge -vm-class=best-effort-4xlarge test-namespace`
+  govc namespace.create -cluster C1 test-namespace
+  govc namespace.create -cluster C1 -library vmsvc test-namespace
+  govc namespace.create -cluster C1 -library vmsvc -library tkgs test-namespace -storage wcp-policy
+  govc namespace.create -cluster C1 -vmclass best-effort-2xlarge test-namespace
+  govc namespace.create -cluster C1 -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace
+  govc namespace.create -cluster C1 -library vmsvc -library tkgs -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace`
 }
 
 func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
diff --git a/govc/namespace/flag.go b/govc/namespace/flag.go
new file mode 100644
index 00000000..a0db25b3
--- /dev/null
+++ b/govc/namespace/flag.go
@@ -0,0 +1,93 @@
+/*
+Copyright (c) 2024-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
+
+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 namespace
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/namespace"
+)
+
+type namespaceFlag struct {
+	*flags.ClientFlag
+
+	library flags.StringList
+	vmclass flags.StringList
+	storage flags.StringList
+}
+
+func (ns *namespaceFlag) Register(ctx context.Context, f *flag.FlagSet) {
+	ns.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	ns.ClientFlag.Register(ctx, f)
+
+	f.Var(&ns.library, "library", "Content library IDs to associate with the vSphere Namespace.")
+	f.Var(&ns.vmclass, "vmclass", "Virtual machine class IDs to associate with the vSphere Namespace.")
+	f.Var(&ns.storage, "storage", "Storage profile IDs to associate with the vSphere Namespace.")
+}
+
+func (ns *namespaceFlag) Process(ctx context.Context) error {
+	if err := ns.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	rc, err := ns.RestClient()
+	if err != nil {
+		return err
+	}
+
+	for i, name := range ns.library {
+		l, err := flags.ContentLibrary(ctx, rc, name)
+		if err == nil {
+			ns.library[i] = l.ID
+		}
+	}
+
+	pc, err := ns.PbmClient()
+	if err != nil {
+		return err
+	}
+
+	m, err := pc.ProfileMap(ctx)
+	if err != nil {
+		return err
+	}
+
+	for i, name := range ns.storage {
+		if n, ok := m.Name[name]; ok {
+			ns.storage[i] = n.GetPbmProfile().ProfileId.UniqueId
+		}
+	}
+
+	return nil
+}
+
+func (ns *namespaceFlag) storageSpec() []namespace.StorageSpec {
+	s := make([]namespace.StorageSpec, len(ns.storage))
+	for i := range ns.storage {
+		s[i].Policy = ns.storage[i]
+	}
+	return s
+}
+
+func (ns *namespaceFlag) vmServiceSpec() namespace.VmServiceSpec {
+	return namespace.VmServiceSpec{
+		ContentLibraries: ns.library,
+		VmClasses:        ns.vmclass,
+	}
+}
diff --git a/govc/namespace/info.go b/govc/namespace/info.go
index 34cc2216..fadcd7b1 100644
--- a/govc/namespace/info.go
+++ b/govc/namespace/info.go
@@ -24,20 +24,72 @@ import (
 	"strings"
 	"text/tabwriter"
 
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/vapi/library"
 	"github.com/vmware/govmomi/vapi/namespace"
+	"github.com/vmware/govmomi/vim25/types"
 
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
 )
 
-type infoResult namespace.NamespacesInstanceInfo
+type infoResult struct {
+	namespace.NamespacesInstanceInfo
+
+	ctx context.Context
+	cmd *info
+}
 
 func (r infoResult) Write(w io.Writer) error {
+	c, err := r.cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	rc, err := r.cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	l := library.NewManager(rc)
+
+	pc, err := r.cmd.PbmClient()
+	if err != nil {
+		return err
+	}
+
+	var ids []string
+	for _, s := range r.StorageSpecs {
+		ids = append(ids, s.Policy)
+	}
+	m, err := pc.ProfileMap(r.ctx, ids...)
+	if err != nil {
+		return err
+	}
+
 	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
-	fmt.Fprintf(tw, "Cluster:\t%s\n", r.ClusterId)
+
+	cluster := types.ManagedObjectReference{Type: "ClusterComputeResource", Value: r.ClusterId}
+	path, err := find.InventoryPath(r.ctx, c, cluster)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(tw, "Cluster:\t%s\n", path)
 	fmt.Fprintf(tw, "Status:\t%s\n", r.ConfigStatus)
 	fmt.Fprintf(tw, "VM Classes:\t%s\n", strings.Join(r.VmServiceSpec.VmClasses, ","))
-	fmt.Fprintf(tw, "VM Libraries:\t%s\n", strings.Join(r.VmServiceSpec.ContentLibraries, ","))
+
+	for _, s := range r.VmServiceSpec.ContentLibraries {
+		info, err := l.GetLibraryByID(r.ctx, s)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(tw, "Content Library:\t%s\n", info.Name)
+	}
+
+	for _, s := range r.StorageSpecs {
+		fmt.Fprintf(tw, "Storage Policy:\t%s\n", m.Name[s.Policy].GetPbmProfile().Name)
+	}
+
 	return tw.Flush()
 }
 
@@ -97,5 +149,5 @@ func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
 		return err
 	}
 
-	return cmd.WriteResult(infoResult(d))
+	return cmd.WriteResult(&infoResult{d, ctx, cmd})
 }
diff --git a/govc/namespace/update.go b/govc/namespace/update.go
index 5fbdd863..ab353210 100644
--- a/govc/namespace/update.go
+++ b/govc/namespace/update.go
@@ -21,16 +21,13 @@ import (
 	"flag"
 
 	"github.com/vmware/govmomi/govc/cli"
-	"github.com/vmware/govmomi/govc/flags"
 	"github.com/vmware/govmomi/vapi/namespace"
 )
 
 type update struct {
-	*flags.ClientFlag
+	*namespaceFlag
 
-	libraries flags.StringList
-	vmClasses flags.StringList
-	spec      namespace.NamespacesInstanceUpdateSpec
+	spec namespace.NamespacesInstanceUpdateSpec
 }
 
 func init() {
@@ -38,18 +35,19 @@ func init() {
 }
 
 func (cmd *update) Register(ctx context.Context, f *flag.FlagSet) {
-	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
-	cmd.ClientFlag.Register(ctx, f)
-
-	f.Var(&cmd.libraries, "library", "Content library IDs to associate with the vSphere Namespace.")
-	f.Var(&cmd.vmClasses, "vm-class", "Virtual machine class IDs to associate with the vSphere Namespace.")
+	cmd.namespaceFlag = &namespaceFlag{}
+	cmd.namespaceFlag.Register(ctx, f)
 }
 
 func (cmd *update) Process(ctx context.Context) error {
-	cmd.spec.VmServiceSpec.ContentLibraries = cmd.libraries
-	cmd.spec.VmServiceSpec.VmClasses = cmd.vmClasses
+	if err := cmd.namespaceFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	cmd.spec.StorageSpecs = cmd.storageSpec()
+	cmd.spec.VmServiceSpec = cmd.vmServiceSpec()
 
-	return cmd.ClientFlag.Process(ctx)
+	return nil
 }
 
 func (cmd *update) Usage() string {
@@ -60,11 +58,11 @@ func (cmd *update) Description() string {
 	return `Modifies an existing vSphere Namespace on a Supervisor.
 
 Examples:
-  govc namespace.update -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 test-namespace
-  govc namespace.update -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=617a3ee3-a2ff-4311-9a7c-0016ccf958bd test-namespace
-  govc namespace.update -vm-class=best-effort-2xlarge test-namespace
-  govc namespace.update -vm-class=best-effort-2xlarge -vm-class=best-effort-4xlarge test-namespace
-  govc namespace.update -library=dca9cc16-9460-4da0-802c-4aa148ac6cf7 -library=617a3ee3-a2ff-4311-9a7c-0016ccf958bd -vm-class=best-effort-2xlarge -vm-class=best-effort-4xlarge test-namespace`
+  govc namespace.update -library vmsvc test-namespace
+  govc namespace.update -library vmsvc -library tkgs -storage wcp-policy test-namespace
+  govc namespace.update -vmclass best-effort-2xlarge test-namespace
+  govc namespace.update -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace
+  govc namespace.update -library vmsvc -library tkgs -vmclass best-effort-2xlarge -vmclass best-effort-4xlarge test-namespace`
 }
 
 func (cmd *update) Run(ctx context.Context, f *flag.FlagSet) error {
diff --git a/govc/storage/policy/info.go b/govc/storage/policy/info.go
index c1d6db44..99a464f1 100644
--- a/govc/storage/policy/info.go
+++ b/govc/storage/policy/info.go
@@ -29,7 +29,6 @@ import (
 	"github.com/vmware/govmomi/govc/flags"
 	"github.com/vmware/govmomi/pbm/types"
 	"github.com/vmware/govmomi/property"
-	"github.com/vmware/govmomi/view"
 	vim "github.com/vmware/govmomi/vim25/types"
 )
 
@@ -120,38 +119,18 @@ func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
 		return err
 	}
 
-	profiles, err := ListProfiles(ctx, c, f.Arg(0))
-	if err != nil {
-		return err
-	}
-
 	pc := property.DefaultCollector(vc)
-	kind := []string{"Datastore"}
-	m := view.NewManager(vc)
 
-	v, err := m.CreateContainerView(ctx, vc.ServiceContent.RootFolder, kind, true)
+	profiles, err := ListProfiles(ctx, c, f.Arg(0))
 	if err != nil {
 		return err
 	}
 
-	var content []vim.ObjectContent
-	err = v.Retrieve(ctx, kind, []string{"name"}, &content)
-	_ = v.Destroy(ctx)
+	ds, err := c.DatastoreMap(ctx, vc, vc.ServiceContent.RootFolder)
 	if err != nil {
 		return err
 	}
 
-	dsmap := make(map[string]string)
-	var hubs []types.PbmPlacementHub
-
-	for _, ds := range content {
-		hubs = append(hubs, types.PbmPlacementHub{
-			HubType: ds.Obj.Type,
-			HubId:   ds.Obj.Value,
-		})
-		dsmap[ds.Obj.Value] = ds.PropSet[0].Val.(string)
-	}
-
 	var policies []Policy
 
 	for _, profile := range profiles {
@@ -183,6 +162,7 @@ func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
 				}
 			}
 
+			var content []vim.ObjectContent
 			err = pc.Retrieve(ctx, refs, []string{"name"}, &content)
 			if err != nil {
 				return err
@@ -200,13 +180,13 @@ func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
 				},
 			}
 
-			res, err := c.CheckRequirements(ctx, hubs, nil, req)
+			res, err := c.CheckRequirements(ctx, ds.PlacementHub, nil, req)
 			if err != nil {
 				return err
 			}
 
 			for _, hub := range res.CompatibleDatastores() {
-				policy.CompatibleDatastores = append(policy.CompatibleDatastores, dsmap[hub.HubId])
+				policy.CompatibleDatastores = append(policy.CompatibleDatastores, ds.Name[hub.HubId])
 			}
 		}
 
diff --git a/govc/storage/policy/ls.go b/govc/storage/policy/ls.go
index b42f9ec5..83002fbe 100644
--- a/govc/storage/policy/ls.go
+++ b/govc/storage/policy/ls.go
@@ -74,33 +74,17 @@ Examples:
 }
 
 func ListProfiles(ctx context.Context, c *pbm.Client, name string) ([]types.BasePbmProfile, error) {
-	rtype := types.PbmProfileResourceType{
-		ResourceType: string(types.PbmProfileResourceTypeEnumSTORAGE),
-	}
-
-	category := types.PbmProfileCategoryEnumREQUIREMENT
-
-	ids, err := c.QueryProfile(ctx, rtype, string(category))
+	m, err := c.ProfileMap(ctx)
 	if err != nil {
 		return nil, err
 	}
-
-	profiles, err := c.RetrieveContent(ctx, ids)
-	if err != nil {
-		return nil, err
-	}
-
 	if name == "" {
-		return profiles, nil
+		return m.Profile, nil
 	}
-
-	for _, p := range profiles {
-		if p.GetPbmProfile().Name == name {
-			return []types.BasePbmProfile{p}, nil
-		}
+	if p, ok := m.Name[name]; ok {
+		return []types.BasePbmProfile{p}, nil
 	}
-
-	return c.RetrieveContent(ctx, []types.PbmProfileId{{UniqueId: name}})
+	return nil, fmt.Errorf("profile %q not found", name)
 }
 
 type lsResult struct {
diff --git a/govc/vm/create.go b/govc/vm/create.go
index a2eb6cc8..46c3a57d 100644
--- a/govc/vm/create.go
+++ b/govc/vm/create.go
@@ -20,8 +20,11 @@ import (
 	"context"
 	"flag"
 	"fmt"
+	"io"
 	"strings"
+	"text/tabwriter"
 
+	"github.com/vmware/govmomi/find"
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
 	"github.com/vmware/govmomi/object"
@@ -60,6 +63,8 @@ type create struct {
 	annotation string
 	firmware   string
 	version    string
+	place      bool
+	profile    string
 
 	iso              string
 	isoDatastoreFlag *flags.DatastoreFlag
@@ -122,8 +127,11 @@ func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
 	f.BoolVar(&cmd.force, "force", false, "Create VM if vmx already exists")
 	f.StringVar(&cmd.controller, "disk.controller", "scsi", "Disk controller type")
 	f.StringVar(&cmd.annotation, "annotation", "", "VM description")
-
 	f.StringVar(&cmd.firmware, "firmware", FirmwareTypes[0], FirmwareUsage)
+	f.StringVar(&cmd.profile, "profile", "", "Storage profile name or ID")
+	if cli.ShowUnreleased() {
+		f.BoolVar(&cmd.place, "place", false, "Place VM without creating")
+	}
 
 	esxiVersions := types.GetESXiVersions()
 	esxiVersionStrings := make([]string, len(esxiVersions))
@@ -299,7 +307,9 @@ func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
 	if err != nil {
 		return err
 	}
-
+	if cmd.place {
+		return nil
+	}
 	info, err := task.WaitForResult(ctx, nil)
 	if err != nil {
 		return err
@@ -322,6 +332,61 @@ func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
 	return nil
 }
 
+type place struct {
+	Spec            types.PlacementSpec           `json:"spec"`
+	Recommendations []types.ClusterRecommendation `json:"recommendations"`
+
+	ctx context.Context
+	cmd *create
+}
+
+func (p *place) Dump() interface{} {
+	return p.Recommendations
+}
+
+func (p *place) action(w io.Writer, r types.ClusterRecommendation, a *types.PlacementAction) error {
+	spec := a.RelocateSpec
+	if spec == nil {
+		return nil
+	}
+
+	fields := []struct {
+		name string
+		moid *types.ManagedObjectReference
+	}{
+		{"Target", r.Target},
+		{"  Folder", spec.Folder},
+		{"  Datastore", spec.Datastore},
+		{"  Pool", spec.Pool},
+		{"  Host", spec.Host},
+	}
+
+	for _, f := range fields {
+		if f.moid == nil {
+			continue
+		}
+		path, err := find.InventoryPath(p.ctx, p.cmd.Client, *f.moid)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(w, "%s:\t%s\n", f.name, path)
+	}
+
+	return nil
+}
+
+func (p *place) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	for _, r := range p.Recommendations {
+		for _, a := range r.Action {
+			p.action(tw, r, a.(*types.PlacementAction))
+		}
+	}
+
+	return tw.Flush()
+}
+
 func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
 	var devices object.VirtualDeviceList
 	var err error
@@ -346,6 +411,24 @@ func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
 		Version:    cmd.version,
 	}
 
+	if cmd.profile != "" {
+		c, err := cmd.PbmClient()
+		if err != nil {
+			return nil, err
+		}
+		m, err := c.ProfileMap(ctx)
+		if err != nil {
+			return nil, err
+		}
+		p, ok := m.Name[cmd.profile]
+		if !ok {
+			return nil, fmt.Errorf("profile %q not found", cmd.profile)
+		}
+		spec.VmProfile = []types.BaseVirtualMachineProfileSpec{&types.VirtualMachineDefinedProfileSpec{
+			ProfileId: p.GetPbmProfile().ProfileId.UniqueId,
+		}}
+	}
+
 	devices, err = cmd.addStorage(nil)
 	if err != nil {
 		return nil, err
@@ -384,6 +467,9 @@ func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
 		}
 
 		recs := result.Recommendations
+		if cmd.place {
+			return nil, cmd.WriteResult(&place{pspec, recs, ctx, cmd})
+		}
 		if len(recs) == 0 {
 			return nil, fmt.Errorf("no cluster recommendations")
 		}
diff --git a/pbm/pbm_util.go b/pbm/pbm_util.go
index d773b8db..5dcd1457 100644
--- a/pbm/pbm_util.go
+++ b/pbm/pbm_util.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2017 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-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,
@@ -17,11 +17,17 @@ limitations under the License.
 package pbm
 
 import (
+	"context"
 	"fmt"
 	"strconv"
 	"strings"
 
 	"github.com/vmware/govmomi/pbm/types"
+	"github.com/vmware/govmomi/property"
+	"github.com/vmware/govmomi/view"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/mo"
+	vim "github.com/vmware/govmomi/vim25/types"
 )
 
 // A struct to capture pbm create spec details.
@@ -146,3 +152,100 @@ func verifyPropertyValueIsBoolean(propertyValue string, dataType string) (bool,
 	}
 	return val, nil
 }
+
+// ProfileMap contains a map of storage profiles by name.
+type ProfileMap struct {
+	Name    map[string]types.BasePbmProfile
+	Profile []types.BasePbmProfile
+}
+
+// ProfileMap builds a map of storage profiles by name.
+func (c *Client) ProfileMap(ctx context.Context, uid ...string) (*ProfileMap, error) {
+	m := &ProfileMap{Name: make(map[string]types.BasePbmProfile)}
+
+	rtype := types.PbmProfileResourceType{
+		ResourceType: string(types.PbmProfileResourceTypeEnumSTORAGE),
+	}
+
+	category := types.PbmProfileCategoryEnumREQUIREMENT
+
+	var ids []types.PbmProfileId
+	if len(uid) == 0 {
+		var err error
+		ids, err = c.QueryProfile(ctx, rtype, string(category))
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		ids = make([]types.PbmProfileId, len(uid))
+		for i, id := range uid {
+			ids[i].UniqueId = id
+		}
+	}
+
+	profiles, err := c.RetrieveContent(ctx, ids)
+	if err != nil {
+		return nil, err
+	}
+	m.Profile = profiles
+
+	for _, p := range profiles {
+		base := p.GetPbmProfile()
+		m.Name[base.Name] = p
+		m.Name[base.ProfileId.UniqueId] = p
+	}
+
+	return m, nil
+}
+
+// DatastoreMap contains a map of Datastore by name.
+type DatastoreMap struct {
+	Name         map[string]string
+	PlacementHub []types.PbmPlacementHub
+}
+
+// DatastoreMap returns a map of Datastore by name.
+// The root reference can be a ClusterComputeResource or Folder.
+func (c *Client) DatastoreMap(ctx context.Context, vc *vim25.Client, root vim.ManagedObjectReference) (*DatastoreMap, error) {
+	m := &DatastoreMap{Name: make(map[string]string)}
+
+	prop := []string{"name"}
+	var content []vim.ObjectContent
+
+	if root.Type == "ClusterComputeResource" {
+		pc := property.DefaultCollector(vc)
+		var cluster mo.ClusterComputeResource
+
+		if err := pc.RetrieveOne(ctx, root, []string{"datastore"}, &cluster); err != nil {
+			return nil, err
+		}
+
+		if err := pc.Retrieve(ctx, cluster.Datastore, prop, &content); err != nil {
+			return nil, err
+		}
+	} else {
+		kind := []string{"Datastore"}
+		m := view.NewManager(vc)
+
+		v, err := m.CreateContainerView(ctx, root, kind, true)
+		if err != nil {
+			return nil, err
+		}
+
+		err = v.Retrieve(ctx, kind, prop, &content)
+		_ = v.Destroy(ctx)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for _, item := range content {
+		m.PlacementHub = append(m.PlacementHub, types.PbmPlacementHub{
+			HubType: item.Obj.Type,
+			HubId:   item.Obj.Value,
+		})
+		m.Name[item.Obj.Value] = item.PropSet[0].Val.(string)
+	}
+
+	return m, nil
+}
diff --git a/vapi/namespace/namespace.go b/vapi/namespace/namespace.go
index f54d94d9..b2b7349f 100644
--- a/vapi/namespace/namespace.go
+++ b/vapi/namespace/namespace.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2020-2022 VMware, Inc. All Rights Reserved.
+Copyright (c) 2020-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,
@@ -680,6 +680,7 @@ type NamespacesInstanceInfo struct {
 	SelfServiceNamespace bool                    `json:"self_service_namespace,omitempty"`
 	Messages             []LocalizableMessage    `json:"message"`
 	VmServiceSpec        VmServiceSpec           `json:"vm_service_spec,omitempty"`
+	StorageSpecs         []StorageSpec           `json:"storage_specs,omitempty"`
 }
 
 // NamespacesInstanceCreateSpec https://developer.vmware.com/apis/vsphere-automation/v7.0U3/vcenter/data-structures/Namespaces/Instances/CreateSpec/
@@ -687,6 +688,7 @@ type NamespacesInstanceCreateSpec struct {
 	Cluster       string        `json:"cluster"`
 	Namespace     string        `json:"namespace"`
 	VmServiceSpec VmServiceSpec `json:"vm_service_spec,omitempty"`
+	StorageSpecs  []StorageSpec `json:"storage_specs,omitempty"`
 }
 
 // VmServiceSpec https://developer.vmware.com/apis/vsphere-automation/v7.0U3/vcenter/data-structures/Namespaces/Instances/VMServiceSpec/
@@ -695,9 +697,16 @@ type VmServiceSpec struct {
 	VmClasses        []string `json:"vm_classes,omitempty"`
 }
 
+// StorageSpec https://developer.broadcom.com/apis/vsphere-automation-api/v7.0U3/vcenter/data-structures/Namespaces_Instances_StorageSpec/
+type StorageSpec struct {
+	Policy string `json:"policy"`
+	Limit  int64  `json:"limit,omitempty"`
+}
+
 // NamespacesInstanceUpdateSpec https://developer.vmware.com/apis/vsphere-automation/v7.0U3/vcenter/data-structures/Namespaces/Instances/UpdateSpec/
 type NamespacesInstanceUpdateSpec struct {
 	VmServiceSpec VmServiceSpec `json:"vm_service_spec,omitempty"`
+	StorageSpecs  []StorageSpec `json:"storage_specs,omitempty"`
 }
 
 // ListNamespaces https://developer.vmware.com/apis/vsphere-automation/v7.0U3/vcenter/api/vcenter/namespaces/instances/get/


Test patch:
diff --git a/govc/test/namespace.bats b/govc/test/namespace.bats
index eb460af6..6755d565 100755
--- a/govc/test/namespace.bats
+++ b/govc/test/namespace.bats
@@ -117,14 +117,15 @@ load test_helper
 @test "namespace.create" {
     vcsim_env
 
-    run govc namespace.create -supervisor=domain-c1 test-namespace-1
+    run govc namespace.create -cluster DC0_C0 test-namespace-1
     assert_success
 
     ns=$(govc namespace.info -json test-namespace-1 | jq)
-    assert_equal "domain-c1" $(echo $ns | jq -r '."cluster"')
+    id=$(govc find -i -maxdepth 0 /DC0/host/DC0_C0 | cut -d: -f2)
+    assert_equal "$id" $(echo $ns | jq -r '."cluster"')
     assert_equal "0" $(echo $ns | jq -r '."vm_service_spec"."content_libraries"' | jq length)
 
-    run govc namespace.create -supervisor=domain-c1 -library=lib1 -library=lib2 test-namespace-2
+    run govc namespace.create -cluster DC0_C0 -library=lib1 -library=lib2 test-namespace-2
     assert_success
 
     ns=$(govc namespace.info -json test-namespace-2 | jq)
@@ -134,9 +135,9 @@ load test_helper
 @test "namespace.update" {
     vcsim_env
 
-    govc namespace.create -supervisor=domain-c1 test-namespace-1
+    govc namespace.create -cluster DC0_C0 test-namespace-1
 
-    run govc namespace.update -library=lib1 -library=lib2 -vm-class=class1 test-namespace-1
+    run govc namespace.update -library=lib1 -library=lib2 -vmclass=class1 test-namespace-1
     assert_success
 
     ns=$(govc namespace.info -json test-namespace-1 | jq)
@@ -154,7 +155,7 @@ load test_helper
     assert_equal "lib3" $(echo $ns | jq -r '."vm_service_spec"."content_libraries"[0]')
     assert_equal "0" $(echo $ns | jq -r '."vm_service_spec"."vm_classes"' | jq length)
 
-    run govc namespace.update -vm-class=class3 test-namespace-1
+    run govc namespace.update -vmclass=class3 test-namespace-1
     assert_success
 
     ns=$(govc namespace.info -json test-namespace-1 | jq)
@@ -162,7 +163,7 @@ load test_helper
     assert_equal "1" $(echo $ns | jq -r '."vm_service_spec"."vm_classes"' | jq length)
     assert_equal "class3" $(echo $ns | jq -r '."vm_service_spec"."vm_classes"[0]')
 
-    run govc namespace.update -library=lib4 -vm-class=class4 test-namespace-1
+    run govc namespace.update -library=lib4 -vmclass=class4 test-namespace-1
     assert_success
 
     ns=$(govc namespace.info -json test-namespace-1 | jq)
@@ -171,7 +172,7 @@ load test_helper
     assert_equal "1" $(echo $ns | jq -r '."vm_service_spec"."vm_classes"' | jq length)
     assert_equal "class4" $(echo $ns | jq -r '."vm_service_spec"."vm_classes"[0]')
 
-    run govc namespace.update -library=lib1 -library=lib2 -vm-class=class1 non-existing-namespace
+    run govc namespace.update -library=lib1 -library=lib2 -vmclass=class1 non-existing-namespace
     assert_failure
     assert_matches "404 Not Found"
 }
@@ -179,10 +180,11 @@ load test_helper
 @test "namespace.info" {
     vcsim_env
 
-    govc namespace.create -supervisor=domain-c1 test-namespace-1
+    govc namespace.create -cluster DC0_C0 test-namespace-1
 
     ns=$(govc namespace.info -json test-namespace-1 | jq)
-    assert_equal "domain-c1" $(echo $ns | jq -r '."cluster"')
+    id=$(govc find -i -maxdepth 0 /DC0/host/DC0_C0 | cut -d: -f2)
+    assert_equal "$id" $(echo $ns | jq -r '."cluster"')
 
     run govc namespace.info test-namespace-1
     assert_success
@@ -201,7 +203,7 @@ load test_helper
     ls=$(govc namespace.ls -json)
     assert_equal "0" $(echo $ls | jq length)
 
-    govc namespace.create -supervisor=domain-c1 test-namespace-1
+    govc namespace.create -cluster DC0_C0 test-namespace-1
     ls=$(govc namespace.ls -json)
     assert_equal "1" $(echo $ls | jq length)
     assert_equal "test-namespace-1" $(echo $ls | jq -r '.[0]."namespace"')
@@ -209,12 +211,13 @@ load test_helper
     run govc namespace.ls
     assert_success test-namespace-1
 
-    govc namespace.create -supervisor=domain-c1 test-namespace-2
+    govc namespace.create -cluster DC0_C0 test-namespace-2
     ls=$(govc namespace.ls -json)
     assert_equal "2" $(echo $ls | jq length)
-    assert_equal "domain-c1" $(echo $ls | jq -r '.[0]."cluster"')
+    id=$(govc find -i -maxdepth 0 /DC0/host/DC0_C0 | cut -d: -f2)
+    assert_equal "$id" $(echo $ls | jq -r '.[0]."cluster"')
     assert_matches "test-namespace-[0-9]+" $(echo $ls | jq -r '.[0]."namespace"')
-    assert_equal "domain-c1" $(echo $ls | jq -r '.[1]."cluster"')
+    assert_equal "$id" $(echo $ls | jq -r '.[1]."cluster"')
     assert_matches "test-namespace-[0-9]+" $(echo $ls | jq -r '.[1]."namespace"')
 }
 
@@ -225,7 +228,8 @@ load test_helper
     assert_failure
     assert_matches "404 Not Found"
 
-    govc namespace.create -supervisor=domain-c1 test-namespace-1
+    run govc namespace.create -cluster DC0_C0 test-namespace-1
+    assert_success
 
     run govc namespace.rm test-namespace-1
     assert_success