From 2d16e19f266383d7b7bc80194478465672c4fd0b Mon Sep 17 00:00:00 2001 From: akutz Date: Wed, 3 Jul 2024 14:59:06 -0500 Subject: [PATCH] api: GetVirtualDiskInfoByUUID for getting capacity/size info This patch introduces the helper function GetVirtualDiskInfoByUUID for getting the capacity and size information for a virtual disk with a provided UUID. --- vmdk/disk_info.go | 164 ++++++++++++++ vmdk/disk_info_test.go | 480 +++++++++++++++++++++++++++++++++++++++++ vmdk/import.go | 20 +- vmdk/import_test.go | 18 +- 4 files changed, 664 insertions(+), 18 deletions(-) create mode 100644 vmdk/disk_info.go create mode 100644 vmdk/disk_info_test.go diff --git a/vmdk/disk_info.go b/vmdk/disk_info.go new file mode 100644 index 000000000..d33880fc2 --- /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/disk_info_test.go b/vmdk/disk_info_test.go new file mode 100644 index 000000000..7f64d51d6 --- /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.go b/vmdk/import.go index 652c008b3..9354a5537 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 = ` ` -// 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 } diff --git a/vmdk/import_test.go b/vmdk/import_test.go index a8f50a624..313fadb50 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) } }