From 116ba7e89fd684ded3e450ea220d4d37b9d7d6a9 Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Tue, 21 Jan 2025 20:46:41 -0800 Subject: [PATCH] api: add vlsm.GlobalObjectManager.List method Signed-off-by: Doug MacEachern --- cli/disk/manager.go | 32 +--- vslm/global_object_manager.go | 35 +++++ vslm/global_object_manager_test.go | 228 +++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 28 deletions(-) create mode 100644 vslm/global_object_manager_test.go diff --git a/cli/disk/manager.go b/cli/disk/manager.go index 6d15cf986..939c8bdba 100644 --- a/cli/disk/manager.go +++ b/cli/disk/manager.go @@ -144,36 +144,12 @@ func (m *Manager) List(ctx context.Context, qs ...vslmtypes.VslmVsoVStorageObjec return m.ObjectManager.List(ctx, m.Datastore) } - // TODO: move this logic to vslm.GlobalObjectManager - // Need to better understand the QuerySpec + implement in vcsim. - // For now we just want the complete list of IDs (govc disk.ls) - maxResults := int32(100) - - spec := vslmtypes.VslmVsoVStorageObjectQuerySpec{ - QueryField: string(vslmtypes.VslmVsoVStorageObjectQuerySpecQueryFieldEnumId), - QueryOperator: string(vslmtypes.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumGreaterThan), - } - query := qs - var ids []types.ID - - for { - res, err := m.GlobalObjectManager.ListObjectsForSpec(ctx, query, maxResults) - if err != nil { - return nil, err - } - - ids = append(ids, res.Id...) - - if res.AllRecordsReturned || len(ids) == 0 { - break - } - - spec.QueryValue = []string{ids[len(ids)-1].Id} - - query = append(qs, spec) + res, err := m.GlobalObjectManager.List(ctx, qs...) + if err != nil { + return nil, err } - return ids, nil + return res.Id, nil } func (m *Manager) RegisterDisk(ctx context.Context, path, name string) (*types.VStorageObject, error) { diff --git a/vslm/global_object_manager.go b/vslm/global_object_manager.go index 66b5e700c..7632944bb 100644 --- a/vslm/global_object_manager.go +++ b/vslm/global_object_manager.go @@ -302,6 +302,41 @@ func (this *GlobalObjectManager) ListObjectsForSpec(ctx context.Context, query [ return res.Returnval, nil } +var DefaultMaxResults = 100 + +// List wraps ListObjectsForSpec, using maxResult = DefaultMaxResults +// and looping until AllRecordsReturned == true or error is returned. +func (this *GlobalObjectManager) List(ctx context.Context, qs ...types.VslmVsoVStorageObjectQuerySpec) (*types.VslmVsoVStorageObjectQueryResult, error) { + var res types.VslmVsoVStorageObjectQueryResult + + query := qs + + for { + page, err := this.ListObjectsForSpec(ctx, query, int32(DefaultMaxResults)) + if err != nil { + return nil, err + } + + res.Id = append(res.Id, page.Id...) + res.QueryResults = append(res.QueryResults, page.QueryResults...) + res.AllRecordsReturned = page.AllRecordsReturned + + if page.AllRecordsReturned || len(page.Id) == 0 { + break + } + + spec := types.VslmVsoVStorageObjectQuerySpec{ + QueryField: string(types.VslmVsoVStorageObjectQuerySpecQueryFieldEnumId), + QueryOperator: string(types.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumGreaterThan), + QueryValue: []string{page.Id[len(page.Id)-1].Id}, + } + + query = append(qs, spec) + } + + return &res, nil +} + func (this *GlobalObjectManager) Clone(ctx context.Context, id vim.ID, spec vim.VslmCloneSpec) (*Task, error) { req := types.VslmCloneVStorageObject_Task{ This: this.Reference(), diff --git a/vslm/global_object_manager_test.go b/vslm/global_object_manager_test.go new file mode 100644 index 000000000..232e86792 --- /dev/null +++ b/vslm/global_object_manager_test.go @@ -0,0 +1,228 @@ +// © Broadcom. All Rights Reserved. +// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. +// SPDX-License-Identifier: Apache-2.0 + +package vslm_test + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/types" + "github.com/vmware/govmomi/vslm" + _ "github.com/vmware/govmomi/vslm/simulator" + vso "github.com/vmware/govmomi/vslm/types" +) + +func TestList(t *testing.T) { + model := simulator.VPX() + model.Datastore = 4 + + now := time.Now().Format(time.RFC3339Nano) + + simulator.Test(func(ctx context.Context, vc *vim25.Client) { + datastores, err := find.NewFinder(vc).DatastoreList(ctx, "*") + if err != nil { + t.Fatal(err) + } + + c, err := vslm.NewClient(ctx, vc) + if err != nil { + t.Fatal(err) + } + + m := vslm.NewGlobalObjectManager(c) + + namespaces := []string{"foo", "bar", "baz"} + + dsIDs := make([]string, len(datastores)) + + for i, ds := range datastores { + dsIDs[i] = strings.SplitN(ds.Reference().Value, "-", 2)[1] + + for j, ns := range namespaces { + spec := types.VslmCreateSpec{ + Name: fmt.Sprintf("%s-disk-%d", ns, i+j), + CapacityInMB: int64(i+j) * 10, + BackingSpec: &types.VslmCreateSpecDiskFileBackingSpec{ + VslmCreateSpecBackingSpec: types.VslmCreateSpecBackingSpec{ + Datastore: ds.Reference(), + }, + }, + } + t.Logf("CreateDisk %s (%dMB) on datastore-%s", spec.Name, spec.CapacityInMB, dsIDs[i]) + task, err := m.CreateDisk(ctx, spec) + if err != nil { + t.Fatal(err) + } + disk, err := task.Wait(ctx, time.Hour) + if err != nil { + t.Fatal(err) + } + + id := disk.(types.VStorageObject).Config.Id + + metadata := []types.KeyValue{ + {Key: "namespace", Value: ns}, + {Key: "name", Value: spec.Name}, + } + + task, err = m.UpdateMetadata(ctx, id, metadata, nil) + if err != nil { + t.Fatal(err) + } + _, err = task.Wait(ctx, time.Hour) + if err != nil { + t.Fatal(err) + } + } + } + + tests := []struct { + expect int + query []vso.VslmVsoVStorageObjectQuerySpec + }{ + {model.Datastore * len(namespaces), nil}, + {-1, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: "invalid", + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumEquals), + QueryValue: []string{"any"}, + }}}, + {-1, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: "invalid", + QueryValue: []string{"any"}, + }}}, + {-1, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumEquals), + QueryValue: nil, + }}}, + {-1, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumEquals), + QueryValue: []string{"one", "two"}, + }}}, + {-1, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumCapacity), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumStartsWith), + QueryValue: []string{"10"}, + }}}, + {-1, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumCapacity), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumGreaterThan), + QueryValue: []string{"ten"}, + }}}, + {3, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumCapacity), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumGreaterThan), + QueryValue: []string{"30"}, + }}}, + {0, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumCapacity), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumGreaterThan), + QueryValue: []string{"5000"}, + }}}, + {len(namespaces), []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumDatastoreMoId), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumEquals), + QueryValue: []string{dsIDs[0]}, + }}}, + {model.Datastore, []vso.VslmVsoVStorageObjectQuerySpec{ + { + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumMetadataKey), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumEquals), + QueryValue: []string{"namespace"}, + }, + { + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumMetadataValue), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumEquals), + QueryValue: []string{namespaces[0]}, + }, + }}, + {model.Datastore, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumStartsWith), + QueryValue: []string{namespaces[1]}, + }}}, + {model.Datastore, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumStartsWith), + QueryValue: []string{namespaces[1]}, + }}}, + {model.Datastore * len(namespaces), []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumContains), + QueryValue: []string{"disk"}, + }}}, + {0, []vso.VslmVsoVStorageObjectQuerySpec{ + { + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumStartsWith), + QueryValue: []string{namespaces[0]}, + }, + { + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumName), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumStartsWith), + QueryValue: []string{namespaces[1]}, + }, + }}, + {model.Datastore * len(namespaces), []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumCreateTime), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumGreaterThan), + QueryValue: []string{now}, + }}}, + {0, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumCreateTime), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumLessThan), + QueryValue: []string{now}, + }}}, + {-1, []vso.VslmVsoVStorageObjectQuerySpec{{ + QueryField: string(vso.VslmVsoVStorageObjectQuerySpecQueryFieldEnumCreateTime), + QueryOperator: string(vso.VslmVsoVStorageObjectQuerySpecQueryOperatorEnumContains), + QueryValue: []string{now}, + }}}, + } + + for _, test := range tests { + if test.expect > 2 { + vslm.DefaultMaxResults = test.expect / 2 // test pagination + } + t.Run(queryString(test.query), func(t *testing.T) { + res, err := m.List(ctx, test.query...) + if test.expect == -1 { + if err == nil { + t.Error("expected error") + } + } else { + if err != nil { + t.Fatal(err) + } + + if len(res.Id) != test.expect { + t.Errorf("expected %d, got: %d", test.expect, len(res.Id)) + } + } + }) + } + }, model) +} + +func queryString(query []vso.VslmVsoVStorageObjectQuerySpec) string { + if query == nil { + return "no query" + } + res := make([]string, len(query)) + for i, q := range query { + res[i] = fmt.Sprintf("%s.%s=%s", + q.QueryField, q.QueryOperator, + strings.Join(q.QueryValue, ",")) + } + return strings.Join(res, " and ") +}