Repository: vmware/govmomi
Pull request #3482 (2024-07-03 19:57:23)

api: GetVirtualDiskInfoByUUID for getting capacity/size info

## Description

This patch introduces the helper function GetVirtualDiskInfoByUUID for getting the capacity and size information for a virtual disk with a provided UUID.

Closes: `NA`

## 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?

- [x] Ran `go test` which produces ~~almost~~ 100% code coverage ~~(working with @dougm to figure out how to spoof the error in the property collector)~~ (thanks @dougm!):

    ```shell
    $ go test -v -count 1 -run 'TestGetVirtualDiskInfoByUUID' ./vmdk
    === RUN   TestGetVirtualDiskInfoByUUID
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/diskUUID_is_empty
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/no_matching_disks
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskFlatVer2BackingInfo
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskSeSparseBackingInfo
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskRawDiskMappingVer1BackingInfo
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskSparseVer2BackingInfo
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskRawDiskVer2BackingInfo
    === RUN   TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_multiple_chain_entries
    === RUN   TestGetVirtualDiskInfoByUUID/fetch_properties
    === RUN   TestGetVirtualDiskInfoByUUID/fetch_properties/ctx_is_nil
    === RUN   TestGetVirtualDiskInfoByUUID/fetch_properties/client_is_nil
    === RUN   TestGetVirtualDiskInfoByUUID/fetch_properties/failed_to_retrieve_properties
    === RUN   TestGetVirtualDiskInfoByUUID/fetch_properties/fetchProperties_is_false_but_cached_properties_are_missing
    === RUN   TestGetVirtualDiskInfoByUUID/fetch_properties/fetchProperties_is_true_and_cached_properties_are_stale
    --- PASS: TestGetVirtualDiskInfoByUUID (0.41s)
        --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/diskUUID_is_empty (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/no_matching_disks (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskFlatVer2BackingInfo (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskSeSparseBackingInfo (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskRawDiskMappingVer1BackingInfo (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskSparseVer2BackingInfo (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_VirtualDiskRawDiskVer2BackingInfo (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/w_cached_properties/one_disk_w_multiple_chain_entries (0.00s)
        --- PASS: TestGetVirtualDiskInfoByUUID/fetch_properties (0.41s)
            --- PASS: TestGetVirtualDiskInfoByUUID/fetch_properties/ctx_is_nil (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/fetch_properties/client_is_nil (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/fetch_properties/failed_to_retrieve_properties (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/fetch_properties/fetchProperties_is_false_but_cached_properties_are_missing (0.00s)
            --- PASS: TestGetVirtualDiskInfoByUUID/fetch_properties/fetchProperties_is_true_and_cached_properties_are_stale (0.00s)
    PASS
    ok  	github.com/vmware/govmomi/vmdk	0.713s
    ```

<img width="518" alt="image" src="https://github.com/vmware/govmomi/assets/101085/21688c3d-d83a-4028-adf5-aef9e6a16d3d">
<img width="518" alt="image" src="https://github.com/vmware/govmomi/assets/101085/92ddc586-8506-4b7a-9c8d-0b0a872d6b1a">
<img width="518" alt="image" src="https://github.com/vmware/govmomi/assets/101085/723aa57e-319c-4996-b79a-85dde7827e9a">



## 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/gen/vim_wsdl.rb b/gen/vim_wsdl.rb
index 89745c6d..9aa16a02 100644
--- a/gen/vim_wsdl.rb
+++ b/gen/vim_wsdl.rb
@@ -262,7 +262,7 @@ class Simple
 
   def pointer_type?
     ["UnitNumber"].include?(var_name) or
-      optional? && ["CoresPerNumaNode", "IpPoolId", "OwnerId", "GroupId", "MaxWaitSeconds", "Reservation", "Limit", "OverheadLimit", "ResourceReductionToToleratePercent"].include?(var_name)
+      optional? && ["IpPoolId", "OwnerId", "GroupId", "MaxWaitSeconds", "Reservation", "Limit", "OverheadLimit", "ResourceReductionToToleratePercent"].include?(var_name)
   end
 
   def var_type
