Repository: vmware/govmomi
Pull request #3368 (2024-02-22 14:25:21)

Add vAPI bindings for vLCM - offline depots and cluster baseline images
## Description

This PR adds bindings for parts of the vLCM REST API. More specifically for the methods that allow the manipulation of offline depots and of cluster baseline images.

These bindings are necessary for the implementation of vLCM automation in https://github.com/hashicorp/terraform-provider-vsphere

Links to the doc entries for each method included in this change are provided in the source code.

## 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?

Added handlers in the simulator and a new bats test
I have also tested the new bindings on a live environment. Below you can find a scenario that goes through every new method in order:

Preparation steps
1. Deploy a vCenter server and an ESXi host
2. Move the host into a cluster
3. Upload a software bundle (drivers, vibs, etc.) to a location where it is accessible via http/https

Test steps. Call the following methods
1. "CreateOfflineDepot" - to create a new depot with your software bundle
2. "GetOfflineDepots" - to see the list of depots. Copy the identifier of your new depot
3. [Optional] "GetOfflineDepot" - to see the details of an individual depot
4. "GetOfflineDepotContent" - to obtain the identifier and version of your component (driver/vib/something)
5. [Optional] "ListSoftwareDrafts" - to see if there are any drafts
6. [Optional] "DeleteSoftwareDraft" - to clean up any unfinished drafts
7. "CreateSoftwareDraft" - to open a new baseline draft
8. "UpdateSoftwareDraftComponents" - to add your components to the draft
9. [Optional] "GetSoftwareDraftComponent" - to check if everything is okay in the draft
10. "CommitSoftwareDraft" - to commit and apply your draft
11. "CreateSoftwareDraft" - to create a second draft
12. "RemoveSoftwareDraftComponents" - to remove your component from the image
13. "CommitSoftwareDraft" - to commit and apply your second draft
14. "DeleteOfflineDepot" - to delete the offline depot from step 1

## Checklist:

- [X] My code follows the `CONTRIBUTION` [guidelines] of this project
- [X] 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
- [x] Any dependent changes have been merged

[guidelines]: https://github.com/vmware/govmomi/blob/main/CONTRIBUTING.md



