Repository: vmware/govmomi
Pull request #3479 (2024-07-01 11:03:11)

api: Add ExtendVirtualDisk method on VirtualDiskManager
## Description

Adding a binding for `extendVirtualDisk` on `VirtualDiskManager`.

https://github.com/hashicorp/terraform-provider-vsphere/issues/851

## 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?

Extended the tests for `virtual_disk_manager.go` with new scenarios that cover
1.[POSITIVE] Extending a disk
2.[NEGATIVE] Attempting to shrink a disk
3.[NEGATIVE] Attempting to extend a disk that does not exist

The simulation for virtual disks did not include metadata for the disk size.
I've modified it so that it writes the size value (in KB) in the file itself.

Manually verified the same scenarios on a real VMDK.

## 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 d6b53940..87b6d30a 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -81,6 +81,7 @@ but appear via `govc $cmd -h`:
  - [datastore.cp](#datastorecp)
  - [datastore.create](#datastorecreate)
  - [datastore.disk.create](#datastorediskcreate)
+ - [datastore.disk.extend](#datastorediskextend)
  - [datastore.disk.inflate](#datastorediskinflate)
  - [datastore.disk.info](#datastorediskinfo)
  - [datastore.disk.shrink](#datastorediskshrink)
@@ -1222,6 +1223,23 @@ Options:
   -uuid=                 Disk UUID
 ```
 
+## datastore.disk.extend
+
+```
+Usage: govc datastore.disk.extend [OPTIONS] VMDK
+
+Extend VMDK on DS.
+
+Examples:
+  govc datastore.disk.extend disks/disk1.vmdk -size=24G
+  govc datastore.disk.extend disks/disk1.vmdk -size=24G -eagerZero=true
+
+Options:
+  -ds=                   Datastore [GOVC_DATASTORE]
+  -eagerZero=false       If true, the extended part of the disk will be explicitly filled with zeroes
+  -size=0B               New capacity for the disk
+```
+
 ## datastore.disk.inflate
 
 ```
diff --git a/govc/datastore/disk/extend.go b/govc/datastore/disk/extend.go
new file mode 100644
index 00000000..594a5860
--- /dev/null
+++ b/govc/datastore/disk/extend.go
@@ -0,0 +1,92 @@
+/*
+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 disk
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/units"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/object"
+)
+
+type extend struct {
+	*flags.DatastoreFlag
+	Bytes     units.ByteSize
+	EagerZero bool
+}
+
+func init() {
+	cli.Register("datastore.disk.extend", &extend{})
+}
+
+func (cmd *extend) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.DatastoreFlag, ctx = flags.NewDatastoreFlag(ctx)
+	cmd.DatastoreFlag.Register(ctx, f)
+
+	f.Var(&cmd.Bytes, "size", "New capacity for the disk")
+	f.BoolVar(&cmd.EagerZero, "eagerZero", false, "If true, the extended part of the disk will be explicitly filled with zeroes")
+}
+
+func (cmd *extend) Process(ctx context.Context) error {
+	return cmd.DatastoreFlag.Process(ctx)
+}
+
+func (cmd *extend) Usage() string {
+	return "VMDK"
+}
+
+func (cmd *extend) Description() string {
+	return `Extend VMDK on DS.
+
+Examples:
+  govc datastore.disk.extend disks/disk1.vmdk -size=24G
+  govc datastore.disk.extend disks/disk1.vmdk -size=24G -eagerZero=true`
+}
+
+func (cmd *extend) Run(ctx context.Context, f *flag.FlagSet) error {
+	if f.NArg() == 0 {
+		return flag.ErrHelp
+	}
+
+	dc, err := cmd.Datacenter()
+	if err != nil {
+		return err
+	}
+
+	ds, err := cmd.Datastore()
+	if err != nil {
+		return err
+	}
+
+	m := object.NewVirtualDiskManager(ds.Client())
+	path := ds.Path(f.Arg(0))
+	task, err := m.ExtendVirtualDisk(ctx, path, dc, int64(cmd.Bytes/1024), &cmd.EagerZero)
+	if err != nil {
+		return err
+	}
+
+	logger := cmd.ProgressLogger(fmt.Sprintf("Extending %s...", path))
+	defer logger.Wait()
+
+	_, err = task.WaitForResult(ctx, logger)
+	return err
+}
diff --git a/object/virtual_disk_manager.go b/object/virtual_disk_manager.go
index 72439caf..6724843e 100644
--- a/object/virtual_disk_manager.go
+++ b/object/virtual_disk_manager.go
@@ -94,6 +94,33 @@ func (m VirtualDiskManager) CreateVirtualDisk(
 	return NewTask(m.c, res.Returnval), nil
 }
 
+// ExtendVirtualDisk extends an existing virtual disk.
+func (m VirtualDiskManager) ExtendVirtualDisk(
+	ctx context.Context,
+	name string, datacenter *Datacenter,
+	capacityKb int64,
+	eagerZero *bool) (*Task, error) {
+
+	req := types.ExtendVirtualDisk_Task{
+		This:          m.Reference(),
+		Name:          name,
+		NewCapacityKb: capacityKb,
+		EagerZero:     eagerZero,
+	}
+
+	if datacenter != nil {
+		ref := datacenter.Reference()
+		req.Datacenter = &ref
+	}
+
+	res, err := methods.ExtendVirtualDisk_Task(ctx, m.c, &req)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewTask(m.c, res.Returnval), nil
+}
+
 // MoveVirtualDisk moves a virtual disk.
 func (m VirtualDiskManager) MoveVirtualDisk(
 	ctx context.Context,
diff --git a/simulator/virtual_disk_manager.go b/simulator/virtual_disk_manager.go
index 88844b23..3843bdf8 100644
--- a/simulator/virtual_disk_manager.go
+++ b/simulator/virtual_disk_manager.go
@@ -19,6 +19,7 @@ package simulator
 import (
 	"fmt"
 	"os"
+	"strconv"
 	"strings"
 
 	"github.com/google/uuid"
@@ -76,12 +77,59 @@ func vdmCreateVirtualDisk(op types.VirtualDeviceConfigSpecFileOperation, req *ty
 			return fm.fault(name, err, new(types.CannotCreateFile))
 		}
 
+		if req.Spec != nil {
+			var spec interface{} = req.Spec
+			fileBackedSpec, ok := spec.(*types.FileBackedVirtualDiskSpec)
+
+			if !ok {
+				return fm.fault(name, nil, new(types.FileFault))
+			}
+
+			if _, err = f.WriteString(strconv.FormatInt(fileBackedSpec.CapacityKb, 10)); err != nil {
+				return fm.fault(name, err, new(types.FileFault))
+			}
+		}
+
 		_ = f.Close()
 	}
 
 	return nil
 }
 
+func vdmExtendVirtualDisk(req *types.ExtendVirtualDisk_Task) types.BaseMethodFault {
+	fm := Map.FileManager()
+
+	file, fault := fm.resolve(req.Datacenter, req.Name)
+	if fault != nil {
+		return fault
+	}
+
+	for _, name := range vdmNames(file) {
+		_, err := os.Stat(name)
+		if err == nil {
+			content, err := os.ReadFile(name)
+			if err != nil {
+				return fm.fault(name, err, new(types.FileFault))
+			}
+
+			capacity, err := strconv.Atoi(string(content))
+			if err != nil {
+				return fm.fault(name, err, new(types.FileFault))
+			}
+
+			if int64(capacity) > req.NewCapacityKb {
+				// cannot shrink disk
+				return fm.fault(name, nil, new(types.FileFault))
+			}
+			return nil
+		} else {
+			return fm.fault(name, nil, new(types.FileNotFound))
+		}
+	}
+
+	return nil
+}
+
 func (m *VirtualDiskManager) CreateVirtualDiskTask(ctx *Context, req *types.CreateVirtualDisk_Task) soap.HasFault {
 	task := CreateTask(m, "createVirtualDisk", func(*Task) (types.AnyType, types.BaseMethodFault) {
 		if err := vdmCreateVirtualDisk(types.VirtualDeviceConfigSpecFileOperationCreate, req); err != nil {
@@ -97,6 +145,21 @@ func (m *VirtualDiskManager) CreateVirtualDiskTask(ctx *Context, req *types.Crea
 	}
 }
 
+func (m *VirtualDiskManager) ExtendVirtualDiskTask(ctx *Context, req *types.ExtendVirtualDisk_Task) soap.HasFault {
+	task := CreateTask(m, "extendVirtualDisk", func(*Task) (types.AnyType, types.BaseMethodFault) {
+		if err := vdmExtendVirtualDisk(req); err != nil {
+			return "", err
+		}
+		return req.Name, nil
+	})
+
+	return &methods.ExtendVirtualDisk_TaskBody{
+		Res: &types.ExtendVirtualDisk_TaskResponse{
+			Returnval: task.Run(ctx),
+		},
+	}
+}
+
 func (m *VirtualDiskManager) DeleteVirtualDiskTask(ctx *Context, req *types.DeleteVirtualDisk_Task) soap.HasFault {
 	task := CreateTask(m, "deleteVirtualDisk", func(*Task) (types.AnyType, types.BaseMethodFault) {
 		fm := Map.FileManager()


Test patch:
diff --git a/simulator/virtual_disk_manager_test.go b/simulator/virtual_disk_manager_test.go
index 9d392f96..fa0c2c9d 100644
--- a/simulator/virtual_disk_manager_test.go
+++ b/simulator/virtual_disk_manager_test.go
@@ -86,6 +86,32 @@ func TestVirtualDiskManager(t *testing.T) {
 	}
 
 	qname := name
+	for i, fail := range []bool{false, true, true} {
+		if i == 1 {
+			spec.CapacityKb = 0
+		}
+
+		if i == 2 {
+			qname += "_missing_file"
+		}
+		task, err := dm.ExtendVirtualDisk(ctx, qname, nil, spec.CapacityKb*2, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		err = task.Wait(ctx)
+		if fail {
+			if err == nil {
+				t.Error("expected error")
+			}
+		} else {
+			if err != nil {
+				t.Error(err)
+			}
+		}
+	}
+
+	qname = name
 	for _, fail := range []bool{false, true} {
 		id, err := dm.QueryVirtualDiskUuid(ctx, qname, nil)
 		if fail {