diff --git a/vim25/types/types.go b/vim25/types/types.go
index 43f8c0a9..0f8dab11 100644
--- a/vim25/types/types.go
+++ b/vim25/types/types.go
@@ -91361,7 +91361,7 @@ type VirtualMachineVirtualNuma struct {
 	// vNUMA node.
 	// If set to be non zero, VM uses the value as vNUMA node size.
 	// If unset, the VM continue to follow the behavior in last poweron.
-	CoresPerNumaNode *int32 `xml:"coresPerNumaNode" json:"coresPerNumaNode,omitempty"`
+	CoresPerNumaNode int32 `xml:"coresPerNumaNode,omitempty" json:"coresPerNumaNode,omitempty"`
 	// Capability to expose virtual NUMA when CPU hotadd is enabled.
 	//
 	// If set to true, ESXi will consider exposing virtual NUMA to
@@ -91389,7 +91389,7 @@ type VirtualMachineVirtualNumaInfo struct {
 	// field should be ignored.
 	// In other cases, this field represents the virtual NUMA node size
 	// seen by the guest.
-	CoresPerNumaNode *int32 `xml:"coresPerNumaNode" json:"coresPerNumaNode,omitempty"`
+	CoresPerNumaNode int32 `xml:"coresPerNumaNode,omitempty" json:"coresPerNumaNode,omitempty"`
 	// Whether coresPerNode is determined automatically.
 	AutoCoresPerNumaNode *bool `xml:"autoCoresPerNumaNode" json:"autoCoresPerNumaNode,omitempty"`
 	// Whether virtual NUMA topology is exposed when CPU hotadd is
diff --git a/vmdk/disk_info.go b/vmdk/disk_info.go
new file mode 100644
index 00000000..d33880fc
--- /dev/null
+++ b/vmdk/disk_info.go
@@ -0,0 +1,164 @@
+/*
+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 vmdk
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type VirtualDiskInfo struct {
+	CapacityInBytes int64
+	DeviceKey       int32
+	FileName        string
+	Size            int64
+	UniqueSize      int64
+}
+
+// GetVirtualDiskInfoByUUID returns information about a virtual disk identified
+// by the provided UUID. This method is valid for the following backing types:
+//
+// - VirtualDiskFlatVer2BackingInfo
+// - VirtualDiskSeSparseBackingInfo
+// - VirtualDiskRawDiskMappingVer1BackingInfo
+// - VirtualDiskSparseVer2BackingInfo
+// - VirtualDiskRawDiskVer2BackingInfo
+//
+// These are the only backing types that have a Uuid property for comparing the
+// provided value.
+func GetVirtualDiskInfoByUUID(
+	ctx context.Context,
+	client *vim25.Client,
+	mo mo.VirtualMachine,
+	fetchProperties bool,
+	diskUUID string) (VirtualDiskInfo, error) {
+
+	if diskUUID == "" {
+		return VirtualDiskInfo{}, fmt.Errorf("diskUUID is empty")
+	}
+
+	switch {
+	case fetchProperties,
+		mo.Config == nil,
+		mo.Config.Hardware.Device == nil,
+		mo.LayoutEx == nil,
+		mo.LayoutEx.Disk == nil,
+		mo.LayoutEx.File == nil:
+
+		if ctx == nil {
+			return VirtualDiskInfo{}, fmt.Errorf("ctx is nil")
+		}
+		if client == nil {
+			return VirtualDiskInfo{}, fmt.Errorf("client is nil")
+		}
+
+		obj := object.NewVirtualMachine(client, mo.Self)
+
+		if err := obj.Properties(
+			ctx,
+			mo.Self,
+			[]string{"config", "layoutEx"},
+			&mo); err != nil {
+
+			return VirtualDiskInfo{},
+				fmt.Errorf("failed to retrieve properties %w", err)
+		}
+	}
+
+	// Find the disk by UUID by inspecting all of the disk backing types that
+	// can have an associated UUID.
+	var (
+		disk     *types.VirtualDisk
+		fileName string
+	)
+	for i := range mo.Config.Hardware.Device {
+		switch tvd := mo.Config.Hardware.Device[i].(type) {
+		case *types.VirtualDisk:
+			switch tb := tvd.Backing.(type) {
+			case *types.VirtualDiskFlatVer2BackingInfo:
+				if tb.Uuid == diskUUID {
+					disk = tvd
+					fileName = tb.FileName
+				}
+			case *types.VirtualDiskSeSparseBackingInfo:
+				if tb.Uuid == diskUUID {
+					disk = tvd
+					fileName = tb.FileName
+				}
+			case *types.VirtualDiskRawDiskMappingVer1BackingInfo:
+				if tb.Uuid == diskUUID {
+					disk = tvd
+					fileName = tb.FileName
+				}
+			case *types.VirtualDiskSparseVer2BackingInfo:
+				if tb.Uuid == diskUUID {
+					disk = tvd
+					fileName = tb.FileName
+				}
+			case *types.VirtualDiskRawDiskVer2BackingInfo:
+				if tb.Uuid == diskUUID {
+					disk = tvd
+					fileName = tb.DescriptorFileName
+				}
+			}
+		}
+	}
+
+	if disk == nil {
+		return VirtualDiskInfo{},
+			fmt.Errorf("disk not found with uuid %q", diskUUID)
+	}
+
+	// Build a lookup table for determining if file key belongs to this disk
+	// chain.
+	diskFileKeys := map[int32]struct{}{}
+	for i := range mo.LayoutEx.Disk {
+		if d := mo.LayoutEx.Disk[i]; d.Key == disk.Key {
+			for j := range d.Chain {
+				for k := range d.Chain[j].FileKey {
+					diskFileKeys[d.Chain[j].FileKey[k]] = struct{}{}
+				}
+			}
+		}
+	}
+
+	// Sum the disk's total size and unique size.
+	var (
+		size       int64
+		uniqueSize int64
+	)
+	for i := range mo.LayoutEx.File {
+		f := mo.LayoutEx.File[i]
+		if _, ok := diskFileKeys[f.Key]; ok {
+			size += f.Size
+			uniqueSize += f.UniqueSize
+		}
+	}
+
+	return VirtualDiskInfo{
+		CapacityInBytes: disk.CapacityInBytes,
+		DeviceKey:       disk.Key,
+		FileName:        fileName,
+		Size:            size,
+		UniqueSize:      uniqueSize,
+	}, nil
+}
diff --git a/vmdk/import.go b/vmdk/import.go
index 652c008b..9354a553 100644
--- a/vmdk/import.go
+++ b/vmdk/import.go
@@ -10,7 +10,7 @@ You may obtain a copy of the License at
 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.
-nSee the License for the specific language governing permissions and
+See the License for the specific language governing permissions and
 limitations under the License.
 */
 
@@ -41,8 +41,8 @@ var (
 	ErrInvalidFormat = errors.New("vmdk: invalid format (must be streamOptimized)")
 )
 
-// info is used to inspect a vmdk and generate an ovf template
-type info struct {
+// Info is used to inspect a vmdk and generate an ovf template
+type Info struct {
 	Header struct {
 		MagicNumber uint32
 		Version     uint32
@@ -56,15 +56,15 @@ type info struct {
 	ImportName string
 }
 
-// stat looks at the vmdk header to make sure the format is streamOptimized and
+// Stat looks at the vmdk header to make sure the format is streamOptimized and
 // extracts the disk capacity required to properly generate the ovf descriptor.
-func stat(name string) (*info, error) {
+func Stat(name string) (*Info, error) {
 	f, err := os.Open(filepath.Clean(name))
 	if err != nil {
 		return nil, err
 	}
 
-	var di info
+	var di Info
 
 	var buf bytes.Buffer
 
@@ -174,8 +174,8 @@ var ovfenv = `<?xml version="1.0" encoding="UTF-8"?>
   </VirtualSystem>
 </Envelope>`
 
-// ovf returns an expanded descriptor template
-func (di *info) ovf() (string, error) {
+// OVF returns an expanded descriptor template
+func (di *Info) OVF() (string, error) {
 	var buf bytes.Buffer
 
 	tmpl, err := template.New("ovf").Parse(ovfenv)
@@ -209,7 +209,7 @@ func Import(ctx context.Context, c *vim25.Client, name string, datastore *object
 	m := ovf.NewManager(c)
 	fm := datastore.NewFileManager(p.Datacenter, p.Force)
 
-	disk, err := stat(name)
+	disk, err := Stat(name)
 	if err != nil {
 		return err
 	}
@@ -245,7 +245,7 @@ func Import(ctx context.Context, c *vim25.Client, name string, datastore *object
 	}
 
 	// Expand the ovf template
-	descriptor, err := disk.ovf()
+	descriptor, err := disk.OVF()
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/vmdk/disk_info_test.go b/vmdk/disk_info_test.go
new file mode 100644
index 00000000..7f64d51d
--- /dev/null
+++ b/vmdk/disk_info_test.go
@@ -0,0 +1,480 @@
+/*
+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 vmdk_test
+
+import (
+	"bytes"
+	"context"
+	"os"
+	"path"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/vmware/govmomi/find"
+	"github.com/vmware/govmomi/object"
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/types"
+	"github.com/vmware/govmomi/vmdk"
+)
+
+func TestGetVirtualDiskInfoByUUID(t *testing.T) {
+
+	type testCase struct {
+		name            string
+		ctx             context.Context
+		client          *vim25.Client
+		mo              mo.VirtualMachine
+		fetchProperties bool
+		diskUUID        string
+		diskInfo        vmdk.VirtualDiskInfo
+		err             string
+	}
+
+	t.Run("w cached properties", func(t *testing.T) {
+
+		const (
+			deviceKey     = 1000
+			diskUUID      = "123"
+			fileName      = "[datastore] path/to.vmdk"
+			tenGiBInBytes = 10 * 1024 * 1024 * 1024
+		)
+
+		getDisk := func(backing types.BaseVirtualDeviceBackingInfo) *types.VirtualDisk {
+			return &types.VirtualDisk{
+				VirtualDevice: types.VirtualDevice{
+					Key:     deviceKey,
+					Backing: backing,
+				},
+				CapacityInBytes: tenGiBInBytes,
+			}
+		}
+
+		getDiskInfo := func() vmdk.VirtualDiskInfo {
+			return vmdk.VirtualDiskInfo{
+				CapacityInBytes: tenGiBInBytes,
+				DeviceKey:       deviceKey,
+				FileName:        fileName,
+				Size:            (1 * 1024 * 1024 * 1024) + 950,
+				UniqueSize:      (5 * 1024 * 1024) + 100,
+			}
+		}
+
+		getLayoutEx := func() *types.VirtualMachineFileLayoutEx {
+			return &types.VirtualMachineFileLayoutEx{
+				Disk: []types.VirtualMachineFileLayoutExDiskLayout{
+					{
+						Key: 1000,
+						Chain: []types.VirtualMachineFileLayoutExDiskUnit{
+							{
+								FileKey: []int32{
+									4,
+									5,
+								},
+							},
+						},
+					},
+				},
+				File: []types.VirtualMachineFileLayoutExFileInfo{
+					{
+						Key:        4,
+						Size:       1 * 1024 * 1024 * 1024, // 1 GiB
+						UniqueSize: 5 * 1024 * 1024,        // 500 MiB
+					},
+					{
+						Key:        5,
+						Size:       950,
+						UniqueSize: 100,
+					},
+				},
+			}
+		}
+
+		testCases := []testCase{
+			{
+				name: "diskUUID is empty",
+				err:  "diskUUID is empty",
+			},
+			{
+				name: "no matching disks",
+				mo: mo.VirtualMachine{
+					Config: &types.VirtualMachineConfigInfo{
+						Hardware: types.VirtualHardware{
+							Device: []types.BaseVirtualDevice{},
+						},
+					},
+					LayoutEx: &types.VirtualMachineFileLayoutEx{
+						File: []types.VirtualMachineFileLayoutExFileInfo{},
+						Disk: []types.VirtualMachineFileLayoutExDiskLayout{},
+					},
+				},
+				diskUUID: diskUUID,
+				err:      "disk not found with uuid \"123\"",
+			},
+			{
+				name: "one disk w VirtualDiskFlatVer2BackingInfo",
+				mo: mo.VirtualMachine{
+					Config: &types.VirtualMachineConfigInfo{
+						Hardware: types.VirtualHardware{
+							Device: []types.BaseVirtualDevice{
+								getDisk(&types.VirtualDiskFlatVer2BackingInfo{
+									VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
+										FileName: fileName,
+									},
+									Uuid: diskUUID,
+								}),
+							},
+						},
+					},
+					LayoutEx: getLayoutEx(),
+				},
+				diskUUID: diskUUID,
+				diskInfo: getDiskInfo(),
+			},
+			{
+				name: "one disk w VirtualDiskSeSparseBackingInfo",
+				mo: mo.VirtualMachine{
+					Config: &types.VirtualMachineConfigInfo{
+						Hardware: types.VirtualHardware{
+							Device: []types.BaseVirtualDevice{
+								getDisk(&types.VirtualDiskSeSparseBackingInfo{
+									VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
+										FileName: fileName,
+									},
+									Uuid: diskUUID,
+								}),
+							},
+						},
+					},
+					LayoutEx: getLayoutEx(),
+				},
+				diskUUID: diskUUID,
+				diskInfo: getDiskInfo(),
+			},
+			{
+				name: "one disk w VirtualDiskRawDiskMappingVer1BackingInfo",
+				mo: mo.VirtualMachine{
+					Config: &types.VirtualMachineConfigInfo{
+						Hardware: types.VirtualHardware{
+							Device: []types.BaseVirtualDevice{
+								getDisk(&types.VirtualDiskRawDiskMappingVer1BackingInfo{
+									VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
+										FileName: fileName,
+									},
+									Uuid: diskUUID,
+								}),
+							},
+						},
+					},
+					LayoutEx: getLayoutEx(),
+				},
+				diskUUID: diskUUID,
+				diskInfo: getDiskInfo(),
+			},
+			{
+				name: "one disk w VirtualDiskSparseVer2BackingInfo",
+				mo: mo.VirtualMachine{
+					Config: &types.VirtualMachineConfigInfo{
+						Hardware: types.VirtualHardware{
+							Device: []types.BaseVirtualDevice{
+								getDisk(&types.VirtualDiskSparseVer2BackingInfo{
+									VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
+										FileName: fileName,
+									},
+									Uuid: diskUUID,
+								}),
+							},
+						},
+					},
+					LayoutEx: getLayoutEx(),
+				},
+				diskUUID: diskUUID,
+				diskInfo: getDiskInfo(),
+			},
+			{
+				name: "one disk w VirtualDiskRawDiskVer2BackingInfo",
+				mo: mo.VirtualMachine{
+					Config: &types.VirtualMachineConfigInfo{
+						Hardware: types.VirtualHardware{
+							Device: []types.BaseVirtualDevice{
+								getDisk(&types.VirtualDiskRawDiskVer2BackingInfo{
+									DescriptorFileName: fileName,
+									Uuid:               diskUUID,
+								}),
+							},
+						},
+					},
+					LayoutEx: getLayoutEx(),
+				},
+				diskUUID: diskUUID,
+				diskInfo: getDiskInfo(),
+			},
+			{
+				name: "one disk w multiple chain entries",
+				mo: mo.VirtualMachine{
+					Config: &types.VirtualMachineConfigInfo{
+						Hardware: types.VirtualHardware{
+							Device: []types.BaseVirtualDevice{
+								getDisk(&types.VirtualDiskFlatVer2BackingInfo{
+									VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
+										FileName: fileName,
+									},
+									Uuid: diskUUID,
+								}),
+							},
+						},
+					},
+					LayoutEx: &types.VirtualMachineFileLayoutEx{
+						Disk: []types.VirtualMachineFileLayoutExDiskLayout{
+							{
+								Key: deviceKey,
+								Chain: []types.VirtualMachineFileLayoutExDiskUnit{
+									{
+										FileKey: []int32{
+											4,
+											5,
+										},
+									},
+									{
+										FileKey: []int32{
+											6,
+											7,
+										},
+									},
+									{
+										FileKey: []int32{
+											8,
+										},
+									},
+								},
+							},
+						},
+						File: []types.VirtualMachineFileLayoutExFileInfo{
+							{
+								Key:        4,
+								Size:       1 * 1024 * 1024 * 1024, // 1 GiB
+								UniqueSize: 5 * 1024 * 1024,        // 500 MiB
+							},
+							{
+								Key:        5,
+								Size:       950,
+								UniqueSize: 100,
+							},
+							{
+								Key:        6,
+								Size:       500,
+								UniqueSize: 100,
+							},
+							{
+								Key:        7,
+								Size:       500,
+								UniqueSize: 200,
+							},
+							{
+								Key:        8,
+								Size:       1000,
+								UniqueSize: 300,
+							},
+						},
+					},
+				},
+				diskUUID: diskUUID,
+				diskInfo: vmdk.VirtualDiskInfo{
+					CapacityInBytes: tenGiBInBytes,
+					DeviceKey:       deviceKey,
+					FileName:        fileName,
+					Size:            (1 * 1024 * 1024 * 1024) + 950 + 500 + 500 + 1000,
+					UniqueSize:      (5 * 1024 * 1024) + 100 + 100 + 200 + 300,
+				},
+			},
+		}
+
+		for i := range testCases {
+			tc := testCases[i]
+			t.Run(tc.name, func(t *testing.T) {
+				dii, err := vmdk.GetVirtualDiskInfoByUUID(
+					nil, nil, tc.mo, false, tc.diskUUID)
+
+				if tc.err != "" {
+					assert.EqualError(t, err, tc.err)
+				}
+				assert.Equal(t, tc.diskInfo, dii)
+			})
+		}
+	})
+
+	t.Run("fetch properties", func(t *testing.T) {
+		simulator.Test(func(ctx context.Context, c *vim25.Client) {
+
+			finder := find.NewFinder(c, true)
+			datacenter, err := finder.DefaultDatacenter(ctx)
+			if err != nil {
+				t.Fatalf("default datacenter not found: %s", err)
+			}
+			finder.SetDatacenter(datacenter)
+			vmList, err := finder.VirtualMachineList(ctx, "*")
+			if len(vmList) == 0 {
+				t.Fatal("vmList == 0")
+			}
+			vm := vmList[0]
+
+			var moVM mo.VirtualMachine
+			if err := vm.Properties(
+				ctx,
+				vm.Reference(),
+				[]string{"config", "layoutEx"},
+				&moVM); err != nil {
+
+				t.Fatal(err)
+			}
+
+			devs := object.VirtualDeviceList(moVM.Config.Hardware.Device)
+			disks := devs.SelectByType(&types.VirtualDisk{})
+			if len(disks) == 0 {
+				t.Fatal("disks == 0")
+			}
+
+			var (
+				diskUUID     string
+				datastoreRef types.ManagedObjectReference
+				disk         = disks[0].(*types.VirtualDisk)
+				diskBacking  = disk.Backing
+				diskInfo     = vmdk.VirtualDiskInfo{
+					CapacityInBytes: disk.CapacityInBytes,
+					DeviceKey:       disk.Key,
+				}
+			)
+
+			switch tb := disk.Backing.(type) {
+			case *types.VirtualDiskFlatVer2BackingInfo:
+				diskUUID = tb.Uuid
+				diskInfo.FileName = tb.FileName
+				datastoreRef = *tb.Datastore
+			default:
+				t.Fatalf("unsupported disk backing: %T", disk.Backing)
+			}
+
+			datastore := object.NewDatastore(c, datastoreRef)
+			var moDatastore mo.Datastore
+			if err := datastore.Properties(
+				ctx,
+				datastore.Reference(),
+				[]string{"info"},
+				&moDatastore); err != nil {
+
+				t.Fatal(err)
+			}
+
+			datastorePath := moDatastore.Info.GetDatastoreInfo().Url
+			var diskPath object.DatastorePath
+			if !diskPath.FromString(diskInfo.FileName) {
+				t.Fatalf("invalid disk file name: %q", diskInfo.FileName)
+			}
+
+			const vmdkSize = 500
+
+			assert.NoError(t, os.WriteFile(
+				path.Join(datastorePath, diskPath.Path),
+				bytes.Repeat([]byte{1}, vmdkSize),
+				os.ModeAppend))
+			assert.NoError(t, vm.RefreshStorageInfo(ctx))
+
+			diskInfo.Size = vmdkSize
+			diskInfo.UniqueSize = vmdkSize
+
+			testCases := []testCase{
+				{
+					name:     "ctx is nil",
+					ctx:      nil,
+					client:   c,
+					diskUUID: diskUUID,
+					err:      "ctx is nil",
+				},
+				{
+					name:     "client is nil",
+					ctx:      context.Background(),
+					client:   nil,
+					diskUUID: diskUUID,
+					err:      "client is nil",
+				},
+				{
+					name:            "fetchProperties is false but cached properties are missing",
+					ctx:             context.Background(),
+					client:          c,
+					diskUUID:        diskUUID,
+					diskInfo:        diskInfo,
+					fetchProperties: false,
+					mo: mo.VirtualMachine{
+						ManagedEntity: mo.ManagedEntity{
+							ExtensibleManagedObject: mo.ExtensibleManagedObject{
+								Self: vm.Reference(),
+							},
+						},
+					},
+				},
+				{
+					name:            "fetchProperties is true and cached properties are stale",
+					ctx:             context.Background(),
+					client:          c,
+					diskUUID:        diskUUID,
+					diskInfo:        diskInfo,
+					fetchProperties: true,
+					mo: mo.VirtualMachine{
+						ManagedEntity: mo.ManagedEntity{
+							ExtensibleManagedObject: mo.ExtensibleManagedObject{
+								Self: vm.Reference(),
+							},
+						},
+						Config: &types.VirtualMachineConfigInfo{
+							Hardware: types.VirtualHardware{
+								Device: []types.BaseVirtualDevice{
+									&types.VirtualDisk{
+										VirtualDevice: types.VirtualDevice{
+											Key:     diskInfo.DeviceKey,
+											Backing: diskBacking,
+										},
+										CapacityInBytes: 500,
+									},
+								},
+							},
+						},
+					},
+				},
+			}
+
+			for i := range testCases {
+				tc := testCases[i]
+				t.Run(tc.name, func(t *testing.T) {
+					dii, err := vmdk.GetVirtualDiskInfoByUUID(
+						tc.ctx,
+						tc.client,
+						tc.mo,
+						tc.fetchProperties,
+						tc.diskUUID)
+
+					if tc.err != "" {
+						assert.EqualError(t, err, tc.err)
+					}
+					assert.Equal(t, tc.diskInfo, dii)
+				})
+			}
+
+		})
+	})
+}
diff --git a/vmdk/import_test.go b/vmdk/import_test.go
index a8f50a62..313fadb5 100644
--- a/vmdk/import_test.go
+++ b/vmdk/import_test.go
@@ -1,28 +1,30 @@
 /*
-Copyright (c) 2017 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2024 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-    http://www.apache.org/licenses/LICENSE-2.0
+http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-nSee the License for the specific language governing permissions and
+See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package vmdk
+package vmdk_test
 
 import (
 	"os"
 	"testing"
+
+	"github.com/vmware/govmomi/vmdk"
 )
 
 func TestDiskInfo(t *testing.T) {
-	di, err := stat("../govc/test/images/ttylinux-pc_i486-16.1-disk1.vmdk")
+	di, err := vmdk.Stat("../govc/test/images/ttylinux-pc_i486-16.1-disk1.vmdk")
 	if err != nil {
 		if os.IsNotExist(err) {
 			t.SkipNow()
@@ -30,15 +32,15 @@ func TestDiskInfo(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	_, err = di.ovf()
+	_, err = di.OVF()
 	if err != nil {
 		t.Fatal(err)
 	}
 }
 
 func TestDiskInvalid(t *testing.T) {
-	_, err := stat("import_test.go")
-	if err != ErrInvalidFormat {
+	_, err := vmdk.Stat("import_test.go")
+	if err != vmdk.ErrInvalidFormat {
 		t.Errorf("expected ErrInvalidFormat: %s", err)
 	}
 }