Patch:
diff --git a/go.mod b/go.mod
index dbbe0a13..2d238981 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,6 @@ require (
 	github.com/kr/pretty v0.3.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/mod v0.5.1 // indirect
 	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 781025d7..cf2c8cfa 100644
--- a/go.sum
+++ b/go.sum
@@ -31,8 +31,6 @@ github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728 h1:sH9mEk+fly
 github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk=
 github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
 github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
-golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/govc/USAGE.md b/govc/USAGE.md
index c00ff54d..76c1235c 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -41,6 +41,15 @@ but appear via `govc $cmd -h`:
  - [cluster.add](#clusteradd)
  - [cluster.change](#clusterchange)
  - [cluster.create](#clustercreate)
+ - [cluster.draft.commit](#clusterdraftcommit)
+ - [cluster.draft.component.add](#clusterdraftcomponentadd)
+ - [cluster.draft.component.info](#clusterdraftcomponentinfo)
+ - [cluster.draft.component.ls](#clusterdraftcomponentls)
+ - [cluster.draft.component.rm](#clusterdraftcomponentrm)
+ - [cluster.draft.create](#clusterdraftcreate)
+ - [cluster.draft.info](#clusterdraftinfo)
+ - [cluster.draft.ls](#clusterdraftls)
+ - [cluster.draft.rm](#clusterdraftrm)
  - [cluster.group.change](#clustergroupchange)
  - [cluster.group.create](#clustergroupcreate)
  - [cluster.group.ls](#clustergroupls)
@@ -334,6 +343,10 @@ but appear via `govc $cmd -h`:
  - [vcsa.shutdown.poweroff](#vcsashutdownpoweroff)
  - [vcsa.shutdown.reboot](#vcsashutdownreboot)
  - [version](#version)
+ - [vlcm.depot.offline.create](#vlcmdepotofflinecreate)
+ - [vlcm.depot.offline.info](#vlcmdepotofflineinfo)
+ - [vlcm.depot.offline.ls](#vlcmdepotofflinels)
+ - [vlcm.depot.offline.rm](#vlcmdepotofflinerm)
  - [vm.change](#vmchange)
  - [vm.clone](#vmclone)
  - [vm.console](#vmconsole)
@@ -489,6 +502,149 @@ Options:
   -folder=               Inventory folder [GOVC_FOLDER]
 ```
 
+## cluster.draft.commit
+
+```
+Usage: govc cluster.draft.commit [OPTIONS] CLUSTER
+
+Commits the provided software draft.  
+
+Execution will block the terminal for the duration of the task. 
+
+Examples:
+  govc cluster.draft.commit -cluster-id=domain-c21 -draft-id=13
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -draft-id=             The identifier of the software draft.
+```
+
+## cluster.draft.component.add
+
+```
+Usage: govc cluster.draft.component.add [OPTIONS] CLUSTER
+
+Adds a new component to the software draft.  
+
+Examples:
+  govc cluster.draft.component.add -cluster-id=domain-c21 -draft-id=13 -component-id=NVD-AIE-800 -component-version=550.54.10-1OEM.800.1.0.20613240
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -component-id=         The identifier of the software component.
+  -component-version=    The version of the software component.
+  -draft-id=             The identifier of the software draft.
+```
+
+## cluster.draft.component.info
+
+```
+Usage: govc cluster.draft.component.info [OPTIONS] CLUSTER
+
+Displays the details of a component in a software draft.  
+
+Examples:
+  govc cluster.draft.component.info -cluster-id=domain-c21 -draft-id=13 -component-id=NVD-AIE-800
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -component-id=         The identifier of the software component.
+  -draft-id=             The identifier of the software draft.
+```
+
+## cluster.draft.component.ls
+
+```
+Usage: govc cluster.draft.component.ls [OPTIONS] CLUSTER
+
+Displays the list of components in a software draft.
+
+Examples:
+  govc cluster.draft.component.ls -cluster-id=domain-c21 -draft-id=13
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -draft-id=             The identifier of the software draft.
+```
+
+## cluster.draft.component.rm
+
+```
+Usage: govc cluster.draft.component.rm [OPTIONS] CLUSTER
+
+Removes a component from a software draft.  
+
+Examples:
+  govc cluster.draft.component.rm -cluster-id=domain-c21 -draft-id=13 -component-id=NVD-AIE-800
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -component-id=         The identifier of the software component.
+  -draft-id=             The identifier of the software draft.
+```
+
+## cluster.draft.create
+
+```
+Usage: govc cluster.draft.create [OPTIONS] CLUSTER
+
+Creates a new software draft.
+
+There can be only one active draft at a time on every cluster.
+
+Examples:
+  govc cluster.draft.create -cluster-id=domain-c21
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+```
+
+## cluster.draft.info
+
+```
+Usage: govc cluster.draft.info [OPTIONS] CLUSTER
+
+Displays the details of a software draft.
+
+Examples:
+  govc cluster.draft.info -cluster-id=domain-c21 -draft-id=13
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -draft-id=             The identifier of the software draft.
+```
+
+## cluster.draft.ls
+
+```
+Usage: govc cluster.draft.ls [OPTIONS] CLUSTER
+
+Displays the list of software drafts.
+
+Examples:
+  govc cluster.draft.ls -cluster-id=domain-c21
+  govc cluster.draft.ls -cluster-id=domain-c21 -owners=stoyan1,stoyan2
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -owners=               A comma-separated list of owners to filter by.
+```
+
+## cluster.draft.rm
+
+```
+Usage: govc cluster.draft.rm [OPTIONS] CLUSTER
+
+Discards a software draft.
+
+Examples:
+  govc cluster.draft.rm -cluster-id=domain-c21 -draft-id=13
+
+Options:
+  -cluster-id=           The identifier of the cluster.
+  -draft-id=             The identifier of the software draft.
+```
+
 ## cluster.group.change
 
 ```
@@ -5659,6 +5815,72 @@ Options:
   -require=  Require govc version >= this value
 ```
 
+## vlcm.depot.offline.create
+
+```
+Usage: govc vlcm.depot.offline.create [OPTIONS] VLCM
+
+Creates an offline image depot.
+
+Execution will block the terminal for the duration of the task. 
+
+Examples:
+  govc vlcm.depot.offline.create -l=<https://your.server.com/filepath>
+  govc vlcm.depot.offline.create -l=<https://your.server.com/filepath> -source-type=PULL
+  govc vlcm.depot.offline.create -file-id=<your file identifier> -source-type=PUSH
+  govc vlcm.depot.offline.create -l=<https://your.server.com/filepath> -source-type=PULL -d="This is a depot used for testing" -owner-data="After all, why not? Why shouldn't I keep it?"
+
+Options:
+  -d=                    An optional description
+  -file-id=              File identifier. Only used when source-type is PUSH
+  -l=                    The URL to the depot contents. Only applicable when source-type is PULL
+  -owner-data=           Optional data about the depot's owner
+  -source-type=PULL      The depot source type. The default is PULL
+```
+
+## vlcm.depot.offline.info
+
+```
+Usage: govc vlcm.depot.offline.info [OPTIONS] VLCM
+
+Displays the contents of an offline image depot. 
+
+Examples:
+  govc vlcm.depot.offline.info -depot-id=<your depot's identifier>
+
+Options:
+  -depot-id=             The identifier of the depot. Use the 'ls' command to see the list of depots.
+```
+
+## vlcm.depot.offline.ls
+
+```
+Usage: govc vlcm.depot.offline.ls [OPTIONS] VLCM
+
+Displays the list of offline image depots. 
+
+Examples:
+  govc vlcm.depot.offline.ls
+
+Options:
+```
+
+## vlcm.depot.offline.rm
+
+```
+Usage: govc vlcm.depot.offline.rm [OPTIONS] VLCM
+
+Deletes an offline image depot.
+
+Execution will block the terminal for the duration of the task. 
+
+Examples:
+  govc vlcm.depot.offline.rm -depot-id=<your depot's identifier>
+
+Options:
+  -depot-id=             The identifier of the depot. Use the 'ls' command to see the list of depots.
+```
+
 ## vm.change
 
 ```
@@ -5818,7 +6040,7 @@ Options:
   -net.protocol=         Network device protocol. Applicable to vmxnet3vrdma. Default to 'rocev2'
   -on=true               Power on VM
   -pool=                 Resource pool [GOVC_RESOURCE_POOL]
-  -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]
+  -version=              ESXi hardware version [5.0|5.5|6.0|6.5|6.7|7.0]
 ```
 
 ## vm.customize
diff --git a/govc/cluster/draft/commit.go b/govc/cluster/draft/commit.go
new file mode 100644
index 00000000..28b89169
--- /dev/null
+++ b/govc/cluster/draft/commit.go
@@ -0,0 +1,81 @@
+/*
+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 draft
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/cis/tasks"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type commit struct {
+	*flags.ClientFlag
+
+	clusterId string
+	draftId   string
+}
+
+func init() {
+	cli.Register("cluster.draft.commit", &commit{})
+}
+
+func (cmd *commit) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.draftId, "draft-id", "", "The identifier of the software draft.")
+}
+
+func (cmd *commit) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *commit) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *commit) Description() string {
+	return `Commits the provided software draft.  
+
+Execution will block the terminal for the duration of the task. 
+
+Examples:
+  govc cluster.draft.commit -cluster-id=domain-c21 -draft-id=13`
+}
+
+func (cmd *commit) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	if taskId, err := dm.CommitSoftwareDraft(cmd.clusterId, cmd.draftId, clusters.SettingsClustersSoftwareDraftsCommitSpec{}); err != nil {
+		return err
+	} else if _, err = tasks.NewManager(rc).WaitForCompletion(ctx, taskId); err != nil {
+		return err
+	} else {
+		return nil
+	}
+}
diff --git a/govc/cluster/draft/component/add.go b/govc/cluster/draft/component/add.go
new file mode 100644
index 00000000..e09440f7
--- /dev/null
+++ b/govc/cluster/draft/component/add.go
@@ -0,0 +1,81 @@
+/*
+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 component
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type add struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	clusterId        string
+	draftId          string
+	componentId      string
+	componentVersion string
+}
+
+func init() {
+	cli.Register("cluster.draft.component.add", &add{})
+}
+
+func (cmd *add) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.draftId, "draft-id", "", "The identifier of the software draft.")
+	f.StringVar(&cmd.componentId, "component-id", "", "The identifier of the software component.")
+	f.StringVar(&cmd.componentVersion, "component-version", "", "The version of the software component.")
+}
+
+func (cmd *add) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *add) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *add) Description() string {
+	return `Adds a new component to the software draft.  
+
+Examples:
+  govc cluster.draft.component.add -cluster-id=domain-c21 -draft-id=13 -component-id=NVD-AIE-800 -component-version=550.54.10-1OEM.800.1.0.20613240`
+}
+
+func (cmd *add) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	spec := clusters.SoftwareComponentsUpdateSpec{}
+	spec.ComponentsToSet = make(map[string]string)
+	spec.ComponentsToSet[cmd.componentId] = cmd.componentVersion
+	return dm.UpdateSoftwareDraftComponents(cmd.clusterId, cmd.draftId, spec)
+}
diff --git a/govc/cluster/draft/component/info.go b/govc/cluster/draft/component/info.go
new file mode 100644
index 00000000..5a732df4
--- /dev/null
+++ b/govc/cluster/draft/component/info.go
@@ -0,0 +1,97 @@
+/*
+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 component
+
+import (
+	"context"
+	"flag"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type infoResult clusters.SettingsComponentInfo
+
+func (r infoResult) Write(w io.Writer) error {
+	return nil
+}
+
+type info struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	clusterId   string
+	draftId     string
+	componentId string
+}
+
+func init() {
+	cli.Register("cluster.draft.component.info", &info{})
+}
+
+func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.draftId, "draft-id", "", "The identifier of the software draft.")
+	f.StringVar(&cmd.componentId, "component-id", "", "The identifier of the software component.")
+}
+
+func (cmd *info) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (cmd *info) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *info) Description() string {
+	return `Displays the details of a component in a software draft.  
+
+Examples:
+  govc cluster.draft.component.info -cluster-id=domain-c21 -draft-id=13 -component-id=NVD-AIE-800`
+}
+
+func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	if d, err := dm.GetSoftwareDraftComponent(cmd.clusterId, cmd.draftId, cmd.componentId); err != nil {
+		return err
+	} else {
+		if !cmd.All() {
+			cmd.JSON = true
+		}
+		return cmd.WriteResult(infoResult(d))
+	}
+}
diff --git a/govc/cluster/draft/component/ls.go b/govc/cluster/draft/component/ls.go
new file mode 100644
index 00000000..b5de1cdb
--- /dev/null
+++ b/govc/cluster/draft/component/ls.go
@@ -0,0 +1,97 @@
+/*
+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 component
+
+import (
+	"context"
+	"flag"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type lsResult map[string]clusters.SettingsComponentInfo
+
+func (r lsResult) Write(w io.Writer) error {
+	return nil
+}
+
+type ls struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	clusterId string
+	draftId   string
+}
+
+func init() {
+	cli.Register("cluster.draft.component.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)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.draftId, "draft-id", "", "The identifier of the software draft.")
+
+}
+
+func (cmd *ls) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (cmd *ls) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *ls) Description() string {
+	return `Displays the list of components in a software draft.
+
+Examples:
+  govc cluster.draft.component.ls -cluster-id=domain-c21 -draft-id=13`
+}
+
+func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	if d, err := dm.ListSoftwareDraftComponents(cmd.clusterId, cmd.draftId); err != nil {
+		return err
+	} else {
+		if !cmd.All() {
+			cmd.JSON = true
+		}
+		return cmd.WriteResult(lsResult(d))
+	}
+}
diff --git a/govc/cluster/draft/component/rm.go b/govc/cluster/draft/component/rm.go
new file mode 100644
index 00000000..42003fb2
--- /dev/null
+++ b/govc/cluster/draft/component/rm.go
@@ -0,0 +1,76 @@
+/*
+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 component
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type rm struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	clusterId   string
+	draftId     string
+	componentId string
+}
+
+func init() {
+	cli.Register("cluster.draft.component.rm", &rm{})
+}
+
+func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.draftId, "draft-id", "", "The identifier of the software draft.")
+	f.StringVar(&cmd.componentId, "component-id", "", "The identifier of the software component.")
+}
+
+func (cmd *rm) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *rm) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *rm) Description() string {
+	return `Removes a component from a software draft.  
+
+Examples:
+  govc cluster.draft.component.rm -cluster-id=domain-c21 -draft-id=13 -component-id=NVD-AIE-800`
+}
+
+func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	return dm.RemoveSoftwareDraftComponents(cmd.clusterId, cmd.draftId, cmd.componentId)
+}
diff --git a/govc/cluster/draft/create.go b/govc/cluster/draft/create.go
new file mode 100644
index 00000000..680a0cae
--- /dev/null
+++ b/govc/cluster/draft/create.go
@@ -0,0 +1,91 @@
+/*
+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 draft
+
+import (
+	"context"
+	"flag"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type createResult string
+
+func (r createResult) Write(w io.Writer) error {
+	return nil
+}
+
+type create struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	clusterId string
+}
+
+func init() {
+	cli.Register("cluster.draft.create", &create{})
+}
+
+func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+}
+
+func (cmd *create) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *create) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *create) Description() string {
+	return `Creates a new software draft.
+
+There can be only one active draft at a time on every cluster.
+
+Examples:
+  govc cluster.draft.create -cluster-id=domain-c21`
+}
+
+func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	if !cmd.All() {
+		cmd.JSON = true
+	}
+
+	if draftId, err := dm.CreateSoftwareDraft(cmd.clusterId); err != nil {
+		return err
+	} else if err := cmd.WriteResult(createResult(draftId)); err != nil {
+		return err
+	} else {
+		return nil
+	}
+}
diff --git a/govc/cluster/draft/info.go b/govc/cluster/draft/info.go
new file mode 100644
index 00000000..7e5ce437
--- /dev/null
+++ b/govc/cluster/draft/info.go
@@ -0,0 +1,95 @@
+/*
+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 draft
+
+import (
+	"context"
+	"flag"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type infoResult clusters.SettingsClustersSoftwareDraftsInfo
+
+func (r infoResult) Write(w io.Writer) error {
+	return nil
+}
+
+type info struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	clusterId string
+	draftId   string
+}
+
+func init() {
+	cli.Register("cluster.draft.info", &info{})
+}
+
+func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.draftId, "draft-id", "", "The identifier of the software draft.")
+}
+
+func (cmd *info) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (cmd *info) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *info) Description() string {
+	return `Displays the details of a software draft.
+
+Examples:
+  govc cluster.draft.info -cluster-id=domain-c21 -draft-id=13`
+}
+
+func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	if d, err := dm.GetSoftwareDraft(cmd.clusterId, cmd.draftId); err != nil {
+		return err
+	} else {
+		if !cmd.All() {
+			cmd.JSON = true
+		}
+		return cmd.WriteResult(infoResult(d))
+	}
+}
diff --git a/govc/cluster/draft/ls.go b/govc/cluster/draft/ls.go
new file mode 100644
index 00000000..ed717526
--- /dev/null
+++ b/govc/cluster/draft/ls.go
@@ -0,0 +1,104 @@
+/*
+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 draft
+
+import (
+	"context"
+	"flag"
+	"io"
+	"strings"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type lsResult map[string]clusters.SettingsClustersSoftwareDraftsMetadata
+
+func (r lsResult) Write(w io.Writer) error {
+	return nil
+}
+
+type ls struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	clusterId string
+	owners    string
+}
+
+func init() {
+	cli.Register("cluster.draft.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)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.owners, "owners", "", "A comma-separated list of owners to filter by.")
+}
+
+func (cmd *ls) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (cmd *ls) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *ls) Description() string {
+	return `Displays the list of software drafts.
+
+Examples:
+  govc cluster.draft.ls -cluster-id=domain-c21
+  govc cluster.draft.ls -cluster-id=domain-c21 -owners=stoyan1,stoyan2`
+}
+
+func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	var owners *[]string
+	if cmd.owners != "" {
+		o := strings.Split(cmd.owners, ",")
+		owners = &o
+	}
+
+	if d, err := dm.ListSoftwareDrafts(cmd.clusterId, owners); err != nil {
+		return err
+	} else {
+		if !cmd.All() {
+			cmd.JSON = true
+		}
+		return cmd.WriteResult(lsResult(d))
+	}
+}
diff --git a/govc/cluster/draft/rm.go b/govc/cluster/draft/rm.go
new file mode 100644
index 00000000..1b58f669
--- /dev/null
+++ b/govc/cluster/draft/rm.go
@@ -0,0 +1,72 @@
+/*
+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 draft
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+)
+
+type rm struct {
+	*flags.ClientFlag
+
+	clusterId string
+	draftId   string
+}
+
+func init() {
+	cli.Register("cluster.draft.rm", &rm{})
+}
+
+func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.clusterId, "cluster-id", "", "The identifier of the cluster.")
+	f.StringVar(&cmd.draftId, "draft-id", "", "The identifier of the software draft.")
+}
+
+func (cmd *rm) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *rm) Usage() string {
+	return "CLUSTER"
+}
+
+func (cmd *rm) Description() string {
+	return `Discards a software draft.
+
+Examples:
+  govc cluster.draft.rm -cluster-id=domain-c21 -draft-id=13`
+}
+
+func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := clusters.NewManager(rc)
+
+	return dm.DeleteSoftwareDraft(cmd.clusterId, cmd.draftId)
+}
diff --git a/govc/main.go b/govc/main.go
index 0c328c93..914a045b 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -22,6 +22,8 @@ import (
 	_ "github.com/vmware/govmomi/govc/about"
 	"github.com/vmware/govmomi/govc/cli"
 	_ "github.com/vmware/govmomi/govc/cluster"
+	_ "github.com/vmware/govmomi/govc/cluster/draft"
+	_ "github.com/vmware/govmomi/govc/cluster/draft/component"
 	_ "github.com/vmware/govmomi/govc/cluster/group"
 	_ "github.com/vmware/govmomi/govc/cluster/module"
 	_ "github.com/vmware/govmomi/govc/cluster/override"
@@ -103,6 +105,7 @@ import (
 	_ "github.com/vmware/govmomi/govc/vcsa/proxy"
 	_ "github.com/vmware/govmomi/govc/vcsa/shutdown"
 	_ "github.com/vmware/govmomi/govc/version"
+	_ "github.com/vmware/govmomi/govc/vlcm/depot/offline"
 	_ "github.com/vmware/govmomi/govc/vm"
 	_ "github.com/vmware/govmomi/govc/vm/dataset"
 	_ "github.com/vmware/govmomi/govc/vm/dataset/entry"
diff --git a/govc/vlcm/depot/offline/create.go b/govc/vlcm/depot/offline/create.go
new file mode 100644
index 00000000..160d8671
--- /dev/null
+++ b/govc/vlcm/depot/offline/create.go
@@ -0,0 +1,86 @@
+/*
+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 offline
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/cis/tasks"
+	"github.com/vmware/govmomi/vapi/esx/settings/depots"
+)
+
+type create struct {
+	*flags.ClientFlag
+
+	spec depots.SettingsDepotsOfflineCreateSpec
+}
+
+func init() {
+	cli.Register("vlcm.depot.offline.create", &create{})
+}
+
+func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.spec.Location, "l", "", "The URL to the depot contents. Only applicable when source-type is PULL")
+	f.StringVar(&cmd.spec.Description, "d", "", "An optional description")
+	f.StringVar(&cmd.spec.OwnerData, "owner-data", "", "Optional data about the depot's owner")
+	f.StringVar(&cmd.spec.FileId, "file-id", "", "File identifier. Only used when source-type is PUSH")
+	f.StringVar(&cmd.spec.SourceType, "source-type", string(depots.SourceTypePull), "The depot source type. The default is PULL")
+}
+
+func (cmd *create) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *create) Usage() string {
+	return "VLCM"
+}
+
+func (cmd *create) Description() string {
+	return `Creates an offline image depot.
+
+Execution will block the terminal for the duration of the task. 
+
+Examples:
+  govc vlcm.depot.offline.create -l=<https://your.server.com/filepath>
+  govc vlcm.depot.offline.create -l=<https://your.server.com/filepath> -source-type=PULL
+  govc vlcm.depot.offline.create -file-id=<your file identifier> -source-type=PUSH
+  govc vlcm.depot.offline.create -l=<https://your.server.com/filepath> -source-type=PULL -d="This is a depot used for testing" -owner-data="After all, why not? Why shouldn't I keep it?"`
+}
+
+func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := depots.NewManager(rc)
+
+	if taskId, err := dm.CreateOfflineDepot(cmd.spec); err != nil {
+		return err
+	} else if _, err = tasks.NewManager(rc).WaitForCompletion(ctx, taskId); err != nil {
+		return err
+	} else {
+		return nil
+	}
+}
diff --git a/govc/vlcm/depot/offline/info.go b/govc/vlcm/depot/offline/info.go
new file mode 100644
index 00000000..31b26da6
--- /dev/null
+++ b/govc/vlcm/depot/offline/info.go
@@ -0,0 +1,94 @@
+/*
+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 offline
+
+import (
+	"context"
+	"flag"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/depots"
+)
+
+type infoResult depots.SettingsDepotsOfflineContentInfo
+
+func (r infoResult) Write(w io.Writer) error {
+	return nil
+}
+
+type info struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+
+	depotId string
+}
+
+func init() {
+	cli.Register("vlcm.depot.offline.info", &info{})
+}
+
+func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+
+	f.StringVar(&cmd.depotId, "depot-id", "", "The identifier of the depot. Use the 'ls' command to see the list of depots.")
+}
+
+func (cmd *info) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (cmd *info) Usage() string {
+	return "VLCM"
+}
+
+func (cmd *info) Description() string {
+	return `Displays the contents of an offline image depot. 
+
+Examples:
+  govc vlcm.depot.offline.info -depot-id=<your depot's identifier>`
+}
+
+func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := depots.NewManager(rc)
+
+	if d, err := dm.GetOfflineDepotContent(cmd.depotId); err != nil {
+		return err
+	} else {
+		if !cmd.All() {
+			cmd.JSON = true
+		}
+
+		return cmd.WriteResult(infoResult(d))
+	}
+}
diff --git a/govc/vlcm/depot/offline/ls.go b/govc/vlcm/depot/offline/ls.go
new file mode 100644
index 00000000..53baba73
--- /dev/null
+++ b/govc/vlcm/depot/offline/ls.go
@@ -0,0 +1,90 @@
+/*
+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 offline
+
+import (
+	"context"
+	"flag"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/esx/settings/depots"
+)
+
+type lsResult map[string]depots.SettingsDepotsOfflineInfo
+
+func (r lsResult) Write(w io.Writer) error {
+	return nil
+}
+
+type ls struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+}
+
+func init() {
+	cli.Register("vlcm.depot.offline.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)
+}
+
+func (cmd *ls) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (cmd *ls) Usage() string {
+	return "VLCM"
+}
+
+func (cmd *ls) Description() string {
+	return `Displays the list of offline image depots. 
+
+Examples:
+  govc vlcm.depot.offline.ls`
+}
+
+func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := depots.NewManager(rc)
+
+	if d, err := dm.GetOfflineDepots(); err != nil {
+		return err
+	} else {
+		if !cmd.All() {
+			cmd.JSON = true
+		}
+		return cmd.WriteResult(lsResult(d))
+	}
+}
diff --git a/govc/vlcm/depot/offline/rm.go b/govc/vlcm/depot/offline/rm.go
new file mode 100644
index 00000000..d1f40345
--- /dev/null
+++ b/govc/vlcm/depot/offline/rm.go
@@ -0,0 +1,79 @@
+/*
+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 offline
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/cis/tasks"
+	"github.com/vmware/govmomi/vapi/esx/settings/depots"
+)
+
+type rm struct {
+	*flags.ClientFlag
+
+	depotId string
+}
+
+func init() {
+	cli.Register("vlcm.depot.offline.rm", &rm{})
+}
+
+func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.depotId, "depot-id", "", "The identifier of the depot. Use the 'ls' command to see the list of depots.")
+}
+
+func (cmd *rm) Process(ctx context.Context) error {
+	return cmd.ClientFlag.Process(ctx)
+}
+
+func (cmd *rm) Usage() string {
+	return "VLCM"
+}
+
+func (cmd *rm) Description() string {
+	return `Deletes an offline image depot.
+
+Execution will block the terminal for the duration of the task. 
+
+Examples:
+  govc vlcm.depot.offline.rm -depot-id=<your depot's identifier>`
+}
+
+func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
+	rc, err := cmd.RestClient()
+
+	if err != nil {
+		return err
+	}
+
+	dm := depots.NewManager(rc)
+
+	if taskId, err := dm.DeleteOfflineDepot(cmd.depotId); err != nil {
+		return err
+	} else if _, err = tasks.NewManager(rc).WaitForCompletion(ctx, taskId); err != nil {
+		return err
+	} else {
+		return nil
+	}
+}
diff --git a/govc/vm/create.go b/govc/vm/create.go
index bfae8d67..d6c0fc52 100644
--- a/govc/vm/create.go
+++ b/govc/vm/create.go
@@ -32,6 +32,17 @@ import (
 	"github.com/vmware/govmomi/vim25/types"
 )
 
+var hardwareVersions = []struct {
+	esx, vmx string
+}{
+	{"5.0", "vmx-8"},
+	{"5.5", "vmx-10"},
+	{"6.0", "vmx-11"},
+	{"6.5", "vmx-13"},
+	{"6.7", "vmx-14"},
+	{"7.0", "vmx-17"},
+}
+
 var (
 	FirmwareTypes = []string{
 		string(types.GuestOsDescriptorFirmwareTypeBios),
@@ -127,14 +138,12 @@ func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
 	f.StringVar(&cmd.annotation, "annotation", "", "VM description")
 
 	f.StringVar(&cmd.firmware, "firmware", FirmwareTypes[0], FirmwareUsage)
-
-	esxiVersions := types.GetESXiVersions()
-	esxiVersionStrings := make([]string, len(esxiVersions))
-	for i := range esxiVersions {
-		esxiVersionStrings[i] = esxiVersions[i].String()
+	var versions []string
+	for i := range hardwareVersions {
+		versions = append(versions, hardwareVersions[i].esx)
 	}
 	f.StringVar(&cmd.version, "version", "",
-		fmt.Sprintf("ESXi hardware version [%s]", strings.Join(esxiVersionStrings, "|")))
+		fmt.Sprintf("ESXi hardware version [%s]", strings.Join(versions, "|")))
 
 	f.StringVar(&cmd.iso, "iso", "", "ISO path")
 	cmd.isoDatastoreFlag, ctx = flags.NewCustomDatastoreFlag(ctx)
@@ -333,12 +342,11 @@ func (cmd *create) createVM(ctx context.Context) (*object.Task, error) {
 	var err error
 
 	if cmd.version != "" {
-		if v, _ := types.ParseESXiVersion(cmd.version); v.IsValid() {
-			cmd.version = v.HardwareVersion().String()
-		} else if v, _ := types.ParseHardwareVersion(cmd.version); v.IsValid() {
-			cmd.version = v.String()
-		} else {
-			return nil, fmt.Errorf("invalid version: %s", cmd.version)
+		for i := range hardwareVersions {
+			if hardwareVersions[i].esx == cmd.version {
+				cmd.version = hardwareVersions[i].vmx
+				break
+			}
 		}
 	}
 
diff --git a/simulator/cluster_compute_resource.go b/simulator/cluster_compute_resource.go
index fdb273b1..cf992853 100644
--- a/simulator/cluster_compute_resource.go
+++ b/simulator/cluster_compute_resource.go
@@ -72,11 +72,6 @@ func (add *addHost) Run(task *Task) (types.AnyType, types.BaseMethodFault) {
 	host.Summary.Host = &host.Self
 	host.Config.Host = host.Self
 
-	task.ctx.Map.WithLock(task.ctx, *cr.EnvironmentBrowser, func() {
-		eb := task.ctx.Map.Get(*cr.EnvironmentBrowser).(*EnvironmentBrowser)
-		eb.addHost(task.ctx, host.Self)
-	})
-
 	cr.Host = append(cr.Host, host.Reference())
 	addComputeResource(cr.Summary.GetComputeResourceSummary(), host)
 
@@ -474,7 +469,7 @@ func CreateClusterComputeResource(ctx *Context, f *Folder, name string, spec typ
 	}
 
 	cluster := &ClusterComputeResource{}
-	cluster.EnvironmentBrowser = newEnvironmentBrowser(ctx)
+	cluster.EnvironmentBrowser = newEnvironmentBrowser()
 	cluster.Name = name
 	cluster.Network = ctx.Map.getEntityDatacenter(f).defaultNetwork()
 	cluster.Summary = &types.ClusterComputeResourceSummary{
diff --git a/simulator/environment_browser.go b/simulator/environment_browser.go
index 008fedbb..31b50939 100644
--- a/simulator/environment_browser.go
+++ b/simulator/environment_browser.go
@@ -35,98 +35,12 @@ type EnvironmentBrowser struct {
 	QueryTargetCapabilitiesResponse     types.QueryTargetCapabilitiesResponse
 }
 
-func newEnvironmentBrowser(
-	ctx *Context,
-	hostRefs ...types.ManagedObjectReference) *types.ManagedObjectReference {
-
+func newEnvironmentBrowser() *types.ManagedObjectReference {
 	env := new(EnvironmentBrowser)
-	env.initDescriptorReturnVal(ctx, hostRefs...)
 	Map.Put(env)
 	return &env.Self
 }
 
-func (b *EnvironmentBrowser) addHost(
-	ctx *Context, hostRef types.ManagedObjectReference) {
-
-	// Get a set of unique hosts.
-	hostSet := map[types.ManagedObjectReference]struct{}{
-		hostRef: {},
-	}
-	for i := range b.QueryConfigOptionDescriptorResponse.Returnval {
-		cod := b.QueryConfigOptionDescriptorResponse.Returnval[i]
-		for j := range cod.Host {
-			if _, ok := hostSet[cod.Host[j]]; !ok {
-				hostSet[cod.Host[j]] = struct{}{}
-			}
-		}
-	}
-
-	// Get a list of unique hosts.
-	var hostRefs []types.ManagedObjectReference
-	for ref := range hostSet {
-		hostRefs = append(hostRefs, ref)
-	}
-
-	// Clear the descriptor's return val.
-	b.QueryConfigOptionDescriptorResponse.Returnval = nil
-
-	b.initDescriptorReturnVal(ctx, hostRefs...)
-}
-
-func (b *EnvironmentBrowser) initDescriptorReturnVal(
-	ctx *Context, hostRefs ...types.ManagedObjectReference) {
-
-	// Get the max supported hardware version for this list of hosts.
-	var maxHardwareVersion types.HardwareVersion
-	maxHardwareVersionForHost := map[types.ManagedObjectReference]types.HardwareVersion{}
-	for j := range hostRefs {
-		ref := hostRefs[j]
-		ctx.WithLock(ref, func() {
-			host := ctx.Map.Get(ref).(*HostSystem)
-			hostVersion := types.MustParseESXiVersion(host.Config.Product.Version)
-			hostHardwareVersion := hostVersion.HardwareVersion()
-			maxHardwareVersionForHost[ref] = hostHardwareVersion
-			if !maxHardwareVersion.IsValid() {
-				maxHardwareVersion = hostHardwareVersion
-				return
-			}
-			if hostHardwareVersion > maxHardwareVersion {
-				maxHardwareVersion = hostHardwareVersion
-			}
-		})
-	}
-
-	if !maxHardwareVersion.IsValid() {
-		return
-	}
-
-	hardwareVersions := types.GetHardwareVersions()
-	for i := range hardwareVersions {
-		hv := hardwareVersions[i]
-		dco := hv == maxHardwareVersion
-		cod := types.VirtualMachineConfigOptionDescriptor{
-			Key:                 hv.String(),
-			Description:         hv.String(),
-			DefaultConfigOption: types.NewBool(dco),
-			CreateSupported:     types.NewBool(true),
-			RunSupported:        types.NewBool(true),
-			UpgradeSupported:    types.NewBool(true),
-		}
-		for hostRef, hostVer := range maxHardwareVersionForHost {
-			if hostVer >= hv {
-				cod.Host = append(cod.Host, hostRef)
-			}
-		}
-
-		b.QueryConfigOptionDescriptorResponse.Returnval = append(
-			b.QueryConfigOptionDescriptorResponse.Returnval, cod)
-
-		if dco {
-			break
-		}
-	}
-}
-
 func (b *EnvironmentBrowser) hosts(ctx *Context) []types.ManagedObjectReference {
 	ctx.Map.m.Lock()
 	defer ctx.Map.m.Unlock()
@@ -229,6 +143,20 @@ func (b *EnvironmentBrowser) QueryConfigOptionDescriptor(ctx *Context, req *type
 		},
 	}
 
+	if body.Res.Returnval != nil {
+		return body
+	}
+
+	body.Res.Returnval = []types.VirtualMachineConfigOptionDescriptor{{
+		Key:                 esx.HardwareVersion,
+		Description:         esx.HardwareVersion,
+		Host:                b.hosts(ctx),
+		CreateSupported:     types.NewBool(true),
+		DefaultConfigOption: types.NewBool(false),
+		RunSupported:        types.NewBool(true),
+		UpgradeSupported:    types.NewBool(true),
+	}}
+
 	return body
 }
 
diff --git a/simulator/esx/host_config_info.go b/simulator/esx/host_config_info.go
index b98f4cf3..d56c3b60 100644
--- a/simulator/esx/host_config_info.go
+++ b/simulator/esx/host_config_info.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2017-2024 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2023 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.
@@ -25,19 +25,19 @@ var HostConfigInfo = types.HostConfigInfo{
 	Host: types.ManagedObjectReference{Type: "HostSystem", Value: "ha-host"},
 	Product: types.AboutInfo{
 		Name:                  "VMware ESXi",
-		FullName:              "VMware ESXi 8.0.2 build-21997540",
+		FullName:              "VMware ESXi 6.5.0 build-5969303",
 		Vendor:                "VMware, Inc.",
-		Version:               "8.0.2",
-		Build:                 "21997540",
+		Version:               "6.5.0",
+		Build:                 "5969303",
 		LocaleVersion:         "INTL",
 		LocaleBuild:           "000",
 		OsType:                "vmnix-x86",
 		ProductLineId:         "embeddedEsx",
 		ApiType:               "HostAgent",
-		ApiVersion:            "8.0.2.0",
+		ApiVersion:            "6.5",
 		InstanceUuid:          "",
 		LicenseProductName:    "VMware ESX Server",
-		LicenseProductVersion: "8.0.2",
+		LicenseProductVersion: "6.0",
 	},
 	DeploymentInfo: &types.HostDeploymentInfo{
 		BootedFromStatelessCache: types.NewBool(false),
diff --git a/simulator/host_system.go b/simulator/host_system.go
index 6dcab0d9..fd88cb49 100644
--- a/simulator/host_system.go
+++ b/simulator/host_system.go
@@ -435,12 +435,12 @@ func CreateDefaultESX(ctx *Context, f *Folder) {
 		Summary: summary,
 		Network: esx.Datacenter.Network,
 	}
+	cr.EnvironmentBrowser = newEnvironmentBrowser()
 	cr.Self = *host.Parent
 	cr.Name = host.Name
 	cr.Host = append(cr.Host, host.Reference())
 	host.Network = cr.Network
 	ctx.Map.PutEntity(cr, host)
-	cr.EnvironmentBrowser = newEnvironmentBrowser(ctx, host.Reference())
 
 	pool := NewResourcePool()
 	cr.ResourcePool = &pool.Self
@@ -480,12 +480,11 @@ func CreateStandaloneHost(ctx *Context, f *Folder, spec types.HostConnectSpec) (
 		ConfigurationEx: &types.ComputeResourceConfigInfo{
 			VmSwapPlacement: string(types.VirtualMachineConfigInfoSwapPlacementTypeVmDirectory),
 		},
-		Summary: summary,
+		Summary:            summary,
+		EnvironmentBrowser: newEnvironmentBrowser(),
 	}
 
 	ctx.Map.PutEntity(cr, ctx.Map.NewEntity(host))
-	cr.EnvironmentBrowser = newEnvironmentBrowser(ctx, host.Reference())
-
 	host.Summary.Host = &host.Self
 	host.Config.Host = host.Self
 
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index 6070b24b..5eef0a97 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -1884,138 +1884,19 @@ func (vm *VirtualMachine) UpgradeVMTask(ctx *Context, req *types.UpgradeVM_Task)
 	body := &methods.UpgradeVM_TaskBody{}
 
 	task := CreateTask(vm, "upgradeVm", func(t *Task) (types.AnyType, types.BaseMethodFault) {
-
-		newInvalidStateFault := func(format string, args ...any) *types.InvalidState {
-			msg := fmt.Sprintf(format, args...)
-			return &types.InvalidState{
-				VimFault: types.VimFault{
-					MethodFault: types.MethodFault{
-						FaultCause: &types.LocalizedMethodFault{
-							Fault: &types.SystemErrorFault{
-								Reason: msg,
-							},
-							LocalizedMessage: msg,
-						},
-					},
-				},
-			}
-		}
-
-		// InvalidPowerState
-		//
-		// 1. Is VM's power state anything other than powered off?
-		if vm.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOff {
-			return nil, &types.InvalidPowerStateFault{
-				ExistingState:  vm.Runtime.PowerState,
-				RequestedState: types.VirtualMachinePowerStatePoweredOff,
-			}
-		}
-
-		// InvalidState
-		//
-		// 1. Is host on which VM is scheduled in maintenance mode?
-		// 2. Is VM a template?
-		// 3. Is VM already the latest hardware version?
-		var (
-			ebRef                     *types.ManagedObjectReference
-			latestHardwareVersion     string
-			hostRef                   = vm.Runtime.Host
-			supportedHardwareVersions = map[string]struct{}{}
-			vmHardwareVersionString   = vm.Config.Version
-		)
-		if hostRef != nil {
-			var hostInMaintenanceMode bool
-			ctx.WithLock(*hostRef, func() {
-				host := ctx.Map.Get(*hostRef).(*HostSystem)
-				hostInMaintenanceMode = host.Runtime.InMaintenanceMode
-				switch host.Parent.Type {
-				case "ClusterComputeResource":
-					obj := ctx.Map.Get(*host.Parent).(*ClusterComputeResource)
-					ebRef = obj.EnvironmentBrowser
-				case "ComputeResource":
-					obj := ctx.Map.Get(*host.Parent).(*mo.ComputeResource)
-					ebRef = obj.EnvironmentBrowser
-				}
-			})
-			if hostInMaintenanceMode {
-				return nil, newInvalidStateFault("%s in maintenance mode", hostRef.Value)
-			}
-		}
-		if vm.Config.Template {
-			return nil, newInvalidStateFault("%s is template", vm.Reference().Value)
-		}
-		if ebRef != nil {
-			ctx.WithLock(*ebRef, func() {
-				eb := ctx.Map.Get(*ebRef).(*EnvironmentBrowser)
-				for i := range eb.QueryConfigOptionDescriptorResponse.Returnval {
-					cod := eb.QueryConfigOptionDescriptorResponse.Returnval[i]
-					for j := range cod.Host {
-						if cod.Host[j].Value == hostRef.Value {
-							supportedHardwareVersions[cod.Key] = struct{}{}
-						}
-						if latestHardwareVersion == "" {
-							if def := cod.DefaultConfigOption; def != nil && *def {
-								latestHardwareVersion = cod.Key
-							}
-						}
-					}
-				}
-			})
-		}
-
-		if latestHardwareVersion == "" {
-			latestHardwareVersion = esx.HardwareVersion
-		}
-		if vmHardwareVersionString == latestHardwareVersion {
-			return nil, newInvalidStateFault("%s is latest version", vm.Reference().Value)
-		}
 		if req.Version == "" {
-			req.Version = latestHardwareVersion
-		}
-
-		// NotSupported
-		targetVersion, _ := types.ParseHardwareVersion(req.Version)
-		if targetVersion.IsValid() {
-			req.Version = targetVersion.String()
+			req.Version = esx.HardwareVersion
 		}
-		if _, ok := supportedHardwareVersions[req.Version]; !ok {
-			msg := fmt.Sprintf("%s not supported", req.Version)
-			return nil, &types.NotSupported{
-				RuntimeFault: types.RuntimeFault{
-					MethodFault: types.MethodFault{
-						FaultCause: &types.LocalizedMethodFault{
-							Fault: &types.SystemErrorFault{
-								Reason: msg,
-							},
-							LocalizedMessage: msg,
-						},
-					},
+		if req.Version != vm.Config.Version {
+			ctx.Map.Update(vm, []types.PropertyChange{
+				{
+					Name: "config.version", Val: req.Version,
 				},
-			}
-		}
-
-		// AlreadyUpgraded
-		vmHardwareVersion, _ := types.ParseHardwareVersion(vmHardwareVersionString)
-		if targetVersion.IsValid() && vmHardwareVersion.IsValid() &&
-			targetVersion <= vmHardwareVersion {
-
-			return nil, &types.AlreadyUpgradedFault{}
-		}
-
-		// InvalidArgument
-		if targetVersion < types.VMX3 {
-			return nil, &types.InvalidArgument{}
+				{
+					Name: "summary.config.hwVersion", Val: req.Version,
+				},
+			})
 		}
-
-		ctx.Map.Update(vm, []types.PropertyChange{
-			{
-				Name: "config.version", Val: targetVersion.String(),
-			},
-			{
-				Name: "summary.config.hwVersion", Val: targetVersion.String(),
-			},
-		})
-
 		return nil, nil
 	})
 
diff --git a/vapi/cis/tasks/simulator/simulator.go b/vapi/cis/tasks/simulator/simulator.go
new file mode 100644
index 00000000..6d3ebcff
--- /dev/null
+++ b/vapi/cis/tasks/simulator/simulator.go
@@ -0,0 +1,58 @@
+/*
+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 simulator
+
+import (
+	"net/http"
+	"net/url"
+
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/cis/tasks"
+	vapi "github.com/vmware/govmomi/vapi/simulator"
+)
+
+func init() {
+	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
+		New(s.Listen).Register(s, r)
+	})
+}
+
+type Handler struct {
+	URL *url.URL
+}
+
+// New creates a Handler instance
+func New(u *url.URL) *Handler {
+	return &Handler{
+		URL: u,
+	}
+}
+
+func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) {
+	if r.IsVPX() {
+		s.HandleFunc(tasks.TasksPath+"/", h.depotsOffline)
+	}
+}
+
+func (h *Handler) depotsOffline(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodGet:
+		task := make(map[string]string)
+		task["status"] = "SUCCEEDED"
+		vapi.StatusOK(w, task)
+	}
+}
diff --git a/vapi/cis/tasks/tasks.go b/vapi/cis/tasks/tasks.go
new file mode 100644
index 00000000..5e184c99
--- /dev/null
+++ b/vapi/cis/tasks/tasks.go
@@ -0,0 +1,81 @@
+/*
+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 tasks
+
+import (
+	"context"
+	"net/http"
+	"time"
+
+	"github.com/vmware/govmomi/vapi/rest"
+)
+
+const (
+	// TasksPath The endpoint for retrieving tasks
+	TasksPath = "/api/cis/tasks"
+	// The default interval in seconds for polling for task results
+	defaultPollingInterval = 10
+)
+
+// Manager extends rest.Client, adding task related methods.
+type Manager struct {
+	*rest.Client
+	pollingInterval int
+}
+
+// NewManager creates a new Manager instance with the given client.
+func NewManager(client *rest.Client) *Manager {
+	return &Manager{
+		Client:          client,
+		pollingInterval: defaultPollingInterval,
+	}
+}
+
+// NewManager creates a new Manager instance with the given client and custom polling interval
+func NewManagerWithCustomInterval(client *rest.Client, pollingInterval int) *Manager {
+	return &Manager{
+		Client:          client,
+		pollingInterval: pollingInterval,
+	}
+}
+
+func (c *Manager) WaitForCompletion(ctx context.Context, taskId string) (string, error) {
+	ticker := time.NewTicker(time.Second * time.Duration(c.pollingInterval))
+
+	for {
+		select {
+		case <-ctx.Done():
+		case <-ticker.C:
+			taskInfo, err := c.getTaskInfo(taskId)
+			status := taskInfo["status"].(string)
+			if err != nil {
+				return status, err
+			}
+
+			if status != "RUNNING" {
+				return status, nil
+			}
+		}
+	}
+}
+
+func (c *Manager) getTaskInfo(taskId string) (map[string]interface{}, error) {
+	path := c.Resource(TasksPath).WithSubpath(taskId)
+	req := path.Request(http.MethodGet)
+	var res map[string]interface{}
+	return res, c.Do(context.Background(), req, &res)
+}
diff --git a/vapi/esx/settings/clusters/clusters.go b/vapi/esx/settings/clusters/clusters.go
new file mode 100644
index 00000000..344304d0
--- /dev/null
+++ b/vapi/esx/settings/clusters/clusters.go
@@ -0,0 +1,253 @@
+/*
+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 clusters
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/vmware/govmomi/vapi/rest"
+)
+
+const (
+	// SoftwareDraftsPath The endpoint for the software drafts API
+	SoftwareDraftsPath     = "/api/esx/settings/clusters/%s/software/drafts"
+	SoftwareComponentsPath = SoftwareDraftsPath + "/%s/software/components"
+)
+
+// Manager extends rest.Client, adding Software Drafts 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,
+	}
+}
+
+// SettingsClustersSoftwareDraftsMetadata is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Clusters/Software/Drafts/Metadata/
+type SettingsClustersSoftwareDraftsMetadata struct {
+	CreationTime string `json:"creation_time"`
+	Owner        string `json:"owner"`
+	Status       string `json:"status"`
+}
+
+// SettingsBaseImageDetails is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/BaseImageDetails/
+type SettingsBaseImageDetails struct {
+	DisplayName    string `json:"display_name"`
+	DisplayVersion string `json:"display_version"`
+	ReleaseDate    string `json:"release_date"`
+}
+
+// SettingsBaseImageInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/BaseImageInfo/
+type SettingsBaseImageInfo struct {
+	Version string                   `json:"version"`
+	Details SettingsBaseImageDetails `json:"details"`
+}
+
+// SettingsComponentDetails is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/ComponentDetails/
+type SettingsComponentDetails struct {
+	DisplayName string `json:"display_name"`
+	Vendor      string `json:"vendor"`
+}
+
+// SettingsComponentInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/ComponentInfo/
+type SettingsComponentInfo struct {
+	Version string                   `json:"version"`
+	Details SettingsComponentDetails `json:"details"`
+}
+
+// SettingsSolutionComponentSpec is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/SolutionComponentSpec/
+type SettingsSolutionComponentSpec struct {
+	Component string `json:"component"`
+}
+
+// SettingsSolutionComponentDetails is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/SolutionComponentDetails/
+type SettingsSolutionComponentDetails struct {
+	Component      string `json:"component"`
+	DisplayName    string `json:"display_name"`
+	Vendor         string `json:"vendor"`
+	DisplayVersion string `json:"display_version,omitempty"`
+}
+
+// SettingsSolutionDetails is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/SolutionDetails/
+type SettingsSolutionDetails struct {
+	DisplayName    string `json:"display_name"`
+	DisplayVersion string `json:"display_version"`
+}
+
+// SettingsSolutionInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/SolutionInfo/
+type SettingsSolutionInfo struct {
+	Version    string                          `json:"version"`
+	Components []SettingsSolutionComponentSpec `json:"components"`
+	Details    SettingsSolutionDetails         `json:"details"`
+}
+
+// SettingsAddOnDetails is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/AddOnDetails/
+type SettingsAddOnDetails struct {
+	DisplayName    string `json:"display_name"`
+	DisplayVersion string `json:"display_version"`
+	Vendor         string `json:"vendor"`
+}
+
+// SettingsAddOnInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/AddOnInfo/
+type SettingsAddOnInfo struct {
+	Name    string               `json:"name"`
+	Version string               `json:"version"`
+	Details SettingsAddOnDetails `json:"details,omitempty"`
+}
+
+// SettingsHardwareSupportPackageInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/HardwareSupportPackageInfo/
+type SettingsHardwareSupportPackageInfo struct {
+	Pkg     string `json:"pkg"`
+	Version string `json:"version"`
+}
+
+// SettingsHardwareSupportInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/HardwareSupportInfo/
+type SettingsHardwareSupportInfo struct {
+	Packages map[string]SettingsHardwareSupportPackageInfo `json:"packages"`
+}
+
+// SettingsSoftwareInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/SoftwareInfo/
+type SettingsSoftwareInfo struct {
+	BaseImage       SettingsBaseImageInfo            `json:"base_image"`
+	Components      map[string]SettingsComponentInfo `json:"components"`
+	Solutions       map[string]SettingsSolutionInfo  `json:"solutions"`
+	AddOn           SettingsAddOnInfo                `json:"add_on,omitempty"`
+	HardwareSupport SettingsHardwareSupportInfo      `json:"hardware_support,omitempty"`
+}
+
+// SettingsClustersSoftwareDraftsInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Clusters/Software/Drafts/Info/
+type SettingsClustersSoftwareDraftsInfo struct {
+	Metadata SettingsClustersSoftwareDraftsMetadata `json:"metadata"`
+	Software SettingsSoftwareInfo                   `json:"software"`
+}
+
+// SoftwareComponentsUpdateSpec is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Clusters/Software/Drafts/Software/Components/UpdateSpec/
+type SoftwareComponentsUpdateSpec struct {
+	ComponentsToDelete []string          `json:"components_to_delete,omitempty"`
+	ComponentsToSet    map[string]string `json:"components_to_set,omitempty"`
+}
+
+// SettingsClustersSoftwareDraftsCommitSpec is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Clusters/Software/Drafts/CommitSpec/
+type SettingsClustersSoftwareDraftsCommitSpec struct {
+	Message string `json:"message,omitempty"`
+}
+
+// ListSoftwareDrafts retrieves the software drafts for a cluster
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/get/
+func (c *Manager) ListSoftwareDrafts(clusterId string, owners *[]string) (map[string]SettingsClustersSoftwareDraftsMetadata, error) {
+	path := c.Resource(fmt.Sprintf(SoftwareDraftsPath, clusterId))
+
+	if owners != nil && len(*owners) > 0 {
+		path = path.WithParam("owners", strings.Join(*owners, ","))
+	}
+
+	req := path.Request(http.MethodGet)
+	var res map[string]SettingsClustersSoftwareDraftsMetadata
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// CreateSoftwareDraft creates a software draft on the provided cluster
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/post/
+func (c *Manager) CreateSoftwareDraft(clusterId string) (string, error) {
+	path := c.Resource(fmt.Sprintf(SoftwareDraftsPath, clusterId))
+	req := path.Request(http.MethodPost)
+	var res string
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// DeleteSoftwareDraft removes the specified draft
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/draft/delete/
+func (c *Manager) DeleteSoftwareDraft(clusterId, draftId string) error {
+	path := c.Resource(fmt.Sprintf(SoftwareDraftsPath, clusterId)).WithSubpath(draftId)
+	req := path.Request(http.MethodDelete)
+	return c.Do(context.Background(), req, nil)
+}
+
+// GetSoftwareDraft returns the set of components in the specified draft
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/draft/get/
+func (c *Manager) GetSoftwareDraft(clusterId, draftId string) (SettingsClustersSoftwareDraftsInfo, error) {
+	path := c.Resource(fmt.Sprintf(SoftwareDraftsPath, clusterId)).WithSubpath(draftId)
+	req := path.Request(http.MethodGet)
+	var res SettingsClustersSoftwareDraftsInfo
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// CommitSoftwareDraft closes and applies the specified draft
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/draftactioncommitvmw-tasktrue/post/
+func (c *Manager) CommitSoftwareDraft(clusterId, draftId string, spec SettingsClustersSoftwareDraftsCommitSpec) (string, error) {
+	path := c.Resource(fmt.Sprintf(SoftwareDraftsPath, clusterId)).WithSubpath(draftId).WithParam("action", "commit").WithParam("vmw-task", "true")
+	req := path.Request(http.MethodPost, spec)
+	var res string
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// ListSoftwareDraftComponents returns all components in the specified draft
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/draft/software/components/get/
+func (c *Manager) ListSoftwareDraftComponents(clusterId, draftId string) (map[string]SettingsComponentInfo, error) {
+	path := c.Resource(fmt.Sprintf(SoftwareComponentsPath, clusterId, draftId))
+	req := path.Request(http.MethodGet)
+	var res map[string]SettingsComponentInfo
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// GetSoftwareDraftComponent returns a component from the specified draft
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/draft/software/components/component/get/
+func (c *Manager) GetSoftwareDraftComponent(clusterId, draftId, component string) (SettingsComponentInfo, error) {
+	path := c.Resource(fmt.Sprintf(SoftwareComponentsPath, clusterId, draftId)).WithSubpath(component)
+	req := path.Request(http.MethodGet)
+	var res SettingsComponentInfo
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// UpdateSoftwareDraftComponents updates the set of components in the specified draft
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/draft/software/components/patch/
+func (c *Manager) UpdateSoftwareDraftComponents(clusterId, draftId string, spec SoftwareComponentsUpdateSpec) error {
+	path := c.Resource(fmt.Sprintf(SoftwareComponentsPath, clusterId, draftId))
+	req := path.Request(http.MethodPatch, spec)
+	return c.Do(context.Background(), req, nil)
+}
+
+// RemoveSoftwareDraftComponents removes a component from the specified draft
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/clusters/cluster/software/drafts/draft/software/components/component/delete/
+func (c *Manager) RemoveSoftwareDraftComponents(clusterId, draftId, component string) error {
+	path := c.Resource(fmt.Sprintf(SoftwareComponentsPath, clusterId, draftId)).WithSubpath(component)
+	req := path.Request(http.MethodDelete)
+	return c.Do(context.Background(), req, nil)
+}
diff --git a/vapi/esx/settings/depots/depots.go b/vapi/esx/settings/depots/depots.go
new file mode 100644
index 00000000..5524905d
--- /dev/null
+++ b/vapi/esx/settings/depots/depots.go
@@ -0,0 +1,161 @@
+/*
+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 depots
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/vmware/govmomi/vapi/rest"
+)
+
+const (
+	// DepotsOfflinePath The endpoint for the offline depots API
+	DepotsOfflinePath = "/api/esx/settings/depots/offline"
+	// DepotsOfflineContentPath The endpoint for retrieving the components in a depot
+	DepotsOfflineContentPath = DepotsOfflinePath + "/%s/content"
+)
+
+// Manager extends rest.Client, adding vLCM 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,
+	}
+}
+
+type SourceType string
+
+const (
+	SourceTypePush = SourceType("PUSH")
+	SourceTypePull = SourceType("PULL")
+)
+
+// SettingsDepotsOfflineSummary is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Depots/Offline/Summary/
+type SettingsDepotsOfflineSummary struct {
+	Description string `json:"description"`
+	SourceType  string `json:"source_type"`
+	FileId      string `json:"file_id,omitempty"`
+	Location    string `json:"location,omitempty"`
+	Owner       string `json:"owner,omitempty"`
+	OwnerData   string `json:"owner_data,omitempty"`
+}
+
+// SettingsDepotsOfflineInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Depots/Offline/Info/
+type SettingsDepotsOfflineInfo struct {
+	CreateTime  string `json:"create_time"`
+	Description string `json:"description"`
+	SourceType  string `json:"source_type"`
+	FileId      string `json:"file_id,omitempty"`
+	Location    string `json:"location,omitempty"`
+	Owner       string `json:"owner,omitempty"`
+	OwnerData   string `json:"owner_data,omitempty"`
+}
+
+// SettingsDepotsOfflineCreateSpec is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Depots/Offline/CreateSpec/
+type SettingsDepotsOfflineCreateSpec struct {
+	Description string `json:"description,omitempty"`
+	SourceType  string `json:"source_type"`
+	FileId      string `json:"file_id,omitempty"`
+	Location    string `json:"location,omitempty"`
+	OwnerData   string `json:"owner_data,omitempty"`
+}
+
+// SettingsDepotsComponentSummary is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Depots/ComponentSummary/
+type SettingsDepotsComponentSummary struct {
+	DisplayName string             `json:"display_name"`
+	Versions    []ComponentVersion `json:"versions"`
+}
+
+// ComponentVersion is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Depots/ComponentVersion/
+type ComponentVersion struct {
+	DisplayVersion string `json:"display_version"`
+	Version        string `json:"version"`
+}
+
+// SettingsDepotsMetadataInfo is a partial type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Depots/MetadataInfo/
+type SettingsDepotsMetadataInfo struct {
+	Addons                map[string]interface{}                    `json:"addons,omitempty"`
+	BaseImages            []interface{}                             `json:"base_images,omitempty"`
+	FileName              string                                    `json:"file_name"`
+	HardwareSupport       map[string]interface{}                    `json:"hardware_support,omitempty"`
+	IndependentComponents map[string]SettingsDepotsComponentSummary `json:"independent_components,omitempty"`
+	Solutions             map[string]interface{}                    `json:"solutions,omitempty"`
+	Updates               map[string]interface{}                    `json:"updates,omitempty"`
+}
+
+// SettingsDepotsOfflineContentInfo is a type mapping for
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/data-structures/Settings/Depots/Offline/Content/Info/
+type SettingsDepotsOfflineContentInfo struct {
+	MetadataBundles map[string][]SettingsDepotsMetadataInfo `json:"metadata_bundles"`
+}
+
+// GetOfflineDepot retrieves an offline depot by its identifier
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/depots/offline/depot/get/
+func (c *Manager) GetOfflineDepot(depotId string) (SettingsDepotsOfflineSummary, error) {
+	path := c.Resource(DepotsOfflinePath).WithSubpath(depotId)
+	req := path.Request(http.MethodGet)
+	var res SettingsDepotsOfflineSummary
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// GetOfflineDepots retrieves all offline depots
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/depots/offline/get/
+func (c *Manager) GetOfflineDepots() (map[string]SettingsDepotsOfflineInfo, error) {
+	path := c.Resource(DepotsOfflinePath)
+	req := path.Request(http.MethodGet)
+	var res map[string]SettingsDepotsOfflineInfo
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// DeleteOfflineDepot triggers a task to delete an offline depot by its identifier
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/depots/offline/depotvmw-tasktrue/delete/
+func (c *Manager) DeleteOfflineDepot(depotId string) (string, error) {
+	path := c.Resource(DepotsOfflinePath).WithSubpath(depotId).WithParam("vmw-task", "true")
+	req := path.Request(http.MethodDelete)
+	var res string
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// CreateOfflineDepot triggers a task to create an offline depot
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/depots/offlinevmw-tasktrue/post/
+func (c *Manager) CreateOfflineDepot(spec SettingsDepotsOfflineCreateSpec) (string, error) {
+	path := c.Resource(DepotsOfflinePath).WithParam("vmw-task", "true")
+	req := path.Request(http.MethodPost, spec)
+	var res string
+	return res, c.Do(context.Background(), req, &res)
+}
+
+// GetOfflineDepotContent retrieves the contents of a depot
+// https://developer.vmware.com/apis/vsphere-automation/latest/esx/api/esx/settings/depots/offline/depot/content/get/
+func (c *Manager) GetOfflineDepotContent(depotId string) (SettingsDepotsOfflineContentInfo, error) {
+	path := c.Resource(fmt.Sprintf(DepotsOfflineContentPath, depotId))
+	req := path.Request(http.MethodGet)
+	var res SettingsDepotsOfflineContentInfo
+	return res, c.Do(context.Background(), req, &res)
+}
diff --git a/vapi/esx/settings/simulator/simulator.go b/vapi/esx/settings/simulator/simulator.go
new file mode 100644
index 00000000..348c22a2
--- /dev/null
+++ b/vapi/esx/settings/simulator/simulator.go
@@ -0,0 +1,228 @@
+/*
+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 simulator
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/esx/settings/clusters"
+	"github.com/vmware/govmomi/vapi/esx/settings/depots"
+	vapi "github.com/vmware/govmomi/vapi/simulator"
+)
+
+func init() {
+	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
+		New(s.Listen).Register(s, r)
+	})
+}
+
+type Handler struct {
+	URL *url.URL
+
+	Depots          map[string]depots.SettingsDepotsOfflineInfo
+	DepotComponents map[string]depots.SettingsDepotsOfflineContentInfo
+
+	SoftwareDrafts     map[string]clusters.SettingsClustersSoftwareDraftsMetadata
+	SoftwareComponents map[string]clusters.SettingsComponentInfo
+
+	depotCounter int
+	draftCounter int
+}
+
+// New creates a Handler instance
+func New(u *url.URL) *Handler {
+	return &Handler{
+		URL:                u,
+		Depots:             make(map[string]depots.SettingsDepotsOfflineInfo),
+		DepotComponents:    make(map[string]depots.SettingsDepotsOfflineContentInfo),
+		SoftwareDrafts:     make(map[string]clusters.SettingsClustersSoftwareDraftsMetadata),
+		SoftwareComponents: make(map[string]clusters.SettingsComponentInfo),
+		depotCounter:       0,
+	}
+}
+
+func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) {
+	if r.IsVPX() {
+		s.HandleFunc(depots.DepotsOfflinePath, h.depotsOffline)
+		s.HandleFunc(depots.DepotsOfflinePath+"/", h.depotsOffline)
+		s.HandleFunc("/api/esx/settings/clusters/", h.clusters)
+	}
+}
+
+func (h *Handler) depotsOffline(w http.ResponseWriter, r *http.Request) {
+	subpath := r.URL.Path[len(depots.DepotsOfflinePath):]
+	segments := strings.Split(subpath, "/")
+
+	switch r.Method {
+	case http.MethodGet:
+		if len(segments) > 1 {
+			if res, contains := h.DepotComponents[segments[1]]; !contains {
+				vapi.ApiErrorNotFound(w)
+			} else if len(segments) > 2 && segments[2] == "content" {
+				vapi.StatusOK(w, res)
+			} else {
+				vapi.ApiErrorUnsupported(w)
+			}
+		}
+		vapi.StatusOK(w, h.Depots)
+	case http.MethodDelete:
+		if _, contains := h.Depots[segments[1]]; !contains {
+			vapi.ApiErrorNotFound(w)
+		} else {
+			delete(h.Depots, segments[1])
+			delete(h.DepotComponents, segments[1])
+			vapi.StatusOK(w, "")
+		}
+	case http.MethodPost:
+		var spec depots.SettingsDepotsOfflineCreateSpec
+		if vapi.Decode(r, w, &spec) {
+			// Create depot
+			depot := depots.SettingsDepotsOfflineInfo{}
+			depot.SourceType = spec.SourceType
+			depot.FileId = spec.FileId
+			depot.Location = spec.Location
+			depot.OwnerData = spec.OwnerData
+			depot.Description = spec.Description
+
+			h.depotCounter += 1
+			depotId := fmt.Sprintf("depot-%d", h.depotCounter)
+			h.Depots[depotId] = depot
+
+			// Generate content
+			content := depots.SettingsDepotsOfflineContentInfo{}
+			content.MetadataBundles = make(map[string][]depots.SettingsDepotsMetadataInfo)
+			content.MetadataBundles["dummy-content"] = make([]depots.SettingsDepotsMetadataInfo, 1)
+			bundle := depots.SettingsDepotsMetadataInfo{}
+			bundle.IndependentComponents = make(map[string]depots.SettingsDepotsComponentSummary)
+			independentComp := depots.SettingsDepotsComponentSummary{}
+			independentComp.Versions = make([]depots.ComponentVersion, 1)
+			independentComp.Versions[0].Version = "1.0.0"
+			independentComp.Versions[0].DisplayVersion = "1.0.0"
+			independentComp.DisplayName = "DummyComponent"
+			bundle.IndependentComponents["dummy-component"] = independentComp
+			content.MetadataBundles["dummy-content"][0] = bundle
+			h.DepotComponents[depotId] = content
+
+			vapi.StatusOK(w, fmt.Sprintf("depot-task-%d", h.depotCounter))
+		}
+	}
+}
+
+func (h *Handler) clusters(w http.ResponseWriter, r *http.Request) {
+	subpath := r.URL.Path[len("/api/esx/settings/clusters"):]
+	segments := strings.Split(subpath, "/")
+
+	if len(segments) < 4 || segments[2] != "software" || segments[3] != "drafts" {
+		vapi.ApiErrorUnsupported(w)
+		return
+	}
+
+	switch r.Method {
+	case http.MethodGet:
+		id := fmt.Sprintf("%d", h.draftCounter)
+		if isEndpointSoftwareComponents(segments, id) {
+			if len(segments) > 7 {
+				if comp, contains := h.SoftwareComponents[segments[7]]; contains {
+					vapi.StatusOK(w, comp)
+				} else {
+					vapi.ApiErrorNotFound(w)
+				}
+			} else {
+				vapi.StatusOK(w, h.SoftwareComponents)
+			}
+			return
+		}
+
+		if len(segments) > 4 {
+			if draft, contains := h.SoftwareDrafts[id]; contains {
+				vapi.StatusOK(w, draft)
+			} else {
+				vapi.ApiErrorNotFound(w)
+			}
+		} else {
+			vapi.StatusOK(w, h.SoftwareDrafts)
+		}
+	case http.MethodDelete:
+		id := fmt.Sprintf("%d", h.draftCounter)
+		// component delete
+		if isEndpointSoftwareComponents(segments, id) {
+			if len(segments) > 7 {
+				delete(h.SoftwareComponents, segments[7])
+				vapi.StatusOK(w, "")
+				return
+			}
+		}
+
+		// draft delete
+		if len(segments) > 4 && segments[4] == id {
+			delete(h.SoftwareDrafts, id)
+			vapi.StatusOK(w, "")
+			return
+		}
+
+		vapi.ApiErrorNotFound(w)
+	case http.MethodPost:
+		if strings.Contains(r.URL.RawQuery, "action=commit") {
+			id := fmt.Sprintf("%d", h.draftCounter)
+			if len(segments) > 4 && segments[4] == id {
+				delete(h.SoftwareDrafts, id)
+				vapi.StatusOK(w, "")
+			} else {
+				vapi.ApiErrorNotFound(w)
+			}
+			return
+		}
+		// Only one active draft is permitted
+		if len(h.SoftwareDrafts) > 0 {
+			vapi.ApiErrorNotAllowedInCurrentState(w)
+			return
+		}
+
+		h.draftCounter += 1
+		draft := clusters.SettingsClustersSoftwareDraftsMetadata{}
+		id := fmt.Sprintf("%d", h.draftCounter)
+		h.SoftwareDrafts[id] = draft
+		vapi.StatusOK(w, id)
+	case http.MethodPatch:
+		id := fmt.Sprintf("%d", h.draftCounter)
+		if !isEndpointSoftwareComponents(segments, id) {
+			vapi.ApiErrorUnsupported(w)
+			return
+		}
+
+		var spec clusters.SoftwareComponentsUpdateSpec
+		if vapi.Decode(r, w, &spec) {
+			for k, v := range spec.ComponentsToSet {
+				h.SoftwareComponents[k] = clusters.SettingsComponentInfo{
+					Version: v,
+					Details: clusters.SettingsComponentDetails{DisplayName: "DummyComponent"},
+				}
+			}
+		}
+	}
+}
+
+func isEndpointSoftwareComponents(subpathSegments []string, draftId string) bool {
+	return len(subpathSegments) > 6 &&
+		subpathSegments[4] == draftId &&
+		subpathSegments[5] == "software" &&
+		subpathSegments[6] == "components"
+}
diff --git a/vapi/rest/client.go b/vapi/rest/client.go
index 9cd59e5a..52552bf0 100644
--- a/vapi/rest/client.go
+++ b/vapi/rest/client.go
@@ -205,6 +205,7 @@ func (c *Client) Do(ctx context.Context, req *http.Request, resBody interface{})
 		switch res.StatusCode {
 		case http.StatusOK:
 		case http.StatusCreated:
+		case http.StatusAccepted:
 		case http.StatusNoContent:
 		case http.StatusBadRequest:
 			// TODO: structured error types
diff --git a/vapi/rest/resource.go b/vapi/rest/resource.go
index e6e62749..f89e07e3 100644
--- a/vapi/rest/resource.go
+++ b/vapi/rest/resource.go
@@ -37,6 +37,12 @@ func (r *Resource) String() string {
 	return r.u.String()
 }
 
+// WithSubpath appends the provided subpath to the URL.Path
+func (r *Resource) WithSubpath(subpath string) *Resource {
+	r.u.Path += "/" + subpath
+	return r
+}
+
 // WithID appends id to the URL.Path
 func (r *Resource) WithID(id string) *Resource {
 	r.u.Path += "/id:" + id
diff --git a/vcsim/main.go b/vcsim/main.go
index 31e921e6..bf7faef2 100644
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -47,7 +47,9 @@ import (
 	_ "github.com/vmware/govmomi/ssoadmin/simulator"
 	_ "github.com/vmware/govmomi/sts/simulator"
 	_ "github.com/vmware/govmomi/vapi/appliance/simulator"
+	_ "github.com/vmware/govmomi/vapi/cis/tasks/simulator"
 	_ "github.com/vmware/govmomi/vapi/cluster/simulator"
+	_ "github.com/vmware/govmomi/vapi/esx/settings/simulator"
 	_ "github.com/vmware/govmomi/vapi/namespace/simulator"
 	_ "github.com/vmware/govmomi/vapi/simulator"
 	_ "github.com/vmware/govmomi/vapi/vm/simulator"
diff --git a/vim25/types/esxi_version.go b/vim25/types/esxi_version.go
deleted file mode 100644
index e5c7f8d0..00000000
--- a/vim25/types/esxi_version.go
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
-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 types
-
-import (
-	"fmt"
-	"regexp"
-	"strconv"
-)
-
-// ESXiVersion is an ESXi version.
-type ESXiVersion uint8
-
-const (
-	esxiVersionBegin ESXiVersion = iota
-
-	ESXi2000
-	ESXi3000
-	ESXi4000
-	ESXi5000
-	ESXi5100
-	ESXi5500
-	ESXi6000
-	ESXi6500
-	ESXi6700
-	ESXi6720
-	ESXi7000
-	ESXi7010
-	ESXi7020
-	ESXi8000
-	ESXi8010
-	ESXi8020
-
-	esxiVersionEnd
-)
-
-// HardwareVersion returns the maximum hardware version supported by this
-// version of ESXi, per https://kb.vmware.com/s/article/1003746.
-func (ev ESXiVersion) HardwareVersion() HardwareVersion {
-	switch ev {
-	case ESXi2000:
-		return VMX3
-	case ESXi3000:
-		return VMX4
-	case ESXi4000:
-		return VMX7
-	case ESXi5000:
-		return VMX8
-	case ESXi5100:
-		return VMX9
-	case ESXi5500:
-		return VMX10
-	case ESXi6000:
-		return VMX11
-	case ESXi6500:
-		return VMX13
-	case ESXi6700:
-		return VMX14
-	case ESXi6720:
-		return VMX15
-	case ESXi7000:
-		return VMX17
-	case ESXi7010:
-		return VMX18
-	case ESXi7020:
-		return VMX19
-	case ESXi8000, ESXi8010:
-		return VMX20
-	case ESXi8020:
-		return VMX21
-	}
-	return 0
-}
-
-// IsHardwareVersionSupported returns true if the provided hardware version is
-// supported by the given version of ESXi.
-func (ev ESXiVersion) IsHardwareVersionSupported(hv HardwareVersion) bool {
-	return hv <= ev.HardwareVersion()
-}
-
-func (ev ESXiVersion) IsValid() bool {
-	return ev.String() != ""
-}
-
-func (ev ESXiVersion) String() string {
-	switch ev {
-	case ESXi2000:
-		return "2"
-	case ESXi3000:
-		return "3"
-	case ESXi4000:
-		return "4"
-	case ESXi5000:
-		return "5.0"
-	case ESXi5100:
-		return "5.1"
-	case ESXi5500:
-		return "5.5"
-	case ESXi6000:
-		return "6.0"
-	case ESXi6500:
-		return "6.5"
-	case ESXi6700:
-		return "6.7"
-	case ESXi6720:
-		return "6.7.2"
-	case ESXi7000:
-		return "7.0"
-	case ESXi7010:
-		return "7.0.1"
-	case ESXi7020:
-		return "7.0.2"
-	case ESXi8000:
-		return "8.0"
-	case ESXi8010:
-		return "8.0.1"
-	case ESXi8020:
-		return "8.0.2"
-	}
-	return ""
-}
-
-func (ev ESXiVersion) MarshalText() ([]byte, error) {
-	return []byte(ev.String()), nil
-}
-
-func (ev *ESXiVersion) UnmarshalText(text []byte) error {
-	v, err := ParseESXiVersion(string(text))
-	if err != nil {
-		return err
-	}
-	*ev = v
-	return nil
-}
-
-// MustParseESXiVersion parses the provided string into an ESXi version.
-func MustParseESXiVersion(s string) ESXiVersion {
-	v, err := ParseESXiVersion(s)
-	if err != nil {
-		panic(err)
-	}
-	return v
-}
-
-var esxiRe = regexp.MustCompile(`(?i)^v?(\d)(?:\.(\d))?(?:\.(\d))?(?:\s*u(\d))?$`)
-
-// ParseESXiVersion parses the provided string into an ESXi version.
-func ParseESXiVersion(s string) (ESXiVersion, error) {
-	if m := esxiRe.FindStringSubmatch(s); len(m) > 0 {
-		var (
-			major  int64
-			minor  int64
-			patch  int64
-			update int64
-		)
-
-		major, _ = strconv.ParseInt(m[1], 0, 0)
-		if len(m) > 2 {
-			minor, _ = strconv.ParseInt(m[2], 0, 0)
-		}
-		if len(m) > 3 {
-			patch, _ = strconv.ParseInt(m[3], 0, 0)
-		}
-		if len(m) > 4 {
-			update, _ = strconv.ParseInt(m[4], 0, 0)
-		}
-
-		switch {
-		case major == 2 && minor == 0 && patch == 0 && update == 0:
-			return ESXi2000, nil
-		case major == 3 && minor == 0 && patch == 0 && update == 0:
-			return ESXi3000, nil
-		case major == 4 && minor == 0 && patch == 0 && update == 0:
-			return ESXi4000, nil
-		case major == 5 && minor == 0 && patch == 0 && update == 0:
-			return ESXi5000, nil
-		case major == 5 && minor == 1 && patch == 0 && update == 0:
-			return ESXi5100, nil
-		case major == 5 && minor == 5 && patch == 0 && update == 0:
-			return ESXi5500, nil
-		case major == 6 && minor == 0 && patch == 0 && update == 0:
-			return ESXi6000, nil
-		case major == 6 && minor == 5 && patch == 0 && update == 0:
-			return ESXi6500, nil
-		case major == 6 && minor == 7 && patch == 0 && update == 0:
-			return ESXi6700, nil
-		case major == 6 && minor == 7 && patch == 2 && update == 0,
-			major == 6 && minor == 7 && patch == 0 && update == 2:
-			return ESXi6720, nil
-		case major == 7 && minor == 0 && patch == 0 && update == 0:
-			return ESXi7000, nil
-		case major == 7 && minor == 0 && patch == 1 && update == 0,
-			major == 7 && minor == 0 && patch == 0 && update == 1:
-			return ESXi7010, nil
-		case major == 7 && minor == 0 && patch == 2 && update == 0,
-			major == 7 && minor == 0 && patch == 0 && update == 2:
-			return ESXi7020, nil
-		case major == 8 && minor == 0 && patch == 0 && update == 0:
-			return ESXi8000, nil
-		case major == 8 && minor == 0 && patch == 1 && update == 0,
-			major == 8 && minor == 0 && patch == 0 && update == 1:
-			return ESXi8010, nil
-		case major == 8 && minor == 0 && patch == 2 && update == 0,
-			major == 8 && minor == 0 && patch == 0 && update == 2:
-			return ESXi8020, nil
-		}
-	}
-
-	return 0, fmt.Errorf("invalid version: %q", s)
-}
-
-// GetESXiVersions returns a list of ESXi versions.
-func GetESXiVersions() []ESXiVersion {
-	dst := make([]ESXiVersion, esxiVersionEnd-1)
-	for i := esxiVersionBegin + 1; i < esxiVersionEnd; i++ {
-		dst[i-1] = i
-	}
-	return dst
-}
diff --git a/vim25/types/hardware_version.go b/vim25/types/hardware_version.go
deleted file mode 100644
index 39677577..00000000
--- a/vim25/types/hardware_version.go
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
-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 types
-
-import (
-	"fmt"
-	"regexp"
-	"strconv"
-)
-
-// HardwareVersion is a VMX hardware version.
-type HardwareVersion uint8
-
-const (
-	VMX3 HardwareVersion = iota + 3
-	VMX4
-
-	vmx5 // invalid
-
-	VMX6
-	VMX7
-	VMX8
-	VMX9
-	VMX10
-	VMX11
-	VMX12
-	VMX13
-	VMX14
-	VMX15
-	VMX16
-	VMX17
-	VMX18
-	VMX19
-	VMX20
-	VMX21
-)
-
-const (
-	// MinValidHardwareVersion is the minimum, valid hardware version supported
-	// by VMware hypervisors in the wild.
-	MinValidHardwareVersion = VMX3
-
-	// MaxValidHardwareVersion is the maximum, valid hardware version supported
-	// by VMware hypervisors in the wild.
-	MaxValidHardwareVersion = VMX21
-)
-
-func (hv HardwareVersion) IsValid() bool {
-	return hv != vmx5 &&
-		hv >= MinValidHardwareVersion &&
-		hv <= MaxValidHardwareVersion
-}
-
-func (hv HardwareVersion) String() string {
-	if hv.IsValid() {
-		return fmt.Sprintf("vmx-%d", hv)
-	}
-	return ""
-}
-
-func (hv HardwareVersion) MarshalText() ([]byte, error) {
-	return []byte(hv.String()), nil
-}
-
-func (hv *HardwareVersion) UnmarshalText(text []byte) error {
-	v, err := ParseHardwareVersion(string(text))
-	if err != nil {
-		return err
-	}
-	*hv = v
-	return nil
-}
-
-var vmxRe = regexp.MustCompile(`(?i)^vmx-(\d+)$`)
-
-// MustParseHardwareVersion parses the provided string into a hardware version.
-func MustParseHardwareVersion(s string) HardwareVersion {
-	v, err := ParseHardwareVersion(s)
-	if err != nil {
-		panic(err)
-	}
-	return v
-}
-
-// ParseHardwareVersion parses the provided string into a hardware version.
-func ParseHardwareVersion(s string) (HardwareVersion, error) {
-	var u uint64
-	if m := vmxRe.FindStringSubmatch(s); len(m) > 0 {
-		u, _ = strconv.ParseUint(m[1], 10, 8)
-	} else {
-		u, _ = strconv.ParseUint(s, 10, 8)
-	}
-	v := HardwareVersion(u)
-	if !v.IsValid() {
-		return 0, fmt.Errorf("invalid version: %q", s)
-	}
-	return v, nil
-}
-
-var hardwareVersions []HardwareVersion
-
-func init() {
-	for i := MinValidHardwareVersion; i <= MaxValidHardwareVersion; i++ {
-		if i.IsValid() {
-			hardwareVersions = append(hardwareVersions, i)
-		}
-	}
-}
-
-// GetHardwareVersions returns a list of hardware versions.
-func GetHardwareVersions() []HardwareVersion {
-	dst := make([]HardwareVersion, len(hardwareVersions))
-	copy(dst, hardwareVersions)
-	return dst
-}


Test patch:
diff --git a/govc/test/vlcm.bats b/govc/test/vlcm.bats
new file mode 100755
index 00000000..2e663f6a
--- /dev/null
+++ b/govc/test/vlcm.bats
@@ -0,0 +1,165 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+location="http://127.0.0.1:9999/files/custom_driver.zip"
+
+@test "vlcm.depot.offline.ls" {
+  vcsim_env
+
+  run govc vlcm.depot.offline.ls
+  assert_success
+}
+
+@test "vlcm.depot.offline.create" {
+  vcsim_env
+
+  run govc vlcm.depot.offline.create -l=$location -source-type=PULL
+  assert_success
+
+  depot=$(govc vlcm.depot.offline.ls | jq '."depot-1"')
+  l=$(echo $depot | jq -r '.location')
+  assert_equal $location $l
+  srcType=$(echo $depot | jq -r '."source_type"')
+  assert_equal "PULL" $srcType
+}
+
+@test "vlcm.depot.offline.info" {
+  vcsim_env
+
+  run govc vlcm.depot.offline.create -l=$location -source-type=PULL
+  assert_success ""
+
+  depot=$(govc vlcm.depot.offline.info -depot-id=depot-1)
+  comp=$(echo $depot | jq '."metadata_bundles"."dummy-content"[0]."independent_components"."dummy-component"')
+  name=$(echo $comp | jq -r '."display_name"')
+  assert_equal "DummyComponent" $name
+  version=$(echo $comp | jq -r '.versions[0].version')
+  displayVersion=$(echo $comp | jq -r '.versions[0]."display_version"')
+  assert_equal "1.0.0" $version
+  assert_equal "1.0.0" $displayVersion
+}
+
+@test "vlcm.depot.offline.rm" {
+  vcsim_env
+
+  run govc vlcm.depot.offline.create -l=$location -source-type=PULL
+  assert_success
+
+  run govc vlcm.depot.offline.rm -depot-id=depot-1
+  assert_success
+
+  run govc vlcm.depot.offline.info -depot-id=depot-1
+  assert_failure
+  assert_matches "404 Not Found"
+}
+
+@test "cluster.draft.ls" {
+  vcsim_env
+
+  run govc cluster.draft.ls -cluster-id=domain-c21
+  assert_success
+}
+
+@test "cluster.draft.create" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  draft=$(govc cluster.draft.ls -cluster-id=domain-c21 | jq '."1"')
+  assert_equal "" $(echo $draft | jq -r '.owner')
+}
+
+@test "cluster.draft.info" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  run govc cluster.draft.info -cluster-id=domain-c21 -draft-id=1
+  assert_success
+}
+
+@test "cluster.draft.rm" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  run govc cluster.draft.rm -cluster-id=domain-c21 -draft-id=1
+
+  run govc cluster.draft.info -cluster-id=domain-c21 -draft-id=1
+  assert_failure
+  assert_matches "404 Not Found"
+}
+
+@test "cluster.draft.commit" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  run govc cluster.draft.commit -cluster-id=domain-c21 -draft-id=1
+
+  run govc cluster.draft.info -cluster-id=domain-c21 -draft-id=1
+  assert_failure
+  assert_matches "404 Not Found"
+}
+
+@test "cluster.draft.component.ls" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  run govc cluster.draft.component.ls -cluster-id=domain-c21 -draft-id=1
+  assert_success
+}
+
+@test "cluster.draft.component.add" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  run govc cluster.draft.component.add -cluster-id=domain-c21 -draft-id=1 -component-id=comp-id -component-version=1.2.3.4
+  assert_success
+
+  comp=$(govc cluster.draft.component.ls -cluster-id=domain-c21 -draft-id=1 | jq '."comp-id"')
+  assert_equal "1.2.3.4" $(echo $comp | jq -r '.version')
+  assert_equal "DummyComponent" $(echo $comp | jq -r '.details.display_name')
+}
+
+@test "cluster.draft.component.info" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  run govc cluster.draft.component.add -cluster-id=domain-c21 -draft-id=1 -component-id=comp-id -component-version=1.2.3.4
+  assert_success
+
+  comp=$(govc cluster.draft.component.info -cluster-id=domain-c21 -draft-id=1 -component-id=comp-id)
+  assert_equal "1.2.3.4" $(echo $comp | jq -r '.version')
+  assert_equal "DummyComponent" $(echo $comp | jq -r '.details.display_name')
+  run govc cluster.draft.component.info -cluster-id=domain-c21 -draft-id=1 -component-id=invalid-id
+  assert_failure
+  assert_matches "404 Not Found"
+}
+
+@test "cluster.draft.component.rm" {
+  vcsim_env
+
+  run govc cluster.draft.create -cluster-id=domain-c21
+  assert_success
+
+  run govc cluster.draft.component.add -cluster-id=domain-c21 -draft-id=1 -component-id=comp-id -component-version=1.2.3.4
+  assert_success
+
+  run govc cluster.draft.component.rm -cluster-id=domain-c21 -draft-id=1 -component-id=comp-id
+
+  run govc cluster.draft.component.info -cluster-id=domain-c21 -draft-id=1 -component-id=comp-id
+  assert_failure
+  assert_matches "404 Not Found"
+}
\ No newline at end of file
diff --git a/govc/test/vm.bats b/govc/test/vm.bats
index a76c28c5..999f5930 100755
--- a/govc/test/vm.bats
+++ b/govc/test/vm.bats
@@ -1043,7 +1043,7 @@ load test_helper
   [[ "$version" > "vmx-10" ]]
 
   run govc vm.upgrade -vm "$vm"
-  assert_failure
+  assert_success
 
   run govc vm.create -on=false -version vmx-11 "$(new_id)"
   assert_success
diff --git a/simulator/virtual_machine_test.go b/simulator/virtual_machine_test.go
index 4de23eec..57ba4008 100644
--- a/simulator/virtual_machine_test.go
+++ b/simulator/virtual_machine_test.go
@@ -2238,296 +2238,11 @@ func TestLastModifiedAndChangeVersionAreUpdated(t *testing.T) {
 }
 
 func TestUpgradeVm(t *testing.T) {
-
-	const (
-		vmx1  = "vmx-1"
-		vmx2  = "vmx-2"
-		vmx15 = "vmx-15"
-		vmx17 = "vmx-17"
-		vmx19 = "vmx-19"
-		vmx20 = "vmx-20"
-		vmx21 = "vmx-21"
-		vmx22 = "vmx-22"
-	)
-
-	model := VPX()
-	model.Autostart = false
-	model.Cluster = 1
-	model.ClusterHost = 1
-	model.Host = 1
-
 	Test(func(ctx context.Context, c *vim25.Client) {
 		props := []string{"config.version", "summary.config.hwVersion"}
-
 		vm := object.NewVirtualMachine(c, Map.Any("VirtualMachine").Reference())
-		vm2 := Map.Get(vm.Reference()).(*VirtualMachine)
-		Map.WithLock(SpoofContext(), vm2.Reference(), func() {
-			vm2.Config.Version = vmx15
-		})
-
-		host, err := vm.HostSystem(ctx)
-		if err != nil {
-			t.Fatalf("failed to get vm's host: %v", err)
-		}
-		host2 := Map.Get(host.Reference()).(*HostSystem)
-
-		var eb *EnvironmentBrowser
-		{
-			ref := Map.Get(host.Reference()).(*HostSystem).Parent
-			switch ref.Type {
-			case "ClusterComputeResource":
-				obj := Map.Get(*ref).(*ClusterComputeResource)
-				eb = Map.Get(*obj.EnvironmentBrowser).(*EnvironmentBrowser)
-			case "ComputeResource":
-				obj := Map.Get(*ref).(*mo.ComputeResource)
-				eb = Map.Get(*obj.EnvironmentBrowser).(*EnvironmentBrowser)
-			}
-		}
-
-		baseline := func() {
-			Map.WithLock(SpoofContext(), vm2.Reference(), func() {
-				vm2.Config.Version = vmx15
-				vm2.Config.Template = false
-				vm2.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOff
-			})
-			Map.WithLock(SpoofContext(), host2.Reference(), func() {
-				host2.Runtime.InMaintenanceMode = false
-			})
-			Map.WithLock(SpoofContext(), eb.Reference(), func() {
-				for i := range eb.QueryConfigOptionDescriptorResponse.Returnval {
-					cod := &eb.QueryConfigOptionDescriptorResponse.Returnval[i]
-					hostFound := false
-					for j := range cod.Host {
-						if cod.Host[j].Value == host2.Reference().Value {
-							hostFound = true
-							break
-						}
-					}
-					if !hostFound {
-						cod.Host = append(cod.Host, host2.Reference())
-					}
-				}
-			})
-		}
-
-		t.Run("InvalidPowerState", func(t *testing.T) {
-			baseline()
-			Map.WithLock(SpoofContext(), vm2.Reference(), func() {
-				vm2.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOn
-			})
-
-			tsk, err := vm.UpgradeVM(ctx, vmx15)
-			if err != nil {
-				t.Fatalf("failed to call upgradeVm api: %v", err)
-			}
-			if _, err := tsk.WaitForResultEx(ctx); err == nil {
-				t.Fatal("expected error did not occur")
-			} else if err2, ok := err.(task.Error); !ok {
-				t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-			} else if f := err2.Fault(); f == nil {
-				t.Fatal("fault is nil")
-			} else if f2, ok := f.(*types.InvalidPowerStateFault); !ok {
-				t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-			} else {
-				if f2.ExistingState != types.VirtualMachinePowerStatePoweredOn {
-					t.Errorf("unexpected existing state: %v", f2.ExistingState)
-				}
-				if f2.RequestedState != types.VirtualMachinePowerStatePoweredOff {
-					t.Errorf("unexpected requested state: %v", f2.RequestedState)
-				}
-			}
-		})
 
-		t.Run("InvalidState", func(t *testing.T) {
-			t.Run("MaintenanceMode", func(t *testing.T) {
-				baseline()
-				Map.WithLock(SpoofContext(), host2.Reference(), func() {
-					host2.Runtime.InMaintenanceMode = true
-				})
-
-				if tsk, err := vm.UpgradeVM(ctx, vmx15); err != nil {
-					t.Fatalf("failed to call upgradeVm api: %v", err)
-				} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-					t.Fatal("expected error did not occur")
-				} else if err, ok := err.(task.Error); !ok {
-					t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-				} else if f := err.Fault(); f == nil {
-					t.Fatal("fault is nil")
-				} else if f2, ok := f.(*types.InvalidState); !ok {
-					t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-				} else if fc := f2.FaultCause; fc == nil {
-					t.Fatal("fault cause is nil")
-				} else if fc.LocalizedMessage != fmt.Sprintf("%s in maintenance mode", host.Reference().Value) {
-					t.Fatalf("unexpected error message: %s", fc.LocalizedMessage)
-				}
-			})
-
-			t.Run("Template", func(t *testing.T) {
-				baseline()
-				Map.WithLock(SpoofContext(), vm2.Reference(), func() {
-					vm2.Config.Template = true
-				})
-
-				if tsk, err := vm.UpgradeVM(ctx, vmx15); err != nil {
-					t.Fatalf("failed to call upgradeVm api: %v", err)
-				} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-					t.Fatal("expected error did not occur")
-				} else if err, ok := err.(task.Error); !ok {
-					t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-				} else if f := err.Fault(); f == nil {
-					t.Fatal("fault is nil")
-				} else if f2, ok := f.(*types.InvalidState); !ok {
-					t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-				} else if fc := f2.FaultCause; fc == nil {
-					t.Fatal("fault cause is nil")
-				} else if fc.LocalizedMessage != fmt.Sprintf("%s is template", vm.Reference().Value) {
-					t.Fatalf("unexpected error message: %s", fc.LocalizedMessage)
-				}
-			})
-
-			t.Run("LatestHardwareVersion", func(t *testing.T) {
-				baseline()
-				Map.WithLock(SpoofContext(), vm.Reference(), func() {
-					vm2.Config.Version = vmx21
-				})
-
-				if tsk, err := vm.UpgradeVM(ctx, vmx21); err != nil {
-					t.Fatalf("failed to call upgradeVm api: %v", err)
-				} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-					t.Fatal("expected error did not occur")
-				} else if err, ok := err.(task.Error); !ok {
-					t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-				} else if f := err.Fault(); f == nil {
-					t.Fatal("fault is nil")
-				} else if f2, ok := f.(*types.InvalidState); !ok {
-					t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-				} else if fc := f2.FaultCause; fc == nil {
-					t.Fatal("fault cause is nil")
-				} else if fc.LocalizedMessage != fmt.Sprintf("%s is latest version", vm.Reference().Value) {
-					t.Fatalf("unexpected error message: %s", fc.LocalizedMessage)
-				}
-			})
-		})
-
-		t.Run("NotSupported", func(t *testing.T) {
-			t.Run("AtAll", func(t *testing.T) {
-				baseline()
-
-				if tsk, err := vm.UpgradeVM(ctx, vmx22); err != nil {
-					t.Fatalf("failed to call upgradeVm api: %v", err)
-				} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-					t.Fatal("expected error did not occur")
-				} else if err, ok := err.(task.Error); !ok {
-					t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-				} else if f := err.Fault(); f == nil {
-					t.Fatal("fault is nil")
-				} else if f2, ok := f.(*types.NotSupported); !ok {
-					t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-				} else if fc := f2.FaultCause; fc == nil {
-					t.Fatal("fault cause is nil")
-				} else if fc.LocalizedMessage != "vmx-22 not supported" {
-					t.Fatalf("unexpected error message: %s", fc.LocalizedMessage)
-				}
-			})
-			t.Run("OnVmHost", func(t *testing.T) {
-				baseline()
-				Map.WithLock(SpoofContext(), eb.Reference(), func() {
-					for i := range eb.QueryConfigOptionDescriptorResponse.Returnval {
-						cod := &eb.QueryConfigOptionDescriptorResponse.Returnval[i]
-						if cod.Key == vmx17 {
-							cod.Host = nil
-						}
-					}
-				})
-
-				if tsk, err := vm.UpgradeVM(ctx, vmx17); err != nil {
-					t.Fatalf("failed to call upgradeVm api: %v", err)
-				} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-					t.Fatal("expected error did not occur")
-				} else if err, ok := err.(task.Error); !ok {
-					t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-				} else if f := err.Fault(); f == nil {
-					t.Fatal("fault is nil")
-				} else if f2, ok := f.(*types.NotSupported); !ok {
-					t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-				} else if fc := f2.FaultCause; fc == nil {
-					t.Fatal("fault cause is nil")
-				} else if fc.LocalizedMessage != "vmx-17 not supported" {
-					t.Fatalf("unexpected error message: %s", fc.LocalizedMessage)
-				}
-			})
-		})
-
-		t.Run("AlreadyUpgraded", func(t *testing.T) {
-			t.Run("EqualToTargetVersion", func(t *testing.T) {
-				baseline()
-				if tsk, err := vm.UpgradeVM(ctx, vmx15); err != nil {
-					t.Fatalf("failed to call upgradeVm api: %v", err)
-				} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-					t.Fatal("expected error did not occur")
-				} else if err, ok := err.(task.Error); !ok {
-					t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-				} else if f := err.Fault(); f == nil {
-					t.Fatal("fault is nil")
-				} else if _, ok := f.(*types.AlreadyUpgradedFault); !ok {
-					t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-				}
-			})
-
-			t.Run("GreaterThanTargetVersion", func(t *testing.T) {
-				baseline()
-				Map.WithLock(SpoofContext(), vm2.Reference(), func() {
-					vm2.Config.Version = vmx20
-				})
-				if tsk, err := vm.UpgradeVM(ctx, vmx17); err != nil {
-					t.Fatalf("failed to call upgradeVm api: %v", err)
-				} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-					t.Fatal("expected error did not occur")
-				} else if err, ok := err.(task.Error); !ok {
-					t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-				} else if f := err.Fault(); f == nil {
-					t.Fatal("fault is nil")
-				} else if _, ok := f.(*types.AlreadyUpgradedFault); !ok {
-					t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-				}
-			})
-		})
-
-		t.Run("InvalidArgument", func(t *testing.T) {
-			baseline()
-			Map.WithLock(SpoofContext(), vm2.Reference(), func() {
-				vm2.Config.Version = vmx1
-			})
-			Map.WithLock(SpoofContext(), eb.Reference(), func() {
-				eb.QueryConfigOptionDescriptorResponse.Returnval = append(
-					eb.QueryConfigOptionDescriptorResponse.Returnval,
-					types.VirtualMachineConfigOptionDescriptor{
-						Key:  vmx2,
-						Host: []types.ManagedObjectReference{host.Reference()},
-					})
-			})
-
-			if tsk, err := vm.UpgradeVM(ctx, vmx2); err != nil {
-				t.Fatalf("failed to call upgradeVm api: %v", err)
-			} else if _, err := tsk.WaitForResultEx(ctx); err == nil {
-				t.Fatal("expected error did not occur")
-			} else if err, ok := err.(task.Error); !ok {
-				t.Fatalf("unexpected error: %[1]T %+[1]v", err)
-			} else if f := err.Fault(); f == nil {
-				t.Fatal("fault is nil")
-			} else if _, ok := f.(*types.InvalidArgument); !ok {
-				t.Fatalf("unexpected fault: %[1]T %+[1]v", f)
-			}
-		})
-
-		t.Run("UpgradeToLatest", func(t *testing.T) {
-			baseline()
-
-			if tsk, err := vm.UpgradeVM(ctx, ""); err != nil {
-				t.Fatalf("failed to call upgradeVm api: %v", err)
-			} else if _, err := tsk.WaitForResultEx(ctx); err != nil {
-				t.Fatalf("failed to upgrade vm: %v", err)
-			}
+		updateHwVersionAndAssert := func(updateFn func()) {
 			var vmMo mo.VirtualMachine
 			if err := vm.Properties(
 				ctx,
@@ -2535,119 +2250,55 @@ func TestUpgradeVm(t *testing.T) {
 				props,
 				&vmMo); err != nil {
 
-				t.Fatalf("failed to fetch vm props after upgrade: %v", err)
-			}
-			if v := vmMo.Config.Version; v != vmx21 {
-				t.Fatalf("unexpected config.version %v", v)
+				t.Fatalf("failed to fetch vm props: %v", err)
 			}
-			if v := vmMo.Summary.Config.HwVersion; v != vmx21 {
-				t.Fatalf("unexpected summary.config.hwVersion %v", v)
-			}
-		})
 
-		t.Run("UpgradeFrom15To17", func(t *testing.T) {
-			const targetVersion = vmx17
-			baseline()
+			oldConfigVersion := vmMo.Config.Version
+			oldSummaryConfigVersion := vmMo.Summary.Config.HwVersion
+
+			updateFn()
 
-			if tsk, err := vm.UpgradeVM(ctx, targetVersion); err != nil {
-				t.Fatalf("failed to call upgradeVm api: %v", err)
-			} else if _, err := tsk.WaitForResultEx(ctx); err != nil {
-				t.Fatalf("failed to upgrade vm: %v", err)
-			}
-			var vmMo mo.VirtualMachine
 			if err := vm.Properties(
 				ctx,
 				vm.Reference(),
 				props,
 				&vmMo); err != nil {
 
-				t.Fatalf("failed to fetch vm props after upgrade: %v", err)
-			}
-			if v := vmMo.Config.Version; v != targetVersion {
-				t.Fatalf("unexpected config.version %v", v)
-			}
-			if v := vmMo.Summary.Config.HwVersion; v != targetVersion {
-				t.Fatalf("unexpected summary.config.hwVersion %v", v)
+				t.Fatalf("failed to fetch vm props after reconfigure: %v", err)
 			}
-		})
-
-		t.Run("UpgradeFrom17To20", func(t *testing.T) {
-			const targetVersion = vmx20
-			baseline()
-			Map.WithLock(SpoofContext(), vm2.Reference(), func() {
-				vm2.Config.Version = vmx17
-			})
 
-			if tsk, err := vm.UpgradeVM(ctx, targetVersion); err != nil {
-				t.Fatalf("failed to call upgradeVm api: %v", err)
-			} else if _, err := tsk.WaitForResultEx(ctx); err != nil {
-				t.Fatalf("failed to upgrade vm: %v", err)
-			}
-			var vmMo mo.VirtualMachine
-			if err := vm.Properties(
-				ctx,
-				vm.Reference(),
-				props,
-				&vmMo); err != nil {
+			newConfigVersion := vmMo.Config.Version
+			newSummaryConfigVersion := vmMo.Summary.Config.HwVersion
 
-				t.Fatalf("failed to fetch vm props after upgrade: %v", err)
+			if a, e := newConfigVersion, oldConfigVersion; a == e {
+				t.Errorf("config.version was not updated: %v", a)
 			}
-			if v := vmMo.Config.Version; v != targetVersion {
-				t.Fatalf("unexpected config.version %v", v)
-			}
-			if v := vmMo.Summary.Config.HwVersion; v != targetVersion {
-				t.Fatalf("unexpected summary.config.hwVersion %v", v)
-			}
-		})
 
-		t.Run("UpgradeFrom15To17To20", func(t *testing.T) {
-			const (
-				targetVersion1 = vmx17
-				targetVersion2 = vmx20
-			)
-			baseline()
-
-			if tsk, err := vm.UpgradeVM(ctx, targetVersion1); err != nil {
-				t.Fatalf("failed to call upgradeVm api first time: %v", err)
-			} else if _, err := tsk.WaitForResultEx(ctx); err != nil {
-				t.Fatalf("failed to upgrade vm first time: %v", err)
+			if a, e := newSummaryConfigVersion, oldSummaryConfigVersion; a == e {
+				t.Errorf("summary.config.hwVersion was not updated: %v", a)
 			}
-			var vmMo mo.VirtualMachine
-			if err := vm.Properties(
-				ctx,
-				vm.Reference(),
-				props,
-				&vmMo); err != nil {
+		}
 
-				t.Fatalf("failed to fetch vm props after first upgrade: %v", err)
-			}
-			if v := vmMo.Config.Version; v != targetVersion1 {
-				t.Fatalf("unexpected config.version after first upgrade %v", v)
-			}
-			if v := vmMo.Summary.Config.HwVersion; v != targetVersion1 {
-				t.Fatalf("unexpected summary.config.hwVersion after first upgrade %v", v)
+		updateHwVersionAndAssert(func() {
+			tsk, err := vm.Reconfigure(ctx, types.VirtualMachineConfigSpec{
+				Version: "vmx-123",
+			})
+			if err != nil {
+				t.Fatalf("failed to call reconfigure api: %v", err)
 			}
-
-			if tsk, err := vm.UpgradeVM(ctx, targetVersion2); err != nil {
-				t.Fatalf("failed to call upgradeVm api second time: %v", err)
-			} else if _, err := tsk.WaitForResultEx(ctx); err != nil {
-				t.Fatalf("failed to upgrade vm second time: %v", err)
+			if err := tsk.WaitEx(ctx); err != nil {
+				t.Fatalf("failed to reconfigure: %v", err)
 			}
-			if err := vm.Properties(
-				ctx,
-				vm.Reference(),
-				props,
-				&vmMo); err != nil {
+		})
 
-				t.Fatalf("failed to fetch vm props after second upgrade: %v", err)
-			}
-			if v := vmMo.Config.Version; v != targetVersion2 {
-				t.Fatalf("unexpected config.version after second upgrade %v", v)
+		updateHwVersionAndAssert(func() {
+			tsk, err := vm.UpgradeVM(ctx, "vmx-456")
+			if err != nil {
+				t.Fatalf("failed to call upgradeVm api: %v", err)
 			}
-			if v := vmMo.Summary.Config.HwVersion; v != targetVersion2 {
-				t.Fatalf("unexpected summary.config.hwVersion after second upgrade %v", v)
+			if err := tsk.WaitEx(ctx); err != nil {
+				t.Fatalf("failed to upgradeVm: %v", err)
 			}
 		})
-
-	}, model)
+	})
 }
diff --git a/vim25/types/esxi_version_test.go b/vim25/types/esxi_version_test.go
deleted file mode 100644
index 61ac539a..00000000
--- a/vim25/types/esxi_version_test.go
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
-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 types
-
-import (
-	"fmt"
-	"sort"
-	"strconv"
-	"testing"
-)
-
-func TestESXiVersion(t *testing.T) {
-
-	var uniqueExpectedVersions []ESXiVersion
-
-	type testCase struct {
-		name                    string
-		in                      string
-		expectedIsValid         bool
-		expectedVersion         ESXiVersion
-		expectedString          string
-		expectedHardwareVersion HardwareVersion
-	}
-
-	testCasesForMajorVersion := func(
-		major int,
-		expectedVersion ESXiVersion,
-		expectedString string,
-		expectedHardwareVersion HardwareVersion) []testCase {
-
-		uniqueExpectedVersions = append(uniqueExpectedVersions, MustParseESXiVersion(expectedString))
-
-		szMajor := strconv.Itoa(major)
-		return []testCase{
-			{
-				name:                    szMajor,
-				in:                      szMajor,
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-			{
-				name:                    szMajor + ".0",
-				in:                      szMajor + ".0",
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-			{
-				name:                    szMajor + ".0.0",
-				in:                      szMajor + ".0.0",
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-		}
-	}
-
-	testCasesForMajorMinorVersion := func(
-		major, minor int,
-		expectedVersion ESXiVersion,
-		expectedString string,
-		expectedHardwareVersion HardwareVersion) []testCase {
-
-		uniqueExpectedVersions = append(uniqueExpectedVersions, MustParseESXiVersion(expectedString))
-
-		szMajor := strconv.Itoa(major)
-		szMinor := strconv.Itoa(minor)
-
-		return []testCase{
-			{
-				name:                    szMajor + "." + szMinor,
-				in:                      szMajor + "." + szMinor,
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-			{
-				name:                    szMajor + "." + szMinor + ".0",
-				in:                      szMajor + "." + szMinor + ".0",
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-		}
-	}
-
-	testCasesForMajorMinorUpdateVersion := func(
-		major, minor, update int,
-		expectedVersion ESXiVersion,
-		expectedString string,
-		expectedHardwareVersion HardwareVersion) []testCase {
-
-		uniqueExpectedVersions = append(uniqueExpectedVersions, MustParseESXiVersion(expectedString))
-
-		szMajor := strconv.Itoa(major)
-		szMinor := strconv.Itoa(minor)
-		szUpdate := strconv.Itoa(update)
-
-		return []testCase{
-			{
-				name:                    szMajor + "." + szMinor + "." + szUpdate,
-				in:                      szMajor + "." + szMinor + "." + szUpdate,
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-			{
-				name:                    szMajor + "." + szMinor + "u" + szUpdate,
-				in:                      szMajor + "." + szMinor + "u" + szUpdate,
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-			{
-				name:                    szMajor + "." + szMinor + "U" + szUpdate,
-				in:                      szMajor + "." + szMinor + "U" + szUpdate,
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-			{
-				name:                    szMajor + "." + szMinor + " u" + szUpdate,
-				in:                      szMajor + "." + szMinor + " u" + szUpdate,
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-			{
-				name:                    szMajor + "." + szMinor + " U" + szUpdate,
-				in:                      szMajor + "." + szMinor + " U" + szUpdate,
-				expectedIsValid:         true,
-				expectedVersion:         expectedVersion,
-				expectedString:          expectedString,
-				expectedHardwareVersion: expectedHardwareVersion,
-			},
-		}
-	}
-
-	testCases := []testCase{
-		{
-			name: "EmptyString",
-			in:   "",
-		},
-		{
-			name: "InvalidMajorVersion",
-			in:   "1",
-		},
-		{
-			name: "ValidMajorInvalidMinorVersion",
-			in:   "2.1",
-		},
-		{
-			name: "ValidMajorValidMinorInvalidPatchVersion",
-			in:   "7.0.5",
-		},
-		{
-			name: "ValidMajorValidMinorInvalidUpdateVersion",
-			in:   "7.0U5",
-		},
-		{
-			name: "ValidMajorValidMinorValidPatchInvalidUpdateVersion",
-			in:   "7.0.0U5",
-		},
-	}
-
-	testCases = append(testCases, testCasesForMajorVersion(2, ESXi2000, "2", VMX3)...)
-	testCases = append(testCases, testCasesForMajorVersion(3, ESXi3000, "3", VMX4)...)
-	testCases = append(testCases, testCasesForMajorVersion(4, ESXi4000, "4", VMX7)...)
-	testCases = append(testCases, testCasesForMajorVersion(5, ESXi5000, "5.0", VMX8)...)
-	testCases = append(testCases, testCasesForMajorMinorVersion(5, 1, ESXi5100, "5.1", VMX9)...)
-	testCases = append(testCases, testCasesForMajorMinorVersion(5, 5, ESXi5500, "5.5", VMX10)...)
-	testCases = append(testCases, testCasesForMajorVersion(6, ESXi6000, "6.0", VMX11)...)
-	testCases = append(testCases, testCasesForMajorMinorVersion(6, 5, ESXi6500, "6.5", VMX13)...)
-	testCases = append(testCases, testCasesForMajorMinorVersion(6, 7, ESXi6700, "6.7", VMX14)...)
-	testCases = append(testCases, testCasesForMajorMinorUpdateVersion(6, 7, 2, ESXi6720, "6.7.2", VMX15)...)
-	testCases = append(testCases, testCasesForMajorVersion(7, ESXi7000, "7.0", VMX17)...)
-	testCases = append(testCases, testCasesForMajorMinorUpdateVersion(7, 0, 1, ESXi7010, "7.0.1", VMX18)...)
-	testCases = append(testCases, testCasesForMajorMinorUpdateVersion(7, 0, 2, ESXi7020, "7.0.2", VMX19)...)
-	testCases = append(testCases, testCasesForMajorVersion(8, ESXi8000, "8.0", VMX20)...)
-	testCases = append(testCases, testCasesForMajorMinorUpdateVersion(8, 0, 1, ESXi8010, "8.0.1", VMX20)...)
-	testCases = append(testCases, testCasesForMajorMinorUpdateVersion(8, 0, 2, ESXi8020, "8.0.2", VMX21)...)
-
-	t.Run("GetESXiVersions", func(t *testing.T) {
-		a, e := uniqueExpectedVersions, GetESXiVersions()
-		sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
-		sort.Slice(e, func(i, j int) bool { return e[i] < e[j] })
-		if a, e := len(a), len(e); a != e {
-			t.Errorf("unexpected number of versions: a=%d, e=%d", a, e)
-		}
-		for i := range a {
-			if a[i] != e[i] {
-				t.Errorf("unexpected version: i=%d, a=%s, e=%s", i, a, e)
-			}
-		}
-	})
-
-	t.Run("ParseESXiVersion", func(t *testing.T) {
-		for i := range testCases {
-			tc := testCases[i]
-			t.Run(tc.name, func(t *testing.T) {
-				v, err := ParseESXiVersion(tc.in)
-				if err != nil && tc.expectedIsValid {
-					t.Fatalf("unexpected error: %v", err)
-				}
-				if a, e := v.IsValid(), tc.expectedIsValid; a != e {
-					t.Errorf("unexpected invalid value: a=%v, e=%v", a, e)
-				}
-				if tc.expectedIsValid {
-					if a, e := v, tc.expectedVersion; a != e {
-						t.Errorf("unexpected value: a=%v, e=%v", a, e)
-					}
-					if a, e := v.String(), tc.expectedString; a != e {
-						t.Errorf("unexpected string: a=%v, e=%v", a, e)
-					}
-					if a, e := v.IsHardwareVersionSupported(tc.expectedHardwareVersion), true; a != e {
-						t.Errorf("is hardware version supported failed for %s", tc.expectedHardwareVersion)
-					}
-					if a, e := v.HardwareVersion(), tc.expectedHardwareVersion; a != e {
-						t.Errorf("unexpected hardware version: a=%s, e=%s", a, e)
-					}
-				}
-			})
-		}
-	})
-	t.Run("MarshalText", func(t *testing.T) {
-		for i := range testCases {
-			tc := testCases[i]
-			t.Run(tc.name, func(t *testing.T) {
-				data, err := tc.expectedVersion.MarshalText()
-				if err != nil {
-					t.Fatalf("unexpected error marshaling text: %v", err)
-				}
-				if a, e := string(data), tc.expectedString; a != e {
-					t.Errorf("unexpected data marshaling text: %s", a)
-				}
-			})
-		}
-	})
-
-	t.Run("UnmarshalText", func(t *testing.T) {
-		for i := range testCases {
-			tc := testCases[i]
-			t.Run(tc.name, func(t *testing.T) {
-				var (
-					data = []byte(tc.in)
-					v    ESXiVersion
-				)
-				if err := v.UnmarshalText(data); err != nil {
-					if !tc.expectedIsValid {
-						if a, e := err.Error(), fmt.Sprintf("invalid version: %q", tc.in); a != e {
-							t.Errorf("unexpected error unmarshaling text: %q", a)
-						}
-					} else {
-						t.Errorf("unexpected error unmarshaling text: %v", err)
-					}
-				} else if a, e := v, v; a != e {
-					t.Errorf("unexpected data unmarshaling text: %s", a)
-				}
-			})
-		}
-	})
-}
diff --git a/vim25/types/hardware_version_test.go b/vim25/types/hardware_version_test.go
deleted file mode 100644
index fd7d6911..00000000
--- a/vim25/types/hardware_version_test.go
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
-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 types
-
-import (
-	"fmt"
-	"sort"
-	"strconv"
-	"testing"
-)
-
-func TestParseHardwareVersion(t *testing.T) {
-	testCases := []struct {
-		name            string
-		in              string
-		expectedIsValid bool
-		expectedVersion HardwareVersion
-		expectedString  string
-	}{
-		{
-			name: "EmptyString",
-			in:   "",
-		},
-		{
-			name: "PrefixSansNumber",
-			in:   "vmx-",
-		},
-		{
-			name:            "NumberSansPrefix",
-			in:              "13",
-			expectedIsValid: true,
-			expectedVersion: VMX13,
-			expectedString:  "vmx-13",
-		},
-		{
-			name:            "vmx-13",
-			in:              "vmx-13",
-			expectedIsValid: true,
-			expectedVersion: VMX13,
-			expectedString:  "vmx-13",
-		},
-		{
-			name:            "VMX-18",
-			in:              "VMX-18",
-			expectedIsValid: true,
-			expectedVersion: VMX18,
-			expectedString:  "vmx-18",
-		},
-	}
-
-	t.Run("ParseHardwareVersion", func(t *testing.T) {
-		for i := range testCases {
-			tc := testCases[i]
-			t.Run(tc.name, func(t *testing.T) {
-				v, err := ParseHardwareVersion(tc.in)
-				if err != nil && tc.expectedIsValid {
-					t.Fatalf("unexpected error: %v", err)
-				}
-				if a, e := v.IsValid(), tc.expectedIsValid; a != e {
-					t.Errorf("unexpected invalid value: a=%v, e=%v", a, e)
-				}
-				if v.IsValid() {
-					if a, e := v, tc.expectedVersion; a != e {
-						t.Errorf("unexpected value: a=%v, e=%v", a, e)
-					}
-					if a, e := v.String(), tc.expectedString; a != e {
-						t.Errorf("unexpected string: a=%v, e=%v", a, e)
-					}
-				}
-			})
-		}
-	})
-
-}
-
-func TestHardwareVersion(t *testing.T) {
-
-	var uniqueExpectedVersions []HardwareVersion
-
-	type testCase struct {
-		name            string
-		in              string
-		expectedIsValid bool
-		expectedVersion HardwareVersion
-		expectedString  string
-	}
-
-	testCasesForVersion := func(
-		version int,
-		expectedVersion HardwareVersion,
-		expectedString string) []testCase {
-
-		uniqueExpectedVersions = append(uniqueExpectedVersions, MustParseHardwareVersion(expectedString))
-
-		szVersion := strconv.Itoa(version)
-		return []testCase{
-			{
-				name:            szVersion,
-				in:              szVersion,
-				expectedIsValid: true,
-				expectedVersion: expectedVersion,
-				expectedString:  expectedString,
-			},
-			{
-				name:            "vmx-" + szVersion,
-				in:              "vmx-" + szVersion,
-				expectedIsValid: true,
-				expectedVersion: expectedVersion,
-				expectedString:  expectedString,
-			},
-			{
-				name:            "VMX-" + szVersion,
-				in:              "VMX-" + szVersion,
-				expectedIsValid: true,
-				expectedVersion: expectedVersion,
-				expectedString:  expectedString,
-			},
-		}
-	}
-
-	testCases := []testCase{
-		{
-			name: "EmptyString",
-			in:   "",
-		},
-		{
-			name: "PrefixSansVersion",
-			in:   "vmx-",
-		},
-		{
-			name: "PrefixAndInvalidVersion",
-			in:   "vmx-0",
-		},
-		{
-			name: "InvalidVersion",
-			in:   "1",
-		},
-	}
-
-	testCases = append(testCases, testCasesForVersion(3, VMX3, "vmx-3")...)
-	testCases = append(testCases, testCasesForVersion(4, VMX4, "vmx-4")...)
-	testCases = append(testCases, testCasesForVersion(6, VMX6, "vmx-6")...)
-	testCases = append(testCases, testCasesForVersion(7, VMX7, "vmx-7")...)
-	testCases = append(testCases, testCasesForVersion(8, VMX8, "vmx-8")...)
-	testCases = append(testCases, testCasesForVersion(9, VMX9, "vmx-9")...)
-	testCases = append(testCases, testCasesForVersion(10, VMX10, "vmx-10")...)
-	testCases = append(testCases, testCasesForVersion(11, VMX11, "vmx-11")...)
-	testCases = append(testCases, testCasesForVersion(12, VMX12, "vmx-12")...)
-	testCases = append(testCases, testCasesForVersion(13, VMX13, "vmx-13")...)
-	testCases = append(testCases, testCasesForVersion(14, VMX14, "vmx-14")...)
-	testCases = append(testCases, testCasesForVersion(15, VMX15, "vmx-15")...)
-	testCases = append(testCases, testCasesForVersion(16, VMX16, "vmx-16")...)
-	testCases = append(testCases, testCasesForVersion(17, VMX17, "vmx-17")...)
-	testCases = append(testCases, testCasesForVersion(18, VMX18, "vmx-18")...)
-	testCases = append(testCases, testCasesForVersion(19, VMX19, "vmx-19")...)
-	testCases = append(testCases, testCasesForVersion(20, VMX20, "vmx-20")...)
-	testCases = append(testCases, testCasesForVersion(21, VMX21, "vmx-21")...)
-
-	t.Run("GetHardwareVersions", func(t *testing.T) {
-		a, e := uniqueExpectedVersions, GetHardwareVersions()
-		sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
-		sort.Slice(e, func(i, j int) bool { return e[i] < e[j] })
-		if a, e := len(a), len(e); a != e {
-			t.Errorf("unexpected number of versions: a=%d, e=%d", a, e)
-		}
-		for i := range a {
-			if a[i] != e[i] {
-				t.Errorf("unexpected version: i=%d, a=%s, e=%s", i, a, e)
-			}
-		}
-	})
-
-	t.Run("ParseHardwareVersion", func(t *testing.T) {
-		for i := range testCases {
-			tc := testCases[i]
-			t.Run(tc.name, func(t *testing.T) {
-				v, err := ParseHardwareVersion(tc.in)
-				if err != nil && tc.expectedIsValid {
-					t.Fatalf("unexpected error: %v", err)
-				}
-				if a, e := v.IsValid(), tc.expectedIsValid; a != e {
-					t.Errorf("unexpected invalid value: a=%v, e=%v", a, e)
-				}
-				if tc.expectedIsValid {
-					if a, e := v, tc.expectedVersion; a != e {
-						t.Errorf("unexpected value: a=%v, e=%v", a, e)
-					}
-					if a, e := v.String(), tc.expectedString; a != e {
-						t.Errorf("unexpected string: a=%v, e=%v", a, e)
-					}
-				}
-			})
-		}
-	})
-	t.Run("MarshalText", func(t *testing.T) {
-		for i := range testCases {
-			tc := testCases[i]
-			t.Run(tc.name, func(t *testing.T) {
-				data, err := tc.expectedVersion.MarshalText()
-				if err != nil {
-					t.Fatalf("unexpected error marshaling text: %v", err)
-				}
-				if a, e := string(data), tc.expectedString; a != e {
-					t.Errorf("unexpected data marshaling text: %s", a)
-				}
-			})
-		}
-	})
-
-	t.Run("UnmarshalText", func(t *testing.T) {
-		for i := range testCases {
-			tc := testCases[i]
-			t.Run(tc.name, func(t *testing.T) {
-				var (
-					data = []byte(tc.in)
-					v    HardwareVersion
-				)
-				if err := v.UnmarshalText(data); err != nil {
-					if !tc.expectedIsValid {
-						if a, e := err.Error(), fmt.Sprintf("invalid version: %q", tc.in); a != e {
-							t.Errorf("unexpected error unmarshaling text: %q", a)
-						}
-					} else {
-						t.Errorf("unexpected error unmarshaling text: %v", err)
-					}
-				} else if a, e := v, v; a != e {
-					t.Errorf("unexpected data unmarshaling text: %s", a)
-				}
-			})
-		}
-	})
-}