Repository: vmware/govmomi
Pull request #3229 (2023-09-12 15:00:49)

Add support for VM data sets
## Description

Implement support for the [VM data sets API](https://developer.vmware.com/apis/vsphere-automation/latest/vcenter/vm/data_sets/) introduced in vSphere 8.0. 

[Data sets](https://core.vmware.com/resource/vsphere-datasets) provide a mechanism to share data between the vSphere management layer (i.e. vCenter and ESX) and the VM guest OS. See the [VMware Guest SDK Programming Guide](https://developer.vmware.com/docs/16962/vmware-guest-sdk-programming-guide/GUID-AEC8BE71-FA22-4A87-8C6F-F4EBE95AA8DE.html) for details on using data sets from within the guest OS of a VM.

### Guest support for data sets
Using data sets from within a VM guest OS seems to be out of scope for `govmomi` at the moment:
`govmomi` has a guest `toolbox` sub-project but it is an alternative to the VMware tools daemon. `govmomi` does not have an alternative to the VMware tools CLI (i.e. `vmtoolsd --cmd`), nor to the VMware [Guest SDK](https://developer.vmware.com/docs/16962/vmware-guest-sdk-programming-guide/GUID-AEC8BE71-FA22-4A87-8C6F-F4EBE95AA8DE.html).
Data sets are stored on the ESXi host. The data sets feature does not need any collaboration by the VMware tools daemon to function, so there is no need to implement anything in the guest `toolbox`.
Currently data sets can be accessed from within the VM guest OS via:
- the CLI of VMware tools, i.e. `vmtoolsd --cmd`, as described in the [DataSets blog](https://core.vmware.com/resource/vsphere-datasets)
- the VMware [Guest SDK](https://developer.vmware.com/docs/16962/vmware-guest-sdk-programming-guide/GUID-AEC8BE71-FA22-4A87-8C6F-F4EBE95AA8DE.html)

### Implementation of data sets API in vcsim
Store data sets as a field in `simulator.VirtualMachine`, because a data set can be copied during a VM clone or snapshot (see `dataset.CreateSpec.OmitFromSnapshotAndClone`). Clone and snapshot are implemented in `simulator.VirtualMachine`, so it seems more natural to keep data sets in `simulator.VirtualMachine` and access them from the code in the vapi simulator.

Extract the simulation code for REST APIs under `/api/vcenter/vm` into its own sub-simulator:
Data set request paths look like this: `/api/vcenter/vm/{moid}/data-sets/{id}`. Currently vcsim routes API requests via the standard [http.ServeMux](https://pkg.go.dev/net/http#ServeMux). However, [http.ServeMux](https://pkg.go.dev/net/http#ServeMux) does not support placeholders in the middle of the URL path (e.g. VM moid, data set id). So created a separate sub-simulator for everything under `/api/vcenter/vm`. At the moment it contains only delete VM (see https://github.com/vmware/govmomi/pull/1770) and the data sets API.

Inside the new sub-simulator for `/api/vcenter/vm`, API requests are routed via analyzing the URL path segment by segment:
As mentioned above, [http.ServeMux](https://pkg.go.dev/net/http#ServeMux) cannot handle path placeholders. Here I have done routing in code. An alternative would be to use a third-party library for request routing like [go-chi](https://github.com/go-chi/chi) or even a home-grown regex table or a radix tree. Potentially the main vapi simulator could allow sub-simulators to register for paths with placeholders. See also https://github.com/vmware/govmomi/blob/e7aac6ad473846f6a1f5bf10c524ac5a8f93022d/vapi/simulator/simulator.go#L2084

In order to get a lock on a `simulator.VirtualMachine` in the vapi simulator, had to create a `simulator.Context` similar to [SpoofContext](https://github.com/vmware/govmomi/blob/1eeb6f1c33995d1015106ea17498615ba7ad01b4/simulator/model.go#L395) but using the `simulator.Registry` received during registration.

Closes: #3050

## Type of change

- [ ] 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?

- [x] Added `bats` tests which exercise the data sets functionality in `vcsim` and `govc`.
- [x] Developer testing of data sets support in `govc` against a real vCenter.
- [x] Developer testing of data sets support in `vcsim` via curl.

## 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/govc/USAGE.md b/govc/USAGE.md
index c04ee030..e9e07cca 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -339,6 +339,15 @@ but appear via `govc $cmd -h`:
  - [vm.console](#vmconsole)
  - [vm.create](#vmcreate)
  - [vm.customize](#vmcustomize)
+ - [vm.dataset.create](#vmdatasetcreate)
+ - [vm.dataset.entry.get](#vmdatasetentryget)
+ - [vm.dataset.entry.ls](#vmdatasetentryls)
+ - [vm.dataset.entry.rm](#vmdatasetentryrm)
+ - [vm.dataset.entry.set](#vmdatasetentryset)
+ - [vm.dataset.info](#vmdatasetinfo)
+ - [vm.dataset.ls](#vmdatasetls)
+ - [vm.dataset.rm](#vmdatasetrm)
+ - [vm.dataset.update](#vmdatasetupdate)
  - [vm.destroy](#vmdestroy)
  - [vm.disk.attach](#vmdiskattach)
  - [vm.disk.change](#vmdiskchange)
@@ -5853,6 +5862,154 @@ Options:
   -vm=                   Virtual machine [GOVC_VM]
 ```
 
+## vm.dataset.create
+
+```
+Usage: govc vm.dataset.create [OPTIONS] NAME
+
+Create data set on a VM.
+
+This command will output the ID of the new data set.
+
+Examples:
+  govc vm.dataset.create -vm $vm -d "Data set for project 2" -host-access READ_WRITE -guest-access READ_ONLY com.example.project2
+  govc vm.dataset.create -vm $vm -d "Data set for project 3" -omit-from-snapshot=false com.example.project3
+
+Options:
+  -d=                        Description
+  -guest-access=READ_WRITE   Access to the data set entries from the VM guest OS (NONE|READ_ONLY|READ_WRITE)
+  -host-access=READ_WRITE    Access to the data set entries from the ESXi host and the vCenter (NONE|READ_ONLY|READ_WRITE)
+  -omit-from-snapshot=<nil>  Omit the data set from snapshots and clones of the VM (defaults to false)
+  -vm=                       Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.entry.get
+
+```
+Usage: govc vm.dataset.entry.get [OPTIONS] KEY
+
+Read the value of a data set entry.
+
+Examples:
+  govc vm.dataset.entry.get -vm $vm -dataset com.example.project2 somekey
+
+Options:
+  -dataset=              Data set name or ID
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.entry.ls
+
+```
+Usage: govc vm.dataset.entry.ls [OPTIONS]
+
+List the keys of the entries in a data set.
+
+Examples:
+  govc vm.dataset.entry.ls -vm $vm -dataset com.example.project2
+
+Options:
+  -dataset=              Data set name or ID
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.entry.rm
+
+```
+Usage: govc vm.dataset.entry.rm [OPTIONS] KEY
+
+Delete data set entry.
+
+Examples:
+  govc vm.dataset.entry.rm -vm $vm -dataset com.example.project2 somekey
+
+Options:
+  -dataset=              Data set name or ID
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.entry.set
+
+```
+Usage: govc vm.dataset.entry.set [OPTIONS] KEY VALUE
+
+Set the value of a data set entry.
+
+Examples:
+  govc vm.dataset.entry.set -vm $vm -dataset com.example.project2 somekey somevalue
+
+Options:
+  -dataset=              Data set name or ID
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.info
+
+```
+Usage: govc vm.dataset.info [OPTIONS] NAME
+
+Display data set information.
+
+Examples:
+  govc vm.dataset.info -vm $vm
+  govc vm.dataset.info -vm $vm com.example.project2
+
+Options:
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.ls
+
+```
+Usage: govc vm.dataset.ls [OPTIONS]
+
+List datasets.
+
+Examples:
+  govc vm.dataset.ls -vm $vm
+  govc vm.dataset.ls -vm $vm -json | jq '.[].description'
+
+Options:
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.rm
+
+```
+Usage: govc vm.dataset.rm [OPTIONS] NAME
+
+Delete data set.
+
+Fails if the data set has entries, unless the '-force' flag is set.
+
+Examples:
+  govc vm.dataset.rm -vm $vm com.example.project2
+  govc vm.dataset.rm -vm $vm -force=true com.example.project3
+
+Options:
+  -force=false           Delete the data set even if it has entries
+  -vm=                   Virtual machine [GOVC_VM]
+```
+
+## vm.dataset.update
+
+```
+Usage: govc vm.dataset.update [OPTIONS] NAME
+
+Update data set.
+	
+Examples:
+  govc vm.dataset.update -vm $vm -d "New description." -guest-access READ_ONLY com.example.project2
+  govc vm.dataset.update -vm $vm -omit-from-snapshot=false com.example.project3
+
+Options:
+  -d=                        Description
+  -guest-access=             Access to the data set entries from the VM guest OS (NONE|READ_ONLY|READ_WRITE)
+  -host-access=              Access to the data set entries from the ESXi host and the vCenter (NONE|READ_ONLY|READ_WRITE)
+  -omit-from-snapshot=<nil>  Omit the data set from snapshots and clones of the VM
+  -vm=                       Virtual machine [GOVC_VM]
+```
+
 ## vm.destroy
 
 ```
diff --git a/govc/main.go b/govc/main.go
index 3a0ce55a..b01f8439 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -104,6 +104,8 @@ import (
 	_ "github.com/vmware/govmomi/govc/vcsa/shutdown"
 	_ "github.com/vmware/govmomi/govc/version"
 	_ "github.com/vmware/govmomi/govc/vm"
+	_ "github.com/vmware/govmomi/govc/vm/dataset"
+	_ "github.com/vmware/govmomi/govc/vm/dataset/entry"
 	_ "github.com/vmware/govmomi/govc/vm/disk"
 	_ "github.com/vmware/govmomi/govc/vm/guest"
 	_ "github.com/vmware/govmomi/govc/vm/network"
diff --git a/govc/vm/dataset/create.go b/govc/vm/dataset/create.go
new file mode 100755
index 00000000..df3899d2
--- /dev/null
+++ b/govc/vm/dataset/create.go
@@ -0,0 +1,123 @@
+/*
+Copyright (c) 2023-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.
+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 dataset
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"strings"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+var accessModes = []string{
+	string(dataset.AccessNone),
+	string(dataset.AccessReadOnly),
+	string(dataset.AccessReadWrite),
+}
+
+func hostAccessUsage() string {
+	return fmt.Sprintf("Access to the data set entries from the ESXi host and the vCenter (%s)", strings.Join(accessModes, "|"))
+}
+
+func guestAccessUsage() string {
+	return fmt.Sprintf("Access to the data set entries from the VM guest OS (%s)", strings.Join(accessModes, "|"))
+}
+
+func validateDataSetAccess(access dataset.Access) bool {
+	for _, validAccess := range accessModes {
+		if string(access) == validAccess {
+			return true
+		}
+	}
+	return false
+}
+
+type create struct {
+	*flags.VirtualMachineFlag
+	spec dataset.CreateSpec
+}
+
+func init() {
+	cli.Register("vm.dataset.create", &create{})
+}
+
+func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	f.StringVar(&cmd.spec.Description, "d", "", "Description")
+	f.StringVar((*string)(&cmd.spec.Host), "host-access", string(dataset.AccessReadWrite), hostAccessUsage())
+	f.StringVar((*string)(&cmd.spec.Guest), "guest-access", string(dataset.AccessReadWrite), guestAccessUsage())
+	f.Var(flags.NewOptionalBool(&cmd.spec.OmitFromSnapshotAndClone), "omit-from-snapshot", "Omit the data set from snapshots and clones of the VM (defaults to false)")
+}
+
+func (cmd *create) Process(ctx context.Context) error {
+	return cmd.VirtualMachineFlag.Process(ctx)
+}
+
+func (cmd *create) Usage() string {
+	return "NAME"
+}
+
+func (cmd *create) Description() string {
+	return `Create data set on a VM.
+
+This command will output the ID of the new data set.
+
+Examples:
+  govc vm.dataset.create -vm $vm -d "Data set for project 2" -host-access READ_WRITE -guest-access READ_ONLY com.example.project2
+  govc vm.dataset.create -vm $vm -d "Data set for project 3" -omit-from-snapshot=false com.example.project3`
+}
+
+func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	cmd.spec.Name = f.Arg(0)
+	if !validateDataSetAccess(cmd.spec.Host) {
+		return errors.New("please specify valid host access")
+	}
+	if !validateDataSetAccess(cmd.spec.Guest) {
+		return errors.New("please specify valid guest access")
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+	id, err := mgr.CreateDataSet(ctx, vmId, &cmd.spec)
+	if err != nil {
+		return err
+	}
+	fmt.Println(id)
+	return nil
+}
diff --git a/govc/vm/dataset/entry/get.go b/govc/vm/dataset/entry/get.go
new file mode 100755
index 00000000..a60a3ee6
--- /dev/null
+++ b/govc/vm/dataset/entry/get.go
@@ -0,0 +1,95 @@
+/*
+Copyright (c) 2023-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.
+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 entry
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	govc "github.com/vmware/govmomi/govc/vm/dataset"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type get struct {
+	*flags.VirtualMachineFlag
+	dataSet string
+}
+
+func init() {
+	cli.Register("vm.dataset.entry.get", &get{})
+}
+
+func (cmd *get) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	f.StringVar(&cmd.dataSet, "dataset", "", "Data set name or ID")
+}
+
+func (cmd *get) Process(ctx context.Context) error {
+	return cmd.VirtualMachineFlag.Process(ctx)
+}
+
+func (cmd *get) Usage() string {
+	return "KEY"
+}
+
+func (cmd *get) Description() string {
+	return `Read the value of a data set entry.
+
+Examples:
+  govc vm.dataset.entry.get -vm $vm -dataset com.example.project2 somekey`
+}
+
+func (cmd *get) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+	entryKey := f.Arg(0)
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	if cmd.dataSet == "" {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+	dataSetId, err := govc.FindDataSetId(ctx, mgr, vmId, cmd.dataSet)
+	if err != nil {
+		return err
+	}
+	entryValue, err := mgr.GetEntry(ctx, vmId, dataSetId, entryKey)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(entryValue)
+	return nil
+}
diff --git a/govc/vm/dataset/entry/ls.go b/govc/vm/dataset/entry/ls.go
new file mode 100755
index 00000000..52832a24
--- /dev/null
+++ b/govc/vm/dataset/entry/ls.go
@@ -0,0 +1,101 @@
+/*
+Copyright (c) 2023-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.
+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 entry
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	govc "github.com/vmware/govmomi/govc/vm/dataset"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type ls struct {
+	*flags.VirtualMachineFlag
+	*flags.OutputFlag
+	dataSet string
+}
+
+func init() {
+	cli.Register("vm.dataset.entry.ls", &ls{})
+}
+
+func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	f.StringVar(&cmd.dataSet, "dataset", "", "Data set name or ID")
+}
+
+func (cmd *ls) Process(ctx context.Context) error {
+	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
+		return err
+	}
+	return cmd.OutputFlag.Process(ctx)
+}
+
+func (cmd *ls) Description() string {
+	return `List the keys of the entries in a data set.
+
+Examples:
+  govc vm.dataset.entry.ls -vm $vm -dataset com.example.project2`
+}
+
+type lsResult []string
+
+func (r lsResult) Write(w io.Writer) error {
+	for _, key := range r {
+		fmt.Fprintln(w, key)
+	}
+	return nil
+}
+
+func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error {
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	if cmd.dataSet == "" {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+	dataSetId, err := govc.FindDataSetId(ctx, mgr, vmId, cmd.dataSet)
+	if err != nil {
+		return err
+	}
+	l, err := mgr.ListEntries(ctx, vmId, dataSetId)
+	if err != nil {
+		return err
+	}
+	return cmd.WriteResult(lsResult(l))
+}
diff --git a/govc/vm/dataset/entry/rm.go b/govc/vm/dataset/entry/rm.go
new file mode 100755
index 00000000..33472e72
--- /dev/null
+++ b/govc/vm/dataset/entry/rm.go
@@ -0,0 +1,88 @@
+/*
+Copyright (c) 2023-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.
+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 entry
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	govc "github.com/vmware/govmomi/govc/vm/dataset"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type rm struct {
+	*flags.VirtualMachineFlag
+	dataSet string
+}
+
+func init() {
+	cli.Register("vm.dataset.entry.rm", &rm{})
+}
+
+func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	f.StringVar(&cmd.dataSet, "dataset", "", "Data set name or ID")
+}
+
+func (cmd *rm) Process(ctx context.Context) error {
+	return cmd.VirtualMachineFlag.Process(ctx)
+}
+
+func (cmd *rm) Usage() string {
+	return "KEY"
+}
+
+func (cmd *rm) Description() string {
+	return `Delete data set entry.
+
+Examples:
+  govc vm.dataset.entry.rm -vm $vm -dataset com.example.project2 somekey`
+}
+
+func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+	entryKey := f.Arg(0)
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	if cmd.dataSet == "" {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+	dataSetId, err := govc.FindDataSetId(ctx, mgr, vmId, cmd.dataSet)
+	if err != nil {
+		return err
+	}
+	return mgr.DeleteEntry(ctx, vmId, dataSetId, entryKey)
+}
diff --git a/govc/vm/dataset/entry/set.go b/govc/vm/dataset/entry/set.go
new file mode 100755
index 00000000..e44e5e84
--- /dev/null
+++ b/govc/vm/dataset/entry/set.go
@@ -0,0 +1,89 @@
+/*
+Copyright (c) 2023-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.
+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 entry
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	govc "github.com/vmware/govmomi/govc/vm/dataset"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type set struct {
+	*flags.VirtualMachineFlag
+	dataSet string
+}
+
+func init() {
+	cli.Register("vm.dataset.entry.set", &set{})
+}
+
+func (cmd *set) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	f.StringVar(&cmd.dataSet, "dataset", "", "Data set name or ID")
+}
+
+func (cmd *set) Process(ctx context.Context) error {
+	return cmd.VirtualMachineFlag.Process(ctx)
+}
+
+func (cmd *set) Usage() string {
+	return "KEY VALUE"
+}
+
+func (cmd *set) Description() string {
+	return `Set the value of a data set entry.
+
+Examples:
+  govc vm.dataset.entry.set -vm $vm -dataset com.example.project2 somekey somevalue`
+}
+
+func (cmd *set) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 2 {
+		return flag.ErrHelp
+	}
+	entryKey := f.Arg(0)
+	entryValue := f.Arg(1)
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	if cmd.dataSet == "" {
+		return flag.ErrHelp
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+	dataSetId, err := govc.FindDataSetId(ctx, mgr, vmId, cmd.dataSet)
+	if err != nil {
+		return err
+	}
+	return mgr.SetEntry(ctx, vmId, dataSetId, entryKey, entryValue)
+}
diff --git a/govc/vm/dataset/info.go b/govc/vm/dataset/info.go
new file mode 100755
index 00000000..8dec269a
--- /dev/null
+++ b/govc/vm/dataset/info.go
@@ -0,0 +1,136 @@
+/*
+Copyright (c) 2023-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.
+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 dataset
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"text/tabwriter"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type info struct {
+	*flags.VirtualMachineFlag
+	*flags.OutputFlag
+}
+
+func init() {
+	cli.Register("vm.dataset.info", &info{})
+}
+
+func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+}
+
+func (cmd *info) Process(ctx context.Context) error {
+	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
+		return err
+	}
+	return cmd.OutputFlag.Process(ctx)
+}
+
+func (cmd *info) Usage() string {
+	return "NAME"
+}
+
+func (cmd *info) Description() string {
+	return `Display data set information.
+
+Examples:
+  govc vm.dataset.info -vm $vm
+  govc vm.dataset.info -vm $vm com.example.project2`
+}
+
+type infoResult []*dataset.Info
+
+func (r infoResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	for _, item := range r {
+		fmt.Fprintf(tw, "Name:\t%s\n", item.Name)
+		fmt.Fprintf(tw, "  Description:\t%s\n", item.Description)
+		fmt.Fprintf(tw, "  Host:\t%s\n", item.Host)
+		fmt.Fprintf(tw, "  Guest:\t%s\n", item.Guest)
+		fmt.Fprintf(tw, "  Used: \t%d\n", item.Used)
+		fmt.Fprintf(tw, "  OmitFromSnapshotAndClone: \t%t\n", item.OmitFromSnapshotAndClone)
+	}
+
+	return tw.Flush()
+}
+
+func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() > 1 {
+		return errors.New("please specify at most one data set")
+	}
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	mgr := dataset.NewManager(c)
+
+	// Discover the relevant data set id(s)
+	var ids []string
+	if f.NArg() == 1 {
+		// single data set
+		id, err := FindDataSetId(ctx, mgr, vmId, f.Arg(0))
+		if err != nil {
+			return err
+		}
+		ids = []string{id}
+	} else {
+		// all data sets
+		l, err := mgr.ListDataSets(ctx, vmId)
+		if err != nil {
+			return err
+		}
+		for _, summary := range l {
+			ids = append(ids, summary.DataSet)
+		}
+	}
+
+	// Fetch the information for each data set id
+	var res []*dataset.Info
+	for _, id := range ids {
+		inf, err := mgr.GetDataSet(ctx, vmId, id)
+		if err != nil {
+			return err
+		}
+		res = append(res, inf)
+	}
+
+	return cmd.WriteResult(infoResult(res))
+}
diff --git a/govc/vm/dataset/ls.go b/govc/vm/dataset/ls.go
new file mode 100755
index 00000000..af6751f0
--- /dev/null
+++ b/govc/vm/dataset/ls.go
@@ -0,0 +1,90 @@
+/*
+Copyright (c) 2023-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.
+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 dataset
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type ls struct {
+	*flags.VirtualMachineFlag
+	*flags.OutputFlag
+}
+
+func init() {
+	cli.Register("vm.dataset.ls", &ls{})
+}
+
+func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+}
+
+func (cmd *ls) Process(ctx context.Context) error {
+	if err := cmd.VirtualMachineFlag.Process(ctx); err != nil {
+		return err
+	}
+	return cmd.OutputFlag.Process(ctx)
+}
+
+func (cmd *ls) Description() string {
+	return `List datasets.
+
+Examples:
+  govc vm.dataset.ls -vm $vm
+  govc vm.dataset.ls -vm $vm -json | jq '.[].description'`
+}
+
+type lsResult []dataset.Summary
+
+func (r lsResult) Write(w io.Writer) error {
+	for _, d := range r {
+		fmt.Fprintln(w, d.Name)
+	}
+	return nil
+}
+
+func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error {
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+	l, err := mgr.ListDataSets(ctx, vmId)
+	if err != nil {
+		return err
+	}
+	return cmd.WriteResult(lsResult(l))
+}
diff --git a/govc/vm/dataset/rm.go b/govc/vm/dataset/rm.go
new file mode 100755
index 00000000..2744e5b2
--- /dev/null
+++ b/govc/vm/dataset/rm.go
@@ -0,0 +1,92 @@
+/*
+Copyright (c) 2023-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.
+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 dataset
+
+import (
+	"context"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type rm struct {
+	*flags.VirtualMachineFlag
+	force bool
+}
+
+func init() {
+	cli.Register("vm.dataset.rm", &rm{})
+}
+
+func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	f.BoolVar(&cmd.force, "force", false, "Delete the data set even if it has entries")
+}
+
+func (cmd *rm) Process(ctx context.Context) error {
+	return cmd.VirtualMachineFlag.Process(ctx)
+}
+
+func (cmd *rm) Usage() string {
+	return "NAME"
+}
+
+func (cmd *rm) Description() string {
+	return `Delete data set.
+
+Fails if the data set has entries, unless the '-force' flag is set.
+
+Examples:
+  govc vm.dataset.rm -vm $vm com.example.project2
+  govc vm.dataset.rm -vm $vm -force=true com.example.project3`
+}
+
+func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+
+	id, err := FindDataSetId(ctx, mgr, vmId, f.Arg(0))
+	if err != nil {
+		return err
+	}
+
+	err = mgr.DeleteDataSet(ctx, vmId, id, cmd.force)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/govc/vm/dataset/update.go b/govc/vm/dataset/update.go
new file mode 100755
index 00000000..29897822
--- /dev/null
+++ b/govc/vm/dataset/update.go
@@ -0,0 +1,133 @@
+/*
+Copyright (c) 2023-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.
+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 dataset
+
+import (
+	"context"
+	"errors"
+	"flag"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type update struct {
+	*flags.VirtualMachineFlag
+
+	description              string
+	host                     dataset.Access
+	guest                    dataset.Access
+	omitFromSnapshotAndClone *bool
+}
+
+func init() {
+	cli.Register("vm.dataset.update", &update{})
+}
+
+func FindDataSetId(ctx context.Context, mgr *dataset.Manager, vmId string, nameOrId string) (string, error) {
+	l, err := mgr.ListDataSets(ctx, vmId)
+	if err != nil {
+		return "", err
+	}
+	for _, summary := range l {
+		if nameOrId == summary.DataSet || nameOrId == summary.Name {
+			return summary.DataSet, nil
+		}
+	}
+	return nameOrId, nil
+}
+
+func (cmd *update) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx)
+	cmd.VirtualMachineFlag.Register(ctx, f)
+	f.StringVar(&cmd.description, "d", "", "Description")
+	f.StringVar((*string)(&cmd.host), "host-access", "", hostAccessUsage())
+	f.StringVar((*string)(&cmd.guest), "guest-access", "", guestAccessUsage())
+	f.Var(flags.NewOptionalBool(&cmd.omitFromSnapshotAndClone), "omit-from-snapshot", "Omit the data set from snapshots and clones of the VM")
+}
+
+func (cmd *update) Process(ctx context.Context) error {
+	return cmd.VirtualMachineFlag.Process(ctx)
+}
+
+func (cmd *update) Usage() string {
+	return "NAME"
+}
+
+func (cmd *update) Description() string {
+	return `Update data set.
+	
+Examples:
+  govc vm.dataset.update -vm $vm -d "New description." -guest-access READ_ONLY com.example.project2
+  govc vm.dataset.update -vm $vm -omit-from-snapshot=false com.example.project3`
+}
+
+func (cmd *update) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() != 1 {
+		return flag.ErrHelp
+	}
+
+	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
+	if err != nil {
+		return err
+	}
+	if vm == nil {
+		return flag.ErrHelp
+	}
+	vmId := vm.Reference().Value
+
+	if cmd.host != "" && !validateDataSetAccess(cmd.host) {
+		return errors.New("please specify valid host access")
+	}
+	if cmd.guest != "" && !validateDataSetAccess(cmd.guest) {
+		return errors.New("please specify valid guest access")
+	}
+
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+	mgr := dataset.NewManager(c)
+
+	id, err := FindDataSetId(ctx, mgr, vmId, f.Arg(0))
+	if err != nil {
+		return err
+	}
+
+	// Update only the fields which the user asked for
+	updateSpec := dataset.UpdateSpec{}
+	if cmd.description != "" {
+		updateSpec.Description = &cmd.description
+	}
+	if cmd.host != "" {
+		updateSpec.Host = &cmd.host
+	}
+	if cmd.guest != "" {
+		updateSpec.Guest = &cmd.guest
+	}
+	if cmd.omitFromSnapshotAndClone != nil {
+		updateSpec.OmitFromSnapshotAndClone = cmd.omitFromSnapshotAndClone
+	}
+
+	err = mgr.UpdateDataSet(ctx, vmId, id, &updateSpec)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/simulator/dataset.go b/simulator/dataset.go
new file mode 100644
index 00000000..0a1b0627
--- /dev/null
+++ b/simulator/dataset.go
@@ -0,0 +1,65 @@
+/*
+Copyright (c) 2023-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.
+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 (
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+)
+
+type DataSet struct {
+	*dataset.Info
+	ID      string
+	Entries map[string]string
+}
+
+func copyDataSetsForVmClone(src map[string]*DataSet) map[string]*DataSet {
+	copy := make(map[string]*DataSet, len(src))
+	for k, v := range src {
+		if v.OmitFromSnapshotAndClone {
+			continue
+		}
+		copy[k] = copyDataSet(v)
+	}
+	return copy
+}
+
+func copyDataSet(src *DataSet) *DataSet {
+	if src == nil {
+		return nil
+	}
+	copy := &DataSet{
+		Info: &dataset.Info{
+			Name:                     src.Name,
+			Description:              src.Description,
+			Host:                     src.Host,
+			Guest:                    src.Guest,
+			Used:                     src.Used,
+			OmitFromSnapshotAndClone: src.OmitFromSnapshotAndClone,
+		},
+		ID:      src.ID,
+		Entries: copyEntries(src.Entries),
+	}
+	return copy
+}
+
+func copyEntries(src map[string]string) map[string]string {
+	copy := make(map[string]string, len(src))
+	for k, v := range src {
+		copy[k] = v
+	}
+	return copy
+}
diff --git a/simulator/snapshot.go b/simulator/snapshot.go
index bb1b66f6..55d43658 100644
--- a/simulator/snapshot.go
+++ b/simulator/snapshot.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2017 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.
@@ -30,6 +30,7 @@ import (
 
 type VirtualMachineSnapshot struct {
 	mo.VirtualMachineSnapshot
+	DataSets map[string]*DataSet
 }
 
 func (v *VirtualMachineSnapshot) createSnapshotFiles() types.BaseMethodFault {
@@ -158,6 +159,7 @@ func (v *VirtualMachineSnapshot) RevertToSnapshotTask(ctx *Context, req *types.R
 		vm := ctx.Map.Get(v.Vm).(*VirtualMachine)
 
 		ctx.WithLock(vm, func() {
+			vm.DataSets = copyDataSetsForVmClone(v.DataSets)
 			ctx.Map.Update(vm, []types.PropertyChange{
 				{Name: "snapshot.currentSnapshot", Val: v.Self},
 			})
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index d6671edf..fb6f6f1f 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -43,6 +43,7 @@ import (
 
 type VirtualMachine struct {
 	mo.VirtualMachine
+	DataSets map[string]*DataSet
 
 	log string
 	sid int32
@@ -165,6 +166,7 @@ func NewVirtualMachine(ctx *Context, parent types.ManagedObjectReference, spec *
 	vm.Summary.QuickStats.GuestHeartbeatStatus = types.ManagedEntityStatusGray
 	vm.Summary.OverallStatus = types.ManagedEntityStatusGreen
 	vm.ConfigStatus = types.ManagedEntityStatusGreen
+	vm.DataSets = make(map[string]*DataSet)
 
 	// put vm in the folder only if no errors occurred
 	f, _ := asFolderMO(folder)
@@ -2011,6 +2013,7 @@ func (vm *VirtualMachine) CloneVMTask(ctx *Context, req *types.CloneVM_Task) soa
 		}
 		config := types.VirtualMachineConfigSpec{
 			Name:    req.Name,
+			Version: vm.Config.Version,
 			GuestId: vm.Config.GuestId,
 			Files: &types.VirtualMachineFileInfo{
 				VmPathName: vmx.String(),
@@ -2075,6 +2078,7 @@ func (vm *VirtualMachine) CloneVMTask(ctx *Context, req *types.CloneVM_Task) soa
 		if req.Spec.Config != nil && req.Spec.Config.DeviceChange != nil {
 			clone.configureDevices(ctx, &types.VirtualMachineConfigSpec{DeviceChange: req.Spec.Config.DeviceChange})
 		}
+		clone.DataSets = copyDataSetsForVmClone(vm.DataSets)
 
 		if req.Spec.Template {
 			_ = clone.MarkAsTemplate(&types.MarkAsTemplate{This: clone.Self})
@@ -2280,6 +2284,7 @@ func (vm *VirtualMachine) CreateSnapshotTask(ctx *Context, req *types.CreateSnap
 		snapshot := &VirtualMachineSnapshot{}
 		snapshot.Vm = vm.Reference()
 		snapshot.Config = *vm.Config
+		snapshot.DataSets = copyDataSetsForVmClone(vm.DataSets)
 
 		ctx.Map.Put(snapshot)
 
@@ -2337,8 +2342,10 @@ func (vm *VirtualMachine) RevertToCurrentSnapshotTask(ctx *Context, req *types.R
 
 		return body
 	}
+	snapshot := ctx.Map.Get(*vm.Snapshot.CurrentSnapshot).(*VirtualMachineSnapshot)
 
 	task := CreateTask(vm, "revertSnapshot", func(t *Task) (types.AnyType, types.BaseMethodFault) {
+		vm.DataSets = copyDataSetsForVmClone(snapshot.DataSets)
 		return nil, nil
 	})
 
diff --git a/vapi/internal/internal.go b/vapi/internal/internal.go
old mode 100644
new mode 100755
index d4c9b210..2872f380
--- a/vapi/internal/internal.go
+++ b/vapi/internal/internal.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2018-2022 VMware, Inc. All Rights Reserved.
+Copyright (c) 2018-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.
@@ -43,7 +43,6 @@ const (
 	TrustedCertificatesPath        = "/api/content/trusted-certificates"
 	VCenterOVFLibraryItem          = "/com/vmware/vcenter/ovf/library-item"
 	VCenterVMTXLibraryItem         = "/vcenter/vm-template/library-items"
-	VCenterVM                      = "/vcenter/vm"
 	SessionCookieName              = "vmware-api-session-id"
 	UseHeaderAuthn                 = "vmware-use-header-authn"
 	DebugEcho                      = "/vc-sim/debug/echo"
diff --git a/vapi/simulator/simulator.go b/vapi/simulator/simulator.go
old mode 100644
new mode 100755
index 412d57cc..4cfdcaf9
--- a/vapi/simulator/simulator.go
+++ b/vapi/simulator/simulator.go
@@ -168,7 +168,6 @@ func New(u *url.URL, r *simulator.Registry) ([]string, http.Handler) {
 		{internal.VCenterOVFLibraryItem + "/", s.libraryItemOVFID},
 		{internal.VCenterVMTXLibraryItem, s.libraryItemCreateTemplate},
 		{internal.VCenterVMTXLibraryItem + "/", s.libraryItemTemplateID},
-		{internal.VCenterVM + "/", s.vmID},
 		{internal.DebugEcho, s.debugEcho},
 		// /api/ patterns.
 		{internal.SecurityPoliciesPath, s.librarySecurityPolicies},
@@ -325,6 +324,7 @@ func (s *handler) DetachTag(id vim.ManagedObjectReference, tag vim.VslmTagEntry)
 // StatusOK responds with http.StatusOK and encodes val, if specified, to JSON
 // For use with "/api" endpoints.
 func StatusOK(w http.ResponseWriter, val ...interface{}) {
+	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusOK)
 	if len(val) == 0 {
 		return
@@ -373,6 +373,60 @@ func BadRequest(w http.ResponseWriter, kind string) {
 	}
 }
 
+// ApiErrorAlreadyExists responds with a REST error of type "ALREADY_EXISTS".
+// For use with "/api" endpoints.
+func ApiErrorAlreadyExists(w http.ResponseWriter) {
+	apiError(w, http.StatusBadRequest, "ALREADY_EXISTS")
+}
+
+// ApiErrorGeneral responds with a REST error of type "ERROR".
+// For use with "/api" endpoints.
+func ApiErrorGeneral(w http.ResponseWriter) {
+	apiError(w, http.StatusInternalServerError, "ERROR")
+}
+
+// ApiErrorInvalidArgument responds with a REST error of type "INVALID_ARGUMENT".
+// For use with "/api" endpoints.
+func ApiErrorInvalidArgument(w http.ResponseWriter) {
+	apiError(w, http.StatusBadRequest, "INVALID_ARGUMENT")
+}
+
+// ApiErrorNotAllowedInCurrentState responds with a REST error of type "NOT_ALLOWED_IN_CURRENT_STATE".
+// For use with "/api" endpoints.
+func ApiErrorNotAllowedInCurrentState(w http.ResponseWriter) {
+	apiError(w, http.StatusBadRequest, "NOT_ALLOWED_IN_CURRENT_STATE")
+}
+
+// ApiErrorNotFound responds with a REST error of type "NOT_FOUND".
+// For use with "/api" endpoints.
+func ApiErrorNotFound(w http.ResponseWriter) {
+	apiError(w, http.StatusNotFound, "NOT_FOUND")
+}
+
+// ApiErrorResourceInUse responds with a REST error of type "RESOURCE_IN_USE".
+// For use with "/api" endpoints.
+func ApiErrorResourceInUse(w http.ResponseWriter) {
+	apiError(w, http.StatusBadRequest, "RESOURCE_IN_USE")
+}
+
+// ApiErrorUnauthorized responds with a REST error of type "UNAUTHORIZED".
+// For use with "/api" endpoints.
+func ApiErrorUnauthorized(w http.ResponseWriter) {
+	apiError(w, http.StatusBadRequest, "UNAUTHORIZED")
+}
+
+// ApiErrorUnsupported responds with a REST error of type "UNSUPPORTED".
+// For use with "/api" endpoints.
+func ApiErrorUnsupported(w http.ResponseWriter) {
+	apiError(w, http.StatusBadRequest, "UNSUPPORTED")
+}
+
+func apiError(w http.ResponseWriter, statusCode int, errorType string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(statusCode)
+	w.Write([]byte(fmt.Sprintf(`{"error_type":"%s", "messages":[]}`, errorType)))
+}
+
 func (*handler) error(w http.ResponseWriter, err error) {
 	http.Error(w, err.Error(), http.StatusInternalServerError)
 	log.Print(err)
@@ -2281,17 +2335,6 @@ func (s *handler) libraryTrustedCertificatesID(w http.ResponseWriter, r *http.Re
 	}
 }
 
-func (s *handler) vmID(w http.ResponseWriter, r *http.Request) {
-	id := path.Base(r.URL.Path)
-
-	switch r.Method {
-	case http.MethodDelete:
-		s.deleteVM(&types.ManagedObjectReference{Type: "VirtualMachine", Value: id})
-	default:
-		http.NotFound(w, r)
-	}
-}
-
 func (s *handler) debugEcho(w http.ResponseWriter, r *http.Request) {
 	r.Write(w)
 }
diff --git a/vapi/vm/dataset/dataset.go b/vapi/vm/dataset/dataset.go
new file mode 100755
index 00000000..cb178d6f
--- /dev/null
+++ b/vapi/vm/dataset/dataset.go
@@ -0,0 +1,200 @@
+/*
+Copyright (c) 2023-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.
+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 dataset
+
+import (
+	"context"
+	"net/http"
+	"net/url"
+	"path"
+	"strconv"
+
+	"github.com/vmware/govmomi/vapi/rest"
+	"github.com/vmware/govmomi/vapi/vm/internal"
+)
+
+// Manager extends rest.Client, adding data set related methods.
+//
+// Data sets functionality was introduced in vSphere 8.0,
+// and requires the VM to have virtual hardware version 20 or newer.
+//
+// See the VMware Guest SDK Programming Guide for details on using data sets
+// from within the guest OS of a VM.
+//
+// See https://developer.vmware.com/apis/vsphere-automation/latest/vcenter/vm/data_sets/
+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,
+	}
+}
+
+// Access permission to the entries of a data set.
+type Access string
+
+const (
+	AccessNone      = Access("NONE")
+	AccessReadOnly  = Access("READ_ONLY")
+	AccessReadWrite = Access("READ_WRITE")
+)
+
+// Describes a data set to be created.
+type CreateSpec struct {
+	// Name should take the form "com.company.project" to avoid conflict with other uses.
+	// Must not be empty.
+	Name string `json:"name"`
+
+	// Description of the data set.
+	Description string `json:"description"`
+
+	// Host controls access to the data set entries by the ESXi host and the vCenter.
+	// For example, if the host access is set to NONE, the entries of this data set
+	// will not be accessible through the vCenter API.
+	// Must not be empty.
+	Host Access `json:"host"`
+
+	// Guest controls access to the data set entries by the guest OS of the VM (i.e. in-guest APIs).
+	// For example, if the guest access is set to READ_ONLY, it will be forbidden
+	// to create, delete, and update entries in this data set via the VMware Guest SDK.
+	// Must not be empty.
+	Guest Access `json:"guest"`
+
+	// OmitFromSnapshotAndClone controls whether the data set is included in snapshots and clones of the VM.
+	// When a VM is reverted to a snapshot, any data set with OmitFromSnapshotAndClone=true will be destroyed.
+	// Default is false.
+	OmitFromSnapshotAndClone *bool `json:"omit_from_snapshot_and_clone,omitempty"`
+}
+
+// Describes modifications to a data set.
+type UpdateSpec struct {
+	Description              *string `json:"description,omitempty"`
+	Host                     *Access `json:"host,omitempty"`
+	Guest                    *Access `json:"guest,omitempty"`
+	OmitFromSnapshotAndClone *bool   `json:"omit_from_snapshot_and_clone,omitempty"`
+}
+
+// Data set information.
+type Info struct {
+	Name                     string `json:"name"`
+	Description              string `json:"description"`
+	Host                     Access `json:"host"`
+	Guest                    Access `json:"guest"`
+	Used                     int    `json:"used"`
+	OmitFromSnapshotAndClone bool   `json:"omit_from_snapshot_and_clone"`
+}
+
+// Brief data set information.
+type Summary struct {
+	DataSet     string `json:"data_set"`
+	Name        string `json:"name"`
+	Description string `json:"description"`
+}
+
+const dataSetsPathField = "data-sets"
+
+func dataSetPath(vm string, dataSet string) string {
+	return path.Join(internal.VCenterVMPath, url.PathEscape(vm), dataSetsPathField, url.PathEscape(dataSet))
+}
+
+func dataSetsPath(vm string) string {
+	return path.Join(internal.VCenterVMPath, url.PathEscape(vm), dataSetsPathField)
+}
+
+const entriesPathField = "entries"
+
+func entryPath(vm string, dataSet string, key string) string {
+	return path.Join(internal.VCenterVMPath, url.PathEscape(vm), dataSetsPathField, url.PathEscape(dataSet), entriesPathField, url.PathEscape(key))
+}
+
+func entriesPath(vm string, dataSet string) string {
+	return path.Join(internal.VCenterVMPath, url.PathEscape(vm), dataSetsPathField, url.PathEscape(dataSet), entriesPathField)
+}
+
+// CreateDataSet creates a data set associated with the given virtual machine.
+func (c *Manager) CreateDataSet(ctx context.Context, vm string, spec *CreateSpec) (string, error) {
+	url := c.Resource(dataSetsPath(vm))
+	var res string
+	err := c.Do(ctx, url.Request(http.MethodPost, spec), &res)
+	return res, err
+}
+
+// DeleteDataSet deletes an existing data set from the given virtual machine.
+// The operation will fail if the data set is not empty.
+// Set the force flag to delete a non-empty data set.
+func (c *Manager) DeleteDataSet(ctx context.Context, vm string, dataSet string, force bool) error {
+	url := c.Resource(dataSetPath(vm, dataSet))
+	if force {
+		url.WithParam("force", strconv.FormatBool(force))
+	}
+	return c.Do(ctx, url.Request(http.MethodDelete), nil)
+}
+
+// GetDataSet retrieves information about the given data set.
+func (c *Manager) GetDataSet(ctx context.Context, vm string, dataSet string) (*Info, error) {
+	url := c.Resource(dataSetPath(vm, dataSet))
+	var res Info
+	err := c.Do(ctx, url.Request(http.MethodGet), &res)
+	return &res, err
+}
+
+// UpdateDataSet modifies the given data set.
+func (c *Manager) UpdateDataSet(ctx context.Context, vm string, dataSet string, spec *UpdateSpec) error {
+	url := c.Resource(dataSetPath(vm, dataSet))
+	return c.Do(ctx, url.Request(http.MethodPatch, spec), nil)
+}
+
+// ListDataSets returns a list of brief descriptions of the data sets on with the given virtual machine.
+func (c *Manager) ListDataSets(ctx context.Context, vm string) ([]Summary, error) {
+	url := c.Resource(dataSetsPath(vm))
+	var res []Summary
+	err := c.Do(ctx, url.Request(http.MethodGet), &res)
+	return res, err
+}
+
+// SetEntry creates or updates an entry in the given data set.
+// If an entry with the given key already exists, it will be overwritten.
+// The key can be at most 4096 bytes. The value can be at most 1MB.
+func (c *Manager) SetEntry(ctx context.Context, vm string, dataSet string, key string, value string) error {
+	url := c.Resource(entryPath(vm, dataSet, key))
+	return c.Do(ctx, url.Request(http.MethodPut, value), nil)
+}
+
+// GetEntry returns the value of the data set entry with the given key.
+func (c *Manager) GetEntry(ctx context.Context, vm string, dataSet string, key string) (string, error) {
+	url := c.Resource(entryPath(vm, dataSet, key))
+	var res string
+	err := c.Do(ctx, url.Request(http.MethodGet), &res)
+	return res, err
+}
+
+// DeleteEntry removes an existing entry from the given data set.
+func (c *Manager) DeleteEntry(ctx context.Context, vm string, dataSet string, key string) error {
+	url := c.Resource(entryPath(vm, dataSet, key))
+	return c.Do(ctx, url.Request(http.MethodDelete), nil)
+}
+
+// ListEntries returns a list of all entry keys in the given data set.
+func (c *Manager) ListEntries(ctx context.Context, vm string, dataSet string) ([]string, error) {
+	url := c.Resource(entriesPath(vm, dataSet))
+	var res []string
+	err := c.Do(ctx, url.Request(http.MethodGet), &res)
+	return res, err
+}
diff --git a/vapi/vm/internal/internal.go b/vapi/vm/internal/internal.go
new file mode 100644
index 00000000..b016caa1
--- /dev/null
+++ b/vapi/vm/internal/internal.go
@@ -0,0 +1,24 @@
+/*
+Copyright (c) 2023-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.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package internal
+
+const (
+	// VCenterVMPath is the REST endpoint for the virtual machine API
+	VCenterVMPath = "/api/vcenter/vm"
+	// LegacyVCenterVMPath is the legacy REST endpoint for the virtual machine API, relative to the "/rest" base path
+	LegacyVCenterVMPath = "/vcenter/vm"
+)
diff --git a/vapi/vm/simulator/simulator.go b/vapi/vm/simulator/simulator.go
new file mode 100755
index 00000000..9018faa5
--- /dev/null
+++ b/vapi/vm/simulator/simulator.go
@@ -0,0 +1,465 @@
+/*
+Copyright (c) 2023-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.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package simulator
+
+import (
+	"context"
+	"log"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/google/uuid"
+
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/rest"
+	vapi "github.com/vmware/govmomi/vapi/simulator"
+	"github.com/vmware/govmomi/vapi/vm/dataset"
+	"github.com/vmware/govmomi/vapi/vm/internal"
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+const (
+	// Minimal VM hardware version which supports DataSets feature
+	minVmHardwareVersionDataSets = "vmx-20"
+
+	typeVM = "VirtualMachine"
+)
+
+func init() {
+	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
+		New(s.Listen).Register(s, r)
+	})
+}
+
+type Handler struct {
+	u        *url.URL
+	registry *simulator.Registry
+}
+
+func New(u *url.URL) *Handler {
+	h := &Handler{
+		u: u,
+	}
+	return h
+}
+
+const (
+	restPathPrefix = rest.Path + internal.LegacyVCenterVMPath + "/"
+	apiPathPrefix  = internal.VCenterVMPath + "/"
+)
+
+func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) {
+	if r.IsVPX() {
+		h.registry = r
+		s.HandleFunc(restPathPrefix, h.handle)
+		s.HandleFunc(apiPathPrefix, h.handle)
+	}
+}
+
+// path starts with "/api/vcenter/vm"
+func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {
+	// The standard http.ServeMux does not support placeholders, so traverse the path segment by segment.
+	// 'tail' tracks the remaining path segments.
+	p := r.URL.Path
+	p = strings.TrimPrefix(p, restPathPrefix)
+	p = strings.TrimPrefix(p, apiPathPrefix)
+	tail := strings.Split(p, "/")
+	if len(tail) == 0 {
+		// "/api/vcenter/vm"
+		http.NotFound(w, r)
+	} else {
+		// "/api/vcenter/vm/..."
+		switch tail[0] {
+		case "":
+			http.NotFound(w, r)
+			return
+		default:
+			vmId := tail[0]
+			h.handleVm(w, r, tail[1:], vmId)
+		}
+	}
+}
+
+// path starts with "/api/vcenter/vm/{}"
+func (h *Handler) handleVm(w http.ResponseWriter, r *http.Request, tail []string, vmId string) {
+	vm := h.validateVmExists(w, r, vmId)
+	if vm == nil {
+		return
+	}
+	ctx := &simulator.Context{
+		Context: context.Background(),
+		Session: &simulator.Session{
+			UserSession: types.UserSession{
+				Key: uuid.New().String(),
+			},
+			Registry: h.registry,
+		},
+		Map: h.registry,
+	}
+	h.registry.WithLock(ctx, vm.Reference(), func() {
+		if len(tail) == 0 {
+			// "/api/vcenter/vm/{}"
+			switch r.Method {
+			case http.MethodDelete:
+				h.deleteVM(w, r, ctx, vm)
+			default:
+				http.NotFound(w, r)
+			}
+		} else {
+			// "/api/vcenter/vm/{}/..."
+			switch tail[0] {
+			case "data-sets":
+				h.handleVmDataSets(w, r, tail[1:], vm)
+			default:
+				http.NotFound(w, r)
+			}
+		}
+	})
+}
+
+// path starts with "/api/vcenter/vm/{}/data-sets"
+func (h *Handler) handleVmDataSets(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine) {
+	if !h.validateVmHardwareVersionDataSets(w, r, vm) {
+		return
+	}
+	if len(tail) == 0 {
+		// "/api/vcenter/vm/{}/data-sets"
+		switch r.Method {
+		case http.MethodGet:
+			h.listDataSets(w, r, vm)
+		case http.MethodPost:
+			h.createDataSet(w, r, vm)
+		default:
+			http.NotFound(w, r)
+		}
+	} else {
+		// "/api/vcenter/vm/{}/data-sets/..."
+		switch tail[0] {
+		case "":
+			http.NotFound(w, r)
+			return
+		default:
+			dataSetId := tail[0]
+			h.handleVmDataSet(w, r, tail[1:], vm, dataSetId)
+		}
+	}
+}
+
+// path starts with "/api/vcenter/vm/{}/data-sets/{}"
+func (h *Handler) handleVmDataSet(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSetId string) {
+	dataSet := h.validateDataSetExists(w, r, vm, dataSetId)
+	if dataSet == nil {
+		return
+	}
+	if len(tail) == 0 {
+		// "/api/vcenter/vm/{}/data-sets/{}"
+		switch r.Method {
+		case http.MethodGet:
+			vapi.StatusOK(w, dataSet.Info)
+		case http.MethodPatch:
+			h.updateDataSet(w, r, vm, dataSet)
+		case http.MethodDelete:
+			h.deleteDataSet(w, r, vm, dataSet)
+		default:
+			http.NotFound(w, r)
+		}
+	} else {
+		// "/api/vcenter/vm/{}/data-sets/{}/..."
+		switch tail[0] {
+		case "entries":
+			h.handleVmDataSetEntries(w, r, tail[1:], vm, dataSet)
+		default:
+			http.NotFound(w, r)
+		}
+
+	}
+}
+
+// path starts with "/api/vcenter/vm/{}/data-sets/{}/entries"
+func (h *Handler) handleVmDataSetEntries(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
+	if len(tail) == 0 {
+		// "/api/vcenter/vm/{}/data-sets/{}/entries"
+		switch r.Method {
+		case http.MethodGet:
+			h.listDataSetEntries(w, r, vm, dataSet)
+		default:
+			http.NotFound(w, r)
+		}
+	} else {
+		// "/api/vcenter/vm/{}/data-sets/{}/entries/..."
+		switch tail[0] {
+		case "":
+			http.NotFound(w, r)
+			return
+		default:
+			entryKey := tail[0]
+			h.handleVmDataSetEntry(w, r, tail[1:], vm, dataSet, entryKey)
+		}
+	}
+}
+
+// path starts with "/api/vcenter/vm/{}/data-sets/{}/entries/{}"
+func (h *Handler) handleVmDataSetEntry(w http.ResponseWriter, r *http.Request, tail []string, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
+	if len(tail) > 0 {
+		http.NotFound(w, r)
+		return
+	}
+	switch r.Method {
+	case http.MethodGet:
+		h.getDataSetEntry(w, r, vm, dataSet, entryKey)
+	case http.MethodPut:
+		h.setDataSetEntry(w, r, vm, dataSet, entryKey)
+	case http.MethodDelete:
+		h.deleteDataSetEntry(w, r, vm, dataSet, entryKey)
+	default:
+		http.NotFound(w, r)
+	}
+}
+
+func (h *Handler) deleteVM(w http.ResponseWriter, r *http.Request, ctx *simulator.Context, vm *simulator.VirtualMachine) {
+	taskRef := vm.DestroyTask(ctx, &types.Destroy_Task{This: vm.Self}).(*methods.Destroy_TaskBody).Res.Returnval
+	task := ctx.Map.Get(taskRef).(*simulator.Task)
+	task.Wait()
+	if task.Info.Error != nil {
+		log.Printf("%s %s: %v", r.Method, r.RequestURI, task.Info.Error)
+		vapi.ApiErrorGeneral(w)
+		return
+	}
+	vapi.StatusOK(w)
+}
+
+func (h *Handler) createDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) {
+	if !h.validateVmIsNotSuspended(w, r, vm) {
+		return
+	}
+	var createSpec dataset.CreateSpec
+	if !vapi.Decode(r, w, &createSpec) {
+		return
+	}
+	if createSpec.Name == "" {
+		vapi.ApiErrorInvalidArgument(w)
+		return
+	}
+	if createSpec.Host == "" {
+		vapi.ApiErrorInvalidArgument(w)
+		return
+	}
+	if createSpec.Guest == "" {
+		vapi.ApiErrorInvalidArgument(w)
+		return
+	}
+	dataSetId := createSpec.Name
+	_, ok := vm.DataSets[dataSetId]
+	if ok {
+		vapi.ApiErrorAlreadyExists(w)
+		return
+	}
+	dataSet := &simulator.DataSet{
+		Info: &dataset.Info{
+			Name:                     createSpec.Name,
+			Description:              createSpec.Description,
+			Host:                     createSpec.Host,
+			Guest:                    createSpec.Guest,
+			Used:                     0,
+			OmitFromSnapshotAndClone: getOrDefault(createSpec.OmitFromSnapshotAndClone, false),
+		},
+		ID:      dataSetId,
+		Entries: make(map[string]string),
+	}
+	vm.DataSets[dataSetId] = dataSet
+	vapi.StatusOK(w, dataSetId)
+}
+
+func (h *Handler) listDataSets(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) {
+	var result []dataset.Summary = make([]dataset.Summary, 0, len(vm.DataSets))
+	for _, v := range vm.DataSets {
+		summary := dataset.Summary{
+			DataSet:     v.ID,
+			Name:        v.Name,
+			Description: v.Description,
+		}
+		result = append(result, summary)
+	}
+	vapi.StatusOK(w, result)
+}
+
+func (h *Handler) deleteDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
+	if !h.validateVmIsNotSuspended(w, r, vm) {
+		return
+	}
+	force := strings.EqualFold(r.URL.Query().Get("force"), "true")
+	if len(dataSet.Entries) > 0 && !force {
+		// cannot delete non-empty data set without force
+		vapi.ApiErrorResourceInUse(w)
+		return
+	}
+	delete(vm.DataSets, dataSet.ID)
+	vapi.StatusOK(w)
+}
+
+func (h *Handler) updateDataSet(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
+	if !h.validateVmIsNotSuspended(w, r, vm) {
+		return
+	}
+	var updateSpec dataset.UpdateSpec
+	if !vapi.Decode(r, w, &updateSpec) {
+		return
+	}
+	if updateSpec.Description != nil {
+		dataSet.Description = *updateSpec.Description
+	}
+	if updateSpec.Host != nil {
+		dataSet.Host = *updateSpec.Host
+	}
+	if updateSpec.Guest != nil {
+		dataSet.Guest = *updateSpec.Guest
+	}
+	if updateSpec.OmitFromSnapshotAndClone != nil {
+		dataSet.OmitFromSnapshotAndClone = *updateSpec.OmitFromSnapshotAndClone
+	}
+	vapi.StatusOK(w)
+}
+
+func (h *Handler) listDataSetEntries(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet) {
+	if dataSet.Host == dataset.AccessNone {
+		vapi.ApiErrorUnauthorized(w)
+		return
+	}
+	var result []string = make([]string, 0, len(dataSet.Entries))
+	for k := range dataSet.Entries {
+		result = append(result, k)
+	}
+	vapi.StatusOK(w, result)
+}
+
+func (h *Handler) getDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
+	if dataSet.Host == dataset.AccessNone {
+		vapi.ApiErrorUnauthorized(w)
+		return
+	}
+	val, ok := dataSet.Entries[entryKey]
+	if !ok {
+		vapi.ApiErrorNotFound(w)
+		return
+	}
+	vapi.StatusOK(w, val)
+}
+
+const (
+	// A key can be at most 4096 bytes.
+	entryKeyMaxLen = 4096
+	// A value can be at most 1MB.
+	entryValueMaxLen = 1024 * 1024
+)
+
+func (h *Handler) setDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
+	var val string
+	if !vapi.Decode(r, w, &val) {
+		return
+	}
+	if !h.validateVmIsNotSuspended(w, r, vm) {
+		return
+	}
+	if dataSet.Host != dataset.AccessReadWrite {
+		vapi.ApiErrorUnauthorized(w)
+		return
+	}
+	if len(entryKey) > entryKeyMaxLen {
+		vapi.ApiErrorInvalidArgument(w)
+		return
+	}
+	if len(val) > entryValueMaxLen {
+		vapi.ApiErrorInvalidArgument(w)
+		return
+	}
+	old, ok := dataSet.Entries[entryKey]
+	if ok {
+		dataSet.Used -= len(entryKey)
+		dataSet.Used -= len(old)
+	}
+	dataSet.Entries[entryKey] = val
+	dataSet.Used += len(entryKey)
+	dataSet.Used += len(val)
+	vapi.StatusOK(w)
+}
+
+func (h *Handler) deleteDataSetEntry(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSet *simulator.DataSet, entryKey string) {
+	if !h.validateVmIsNotSuspended(w, r, vm) {
+		return
+	}
+	if dataSet.Host != dataset.AccessReadWrite {
+		vapi.ApiErrorUnauthorized(w)
+		return
+	}
+	val, ok := dataSet.Entries[entryKey]
+	if !ok {
+		vapi.ApiErrorNotFound(w)
+		return
+	}
+	dataSet.Used -= len(entryKey)
+	dataSet.Used -= len(val)
+	delete(dataSet.Entries, entryKey)
+	vapi.StatusOK(w)
+}
+
+func getOrDefault(b *bool, defaultValue bool) bool {
+	if b == nil {
+		return defaultValue
+	}
+	return *b
+}
+
+func (h *Handler) validateVmExists(w http.ResponseWriter, r *http.Request, vmId string) *simulator.VirtualMachine {
+	vm, ok := h.registry.Get(types.ManagedObjectReference{Type: typeVM, Value: vmId}).(*simulator.VirtualMachine)
+	if !ok {
+		vapi.ApiErrorNotFound(w)
+		return nil
+	}
+	return vm
+}
+
+func (h *Handler) validateDataSetExists(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine, dataSetId string) *simulator.DataSet {
+	dataSet, ok := vm.DataSets[dataSetId]
+	if !ok {
+		vapi.ApiErrorNotFound(w)
+		return nil
+	}
+	return dataSet
+}
+
+func (h *Handler) validateVmHardwareVersionDataSets(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) bool {
+	version := vm.Config.Version
+	if len(version) < len(minVmHardwareVersionDataSets) {
+		vapi.ApiErrorUnsupported(w)
+		return false
+	}
+	if len(version) == len(minVmHardwareVersionDataSets) && version < minVmHardwareVersionDataSets {
+		vapi.ApiErrorUnsupported(w)
+		return false
+	}
+	return true
+}
+
+func (h *Handler) validateVmIsNotSuspended(w http.ResponseWriter, r *http.Request, vm *simulator.VirtualMachine) bool {
+	if vm.Summary.Runtime.PowerState == types.VirtualMachinePowerStateSuspended {
+		vapi.ApiErrorNotAllowedInCurrentState(w)
+		return false
+	}
+	return true
+}
diff --git a/vcsim/main.go b/vcsim/main.go
old mode 100644
new mode 100755
index dbdda813..b0e226c9
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -1,5 +1,5 @@
 /*
-Copyright (c) 2017 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.
@@ -50,6 +50,7 @@ import (
 	_ "github.com/vmware/govmomi/vapi/cluster/simulator"
 	_ "github.com/vmware/govmomi/vapi/namespace/simulator"
 	_ "github.com/vmware/govmomi/vapi/simulator"
+	_ "github.com/vmware/govmomi/vapi/vm/simulator"
 	_ "github.com/vmware/govmomi/vsan/simulator"
 )
 


Test patch:
diff --git a/govc/test/vm_dataset.bats b/govc/test/vm_dataset.bats
new file mode 100644
index 00000000..18a90f16
--- /dev/null
+++ b/govc/test/vm_dataset.bats
@@ -0,0 +1,475 @@
+#!/usr/bin/env bats
+
+load test_helper
+
+# Data set support requires the virtual machine be at virtual hardware version vmx-20 or later
+new_unsupported_hardware_vm() {
+  id=$(new_id)
+  govc vm.create -version=vmx-19 $id
+  echo $id
+}
+
+new_supported_hardware_vm() {
+  id=$(new_id)
+  govc vm.create -version=vmx-20 $id
+  echo $id
+}
+
+@test "vm.dataset.ls" {
+  vcsim_env
+
+  old_vm=$(new_unsupported_hardware_vm)
+  vm=$(new_supported_hardware_vm)
+
+  run govc vm.dataset.ls -vm $vm
+  assert_success ""
+
+  # non-existing VM
+  run govc vm.dataset.ls -vm enoent
+  assert_failure "govc: vm 'enoent' not found"
+
+  # VM hardware too old
+  run govc vm.dataset.ls -vm $old_vm
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNSUPPORTED\", \"messages\":[]}"
+}
+
+@test "vm.dataset.create" {
+  vcsim_env
+
+  old_vm=$(new_unsupported_hardware_vm)
+  vm=$(new_supported_hardware_vm)
+
+  # create
+  id=$(new_id)
+  run govc vm.dataset.create -vm $vm $id
+  assert_success "$id"
+
+  # list the created data set
+  run govc vm.dataset.ls -vm $vm
+  assert_success "$id"
+
+  # create second data set
+  id2=$(new_id)
+  run govc vm.dataset.create -vm $vm $id2
+  assert_success "$id2"
+
+  # list the two data sets
+  run govc vm.dataset.ls -vm $vm
+  assert_success
+  assert_line "$id"
+  assert_line "$id2"
+
+  # create with duplicate name
+  run govc vm.dataset.create -vm $vm $id
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"ALREADY_EXISTS\", \"messages\":[]}"
+
+  # create on non-existing VM
+  run govc vm.dataset.create -vm enoent $(new_id)
+  assert_failure "govc: vm 'enoent' not found"
+
+  # create on unsupported VM
+  run govc vm.dataset.create -vm $old_vm $(new_id)
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNSUPPORTED\", \"messages\":[]}"
+
+  # create on suspended VM
+  govc vm.power -suspend $vm
+  run govc vm.dataset.create -vm $vm $(new_id)
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"NOT_ALLOWED_IN_CURRENT_STATE\", \"messages\":[]}"
+}
+
+@test "vm.dataset.info" {
+  vcsim_env
+
+  vm=$(new_supported_hardware_vm)
+
+  # create
+  id=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Some description." -guest-access READ_ONLY -omit-from-snapshot=false $id
+  assert_success "$id"
+
+  # get
+  run govc vm.dataset.info -vm $vm $id
+  assert_success
+  assert_line "Name: $id"
+  assert_line "Description: Some description."
+  assert_line "Host: READ_WRITE"
+  assert_line "Guest: READ_ONLY"
+  assert_line "Used: 0"
+  assert_line "OmitFromSnapshotAndClone: false"
+
+  # create an entry
+  run govc vm.dataset.entry.set -vm $vm -dataset $id key1 val1
+  assert_success
+
+  # get to verify the 'Used' field includes the footprint of the entry
+  run govc vm.dataset.info -vm $vm $id
+  assert_success
+  assert_line "Name: $id"
+  assert_line "Description: Some description."
+  assert_line "Host: READ_WRITE"
+  assert_line "Guest: READ_ONLY"
+  assert_line "Used: 8"
+  assert_line "OmitFromSnapshotAndClone: false"
+
+  # non-existing VM
+  run govc vm.dataset.info -vm enoent $id
+  assert_failure "govc: vm 'enoent' not found"
+
+  # non-existing data set
+  run govc vm.dataset.info -vm $vm enoent
+  assert_failure
+  assert_matches "404 Not Found"
+}
+
+@test "vm.dataset.update" {
+  vcsim_env
+
+  vm=$(new_supported_hardware_vm)
+
+  # create
+  id=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Initial description." $id
+  assert_success "$id"
+
+  # update description
+  run govc vm.dataset.update -vm $vm -d "Updated description." $id
+  assert_success ""
+
+  # get the updated
+  run govc vm.dataset.info -vm $vm $id
+  assert_success
+  assert_line "Name: $id"
+  assert_line "Description: Updated description."
+
+  # update on non-existing VM
+  run govc vm.dataset.update -vm enoent -d "Even newer description." $id
+  assert_failure "govc: vm 'enoent' not found"
+
+  # update non-existing data set
+  run govc vm.dataset.update -vm $vm -d "Even newer description." enoent
+  assert_failure
+  assert_matches "404 Not Found"
+
+  # update on suspended VM
+  govc vm.power -suspend $vm
+  run govc vm.dataset.update -vm $vm -d "Even newer description." $id
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"NOT_ALLOWED_IN_CURRENT_STATE\", \"messages\":[]}"
+}
+
+@test "vm.dataset.rm" {
+  vcsim_env
+
+  vm=$(new_supported_hardware_vm)
+
+  # create
+  id=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Initial description." $id
+  assert_success "$id"
+
+  # delete on non-existing VM
+  run govc vm.dataset.rm -vm enoent $id
+  assert_failure "govc: vm 'enoent' not found"
+
+  # delete non-existing data set
+  run govc vm.dataset.rm -vm $vm enoent
+  assert_failure
+  assert_matches "404 Not Found"
+
+  # delete on suspended VM
+  govc vm.power -suspend $vm
+  run govc vm.dataset.rm -vm $vm $id
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"NOT_ALLOWED_IN_CURRENT_STATE\", \"messages\":[]}"
+  govc vm.power -on $vm
+
+  # delete
+  run govc vm.dataset.rm -vm $vm $id
+  assert_success ""
+
+  # list to verify
+  run govc vm.dataset.ls -vm $vm
+  assert_success ""
+
+  # create a data set with an entry
+  id=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Initial description." $id
+  assert_success "$id"
+  run govc vm.dataset.entry.set -vm $vm -dataset $id key1 val1
+  assert_success
+
+  # try to delete the non-empty data set
+  run govc vm.dataset.rm -vm $vm $id
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"RESOURCE_IN_USE\", \"messages\":[]}"
+
+  # delete the non-empty data set with force
+  run govc vm.dataset.rm -vm $vm -force=true $id
+  assert_success ""
+}
+
+@test "vm.dataset.vmclone" {
+  vcsim_env
+
+  vm=$(new_supported_hardware_vm)
+
+  # create data set which is included in clones/snapshots
+  id=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "First data set." -omit-from-snapshot=false $id
+  assert_success "$id"
+
+  # create data set which is excluded from clones/snapshots
+  id2=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Second data set." -omit-from-snapshot=true $id2
+  assert_success "$id2"
+
+  # clone the VM
+  cloned_vm=$(new_id)
+  run govc vm.clone -vm $vm $cloned_vm
+  assert_success
+
+  # update the description of the data set on the original VM
+  run govc vm.dataset.update -vm $vm -d "Updated description." $id
+  assert_success ""
+  
+  # create another data set which is included in clones/snapshots on the original VM
+  id3=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Third data set." -omit-from-snapshot=false $id3
+  assert_success "$id3"
+
+  # list the data sets on the cloned VM
+  run govc vm.dataset.ls -vm $cloned_vm
+  assert_success "$id"
+
+  # verify the data set on the cloned VM was not affected by the update
+  run govc vm.dataset.info -vm $cloned_vm $id
+  assert_success
+  assert_line "Name: $id"
+  assert_line "Description: First data set."
+}
+
+@test "vm.dataset.vmsnapshot" {
+  vcsim_env
+
+  vm=$(new_supported_hardware_vm)
+
+  # create data set which is included in clones/snapshots
+  id=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "First data set." -omit-from-snapshot=false $id
+  assert_success "$id"
+
+  # create data set which is excluded from clones/snapshots
+  id2=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Second data set." -omit-from-snapshot=true $id2
+  assert_success "$id2"
+
+  # create a snapshot
+  snapshot=$(new_id)
+  run govc snapshot.create -vm $vm $snapshot
+  assert_success
+
+  # update the description of the data set on the original VM
+  run govc vm.dataset.update -vm $vm -d "Updated description." $id
+  assert_success ""
+
+  # create another data set which is included in clones/snapshots
+  id3=$(new_id)
+  run govc vm.dataset.create -vm $vm -d "Third data set." -omit-from-snapshot=false $id3
+  assert_success "$id3"
+
+  # revert the VM to the snapshot
+  run govc snapshot.revert -vm $vm $snapshot
+  assert_success
+
+  # list the data sets on the VM
+  run govc vm.dataset.ls -vm $vm
+  assert_success "$id"
+
+  # verify the data set does not contain the update
+  run govc vm.dataset.info -vm $vm $id
+  assert_success
+  assert_line "Name: $id"
+  assert_line "Description: First data set."  
+}
+
+@test "vm.dataset.entry.ls" {
+  vcsim_env
+
+  # setup VM and data set
+  vm=$(new_supported_hardware_vm)
+  ds=$(new_id)
+  run govc vm.dataset.create -vm $vm $ds
+  assert_success "$ds"
+
+  run govc vm.dataset.entry.ls -vm $vm -dataset $ds
+  assert_success ""
+
+  # non-existing VM
+  run govc vm.dataset.entry.ls -vm enoent -dataset $ds
+  assert_failure "govc: vm 'enoent' not found"
+
+  # non-existing data set
+  run govc vm.dataset.entry.ls -vm $vm -dataset enoent
+  assert_failure
+  assert_matches "404 Not Found"
+
+  # host does not have read access to the data set
+  run govc vm.dataset.update -vm $vm -host-access NONE $ds
+  assert_success ""
+  run govc vm.dataset.entry.ls -vm $vm -dataset $ds
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNAUTHORIZED\", \"messages\":[]}"
+}
+
+@test "vm.dataset.entry.get" {
+  vcsim_env
+
+  # setup VM and data set
+  vm=$(new_supported_hardware_vm)
+  ds=$(new_id)
+  run govc vm.dataset.create -vm $vm $ds
+  assert_success "$ds"
+
+  # non-existing VM
+  run govc vm.dataset.entry.get -vm enoent -dataset $ds somekey
+  assert_failure "govc: vm 'enoent' not found"
+
+  # non-existing data set
+  run govc vm.dataset.entry.get -vm $vm -dataset enoent somekey
+  assert_failure
+  assert_matches "404 Not Found"
+
+  # non-existing entry
+  run govc vm.dataset.entry.get -vm $vm -dataset $ds enoent
+  assert_failure
+  assert_matches "404 Not Found"
+}
+
+@test "vm.dataset.entry.set" {
+  vcsim_env
+
+  # setup VM and data set
+  vm=$(new_supported_hardware_vm)
+  ds=$(new_id)
+  run govc vm.dataset.create -vm $vm $ds
+  assert_success "$ds"
+
+  # create new entry
+  run govc vm.dataset.entry.set -vm $vm -dataset $ds key1 val1
+  assert_success
+
+  # list the created entry
+  run govc vm.dataset.entry.ls -vm $vm -dataset $ds
+  assert_success "key1"
+
+  # get the value of the created entry
+  run govc vm.dataset.entry.get -vm $vm -dataset $ds key1
+  assert_success "val1"
+
+  # update the value of the entry
+  run govc vm.dataset.entry.set -vm $vm -dataset $ds key1 val1b
+  assert_success
+
+  # get the updated value
+  run govc vm.dataset.entry.get -vm $vm -dataset $ds key1
+  assert_success "val1b"
+
+  # non-existing VM
+  run govc vm.dataset.entry.set -vm enoent -dataset $ds key2 val2
+  assert_failure "govc: vm 'enoent' not found"
+
+  # non-existing data set
+  run govc vm.dataset.entry.set -vm $vm -dataset enoent key2 val2
+  assert_failure
+  assert_matches "404 Not Found"
+
+  # suspended VM
+  govc vm.power -suspend $vm
+  run govc vm.dataset.entry.set -vm $vm -dataset $ds key2 val2
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"NOT_ALLOWED_IN_CURRENT_STATE\", \"messages\":[]}"
+  govc vm.power -on $vm
+}
+
+@test "vm.dataset.entry.rm" {
+  vcsim_env
+
+  # setup VM, data set and entry
+  vm=$(new_supported_hardware_vm)
+  ds=$(new_id)
+  run govc vm.dataset.create -vm $vm $ds
+  assert_success "$ds"
+  run govc vm.dataset.entry.set -vm $vm -dataset $ds key1 val1
+  assert_success
+
+  # delete on non-existing VM
+  run govc vm.dataset.entry.rm -vm enoent -dataset $ds key1
+  assert_failure "govc: vm 'enoent' not found"
+
+  # delete on non-existing data set
+  run govc vm.dataset.entry.rm -vm $vm -dataset enoent key1
+  assert_failure
+  assert_matches "404 Not Found"
+
+  # delete on suspended VM
+  govc vm.power -suspend $vm
+  run govc vm.dataset.entry.rm -vm $vm -dataset $ds key1
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"NOT_ALLOWED_IN_CURRENT_STATE\", \"messages\":[]}"
+  govc vm.power -on $vm
+
+  # delete
+  run govc vm.dataset.entry.rm -vm $vm -dataset $ds key1
+  assert_success
+
+  # list to verify
+  run govc vm.dataset.entry.ls -vm $vm -dataset $ds
+  assert_success ""
+}
+
+@test "vm.dataset.entry.access" {
+  vcsim_env
+
+  # setup VM, data set and entry
+  vm=$(new_supported_hardware_vm)
+  ds=$(new_id)
+  run govc vm.dataset.create -vm $vm $ds
+  assert_success "$ds"
+  run govc vm.dataset.entry.set -vm $vm -dataset $ds key1 val1
+  assert_success
+
+  # change the host access to NONE (govc calls the VC API, so the guest access does not matter here)
+  # the default access mode is READ_WRITE and has already been covered by previous tests
+  run govc vm.dataset.update -vm $vm -host-access NONE $ds
+  assert_success
+
+  # list
+  run govc vm.dataset.entry.ls -vm $vm -dataset $ds
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNAUTHORIZED\", \"messages\":[]}"
+
+  # get
+  run govc vm.dataset.entry.get -vm $vm -dataset $ds key1
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNAUTHORIZED\", \"messages\":[]}"
+
+  # set
+  run govc vm.dataset.entry.set -vm $vm -dataset $ds key1 val1b
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNAUTHORIZED\", \"messages\":[]}"
+
+  # delete
+  run govc vm.dataset.entry.rm -vm $vm -dataset $ds key1
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNAUTHORIZED\", \"messages\":[]}"
+
+  # change the host access to READ_ONLY
+  run govc vm.dataset.update -vm $vm -host-access READ_ONLY $ds
+  assert_success
+
+  # list
+  run govc vm.dataset.entry.ls -vm $vm -dataset $ds
+  assert_success "key1"
+
+  # get
+  run govc vm.dataset.entry.get -vm $vm -dataset $ds key1
+  assert_success "val1"
+
+  # set
+  run govc vm.dataset.entry.set -vm $vm -dataset $ds key1 val1b
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNAUTHORIZED\", \"messages\":[]}"
+
+  # delete
+  run govc vm.dataset.entry.rm -vm $vm -dataset $ds key1
+  assert_failure "govc: 400 Bad Request: {\"error_type\":\"UNAUTHORIZED\", \"messages\":[]}"
+}