diff --git a/internal/async/async.go b/internal/async/async.go new file mode 100644 index 0000000..c35929d --- /dev/null +++ b/internal/async/async.go @@ -0,0 +1,166 @@ +package async + +import ( + "context" + "errors" + "time" + + "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/client" + "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/sdk/async/dataservices" +) + +type Operation struct { + ctx context.Context + client client.PCBeClient + operationID string + associatedResourceType string + opResp dataservices.V1beta1AsyncOperationsItemAsyncOperationsGetResponseable +} + +// New returns a new Operation struct that can be used to poll +// asynchronous operations for any resource type. +func New( + ctx context.Context, + client client.PCBeClient, + OperationID string, + associatedResourceType string, +) Operation { + a := Operation{} + a.ctx = ctx + a.client = client + a.operationID = OperationID + a.associatedResourceType = associatedResourceType + + return a +} + +func (a Operation) GetSourceResourceURI() (string, error) { + if a.opResp == nil { + msg := "GetSourceResourceURI requies a non-nil opResp" + + return "", errors.New(msg) + } + + if a.opResp.GetSourceResourceUri() == nil { + msg := "GetSourceResourceURI requies a non-nil GetSourceResourceUri" + + return "", errors.New(msg) + } + + return *a.opResp.GetSourceResourceUri(), nil +} + +func (a Operation) GetAssociatedResourceURI() (string, error) { + if a.opResp == nil { + msg := "GetAssociatedResourceURI requies a non-nil opResp" + + return "", errors.New(msg) + } + + if len(a.opResp.GetAssociatedResources()) != 1 { + msg := "GetAssociatedResourceURI requires exactly one associatedResource" + + return "", errors.New(msg) + + } + + if a.opResp.GetAssociatedResources()[0].GetTypeEscaped() == nil { + msg := "GetAssociatedResourceURI requires a non-nil associatedResource type" + + return "", errors.New(msg) + } + + typeEscaped := *(a.opResp.GetAssociatedResources()[0].GetTypeEscaped()) + if typeEscaped != a.associatedResourceType { + msg := "GetAssociatedResourceURI type mismatch (" + typeEscaped + + " != " + a.associatedResourceType + ")" + + return "", errors.New(msg) + } + + if a.opResp.GetAssociatedResources()[0].GetResourceUri() == nil { + msg := "GetAssociatedResourceURI requires a non-nil associatedResource uri" + + return "", errors.New(msg) + } + + return *(a.opResp.GetAssociatedResources()[0].GetResourceUri()), nil +} + +// Poll waits for an asynchronous operation to complete +// It is not thread safe (but does not currently need to be) +// Typically the operation flow will be: +// +// a := New(...) +// a.Poll() +// uri ... = a.GetAssociatedResourceURI() +func (a *Operation) Poll() error { + maxPolls := int(a.client.Config.MaxPolls) + pollWaitTime := time.Duration(a.client.Config.PollInterval) * time.Second + + asyncClient, err := a.client.NewAsyncClient(a.ctx) + if err != nil { + msg := "error polling async operation " + a.operationID + ": " + + err.Error() + + return errors.New(msg) + } + + var opResp dataservices.V1beta1AsyncOperationsItemAsyncOperationsGetResponseable + arc := dataservices. + V1beta1AsyncOperationsAsyncOperationsItemRequestBuilderGetRequestConfiguration{} + + for count := 1; ; count++ { + opResp, err = asyncClient.DataServices(). + V1beta1(). + AsyncOperations(). + ById(a.operationID). + Get(a.ctx, &arc) + + if err != nil { + msg := "error polling async operation " + a.operationID + ": " + err.Error() + + return errors.New(msg) + } + + if opResp == nil { + msg := "error polling async operation " + a.operationID + ": " + + "nil op response" + + return errors.New(msg) + } + + opRespState := opResp.GetState() + if opRespState == nil { + msg := "error polling async operation " + a.operationID + ": " + + "operation has nil state" + + return errors.New(msg) + } + + // TODO: (API) Use enum not string for state when FF-28181 is fixed + if *opRespState == "FAILED" { + msg := "error polling async operation " + a.operationID + "operation state FAILED" + + return errors.New(msg) + } + + // TODO: (API) Use enum not string for state when FF-28181 is fixed + if *opRespState == "SUCCEEDED" { + a.opResp = opResp + + break + } + + if count == maxPolls { + msg := "error polling async operation " + a.operationID + ": " + + "max polls exceeded" + + return errors.New(msg) + } + + time.Sleep(pollWaitTime) + } + + return nil +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 610c31d..c663726 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -3,10 +3,12 @@ package constants const ( - ProviderType = "hpegl" - ProviderBlock = "pc" - NameFilter = "name eq " - HciClusterUUIDFilter = "hciClusterUuid eq " - AndFilter = " and " + ProviderType = "hpegl" + ProviderBlock = "pc" + NameFilter = "name eq " + HciClusterUUIDFilter = "hciClusterUuid eq " + AndFilter = " and " + TaskHypervisorCluster = "hypervisor-cluster" // task's "associatedResources" string + TaskDatastore = "datastore" // task's "associatedResources" string ) diff --git a/internal/poll/operation.go b/internal/poll/operation.go deleted file mode 100644 index 799421d..0000000 --- a/internal/poll/operation.go +++ /dev/null @@ -1,99 +0,0 @@ -package poll - -import ( - "context" - "time" - - "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/client" - "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/sdk/async/dataservices" - "github.com/hashicorp/terraform-plugin-framework/diag" -) - -// AsyncOperation polls an asynchronous operation until it completes. -// It returns the response of the async operation. -func AsyncOperation( - ctx context.Context, - client client.PCBeClient, - OperationID string, - diagsP *diag.Diagnostics, -) dataservices.V1beta1AsyncOperationsItemAsyncOperationsGetResponseable { - maxPolls := int(client.Config.MaxPolls) - pollWaitTime := time.Duration(client.Config.PollInterval) * time.Second - - asyncClient, err := client.NewAsyncClient(ctx) - if err != nil { - (*diagsP).AddError( - "error polling async operation "+OperationID, - err.Error(), - ) - - return nil - } - - var opResp dataservices.V1beta1AsyncOperationsItemAsyncOperationsGetResponseable - arc := dataservices. - V1beta1AsyncOperationsAsyncOperationsItemRequestBuilderGetRequestConfiguration{} - - for count := 1; ; count++ { - opResp, err = asyncClient.DataServices(). - V1beta1(). - AsyncOperations(). - ById(OperationID). - Get(ctx, &arc) - if err != nil { - (*diagsP).AddError( - "error polling async operation "+OperationID, - err.Error(), - ) - - return nil - } - - if opResp == nil { - (*diagsP).AddError( - "error polling async operation "+OperationID, - "nil op response", - ) - - return nil - } - - opRespState := opResp.GetState() - if opRespState == nil { - (*diagsP).AddError( - "error polling async operation "+OperationID, - "operation has nil state", - ) - - return nil - } - - // TODO: (API) Use enum not string for state when FF-28181 is fixed - if *opRespState == "FAILED" { - (*diagsP).AddError( - "error polling async operation "+OperationID, - "operation state FAILED", - ) - - return nil - } - - // TODO: (API) Use enum not string for state when FF-28181 is fixed - if *opRespState == "SUCCEEDED" { - break - } - - if count == maxPolls { - (*diagsP).AddError( - "error polling async operation "+OperationID, - "max polls exceeded", - ) - - return nil - } - - time.Sleep(pollWaitTime) - } - - return opResp -} diff --git a/internal/resources/datastore/resource.go b/internal/resources/datastore/resource.go index 55d2259..472ce7e 100644 --- a/internal/resources/datastore/resource.go +++ b/internal/resources/datastore/resource.go @@ -8,9 +8,9 @@ import ( "fmt" "path" + "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/async" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/client" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/constants" - "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/poll" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/sdk/virt/virtualization" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/sdk/virt/virtualization/v1beta1/datastores" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -65,53 +65,6 @@ func (r *Resource) Configure( r.client = req.ProviderData.(*client.PCBeClient) } -func createNameFilter(name string) string { - return constants.NameFilter + name -} - -// TODO: (API) remove this workaround when FF-28659 is fixed -func getDataStoreID( - ctx context.Context, - client client.PCBeClient, - name string, -) (string, error) { - virtClient, _, err := client.NewVirtClient(ctx) - if err != nil { - msg := "error getting datastore ID" - tflog.Error(ctx, msg) - - return "", errors.New(msg) - } - grc := virtualization.V1beta1DatastoresRequestBuilderGetRequestConfiguration{} - qp := virtualization.V1beta1DatastoresRequestBuilderGetQueryParameters{} - filter := createNameFilter(name) - qp.Filter = &filter - grc.QueryParameters = &qp - - datastores, err := virtClient.Virtualization(). - V1beta1(). - Datastores(). - GetAsDatastoresGetResponse(ctx, &grc) - - if datastores.GetTotal() == nil { - msg := "'total' field is nil" - tflog.Error(ctx, msg) - - return "", errors.New(msg) - } - total := *(datastores.GetTotal()) - if total != 1 { - msg := fmt.Sprintf("required 1 datastore with name %s, got %d", name, total) - tflog.Error(ctx, msg) - - return "", errors.New(msg) - } - - id := datastores.GetItems()[0].GetId() - - return *id, err -} - func doRead( ctx context.Context, client client.PCBeClient, @@ -490,24 +443,33 @@ func doCreate( virtHeaderOpts.ResponseHeaders.Clear() operationID := path.Base(location) - poll.AsyncOperation(ctx, client, operationID, diagsP) - if (*diagsP).HasError() { + asyncOperation := async.New( + ctx, + client, + operationID, + constants.TaskDatastore, + ) + err = asyncOperation.Poll() + if err != nil { + (*diagsP).AddError( + "error creating datastore", + "unexpected poll error: "+err.Error(), + ) + return } - datastoreID, err := getDataStoreID( - ctx, client, - (*dataP).Name.ValueString(), - ) + uri, err := asyncOperation.GetSourceResourceURI() if err != nil { (*diagsP).AddError( "error creating datastore", - "failed to get datastore: "+err.Error(), + "failed to get sourceResourceUri: "+err.Error(), ) return } + datastoreID := path.Base(uri) (*dataP).Id = types.StringValue(datastoreID) } @@ -613,9 +575,14 @@ func (r *Resource) Delete( location := virtHeaderOpts.GetResponseHeaders().Get("Location")[0] virtHeaderOpts.ResponseHeaders.Clear() operationID := path.Base(location) - poll.AsyncOperation(ctx, client, operationID, &resp.Diagnostics) + asyncOperation := async.New(ctx, client, operationID, constants.TaskDatastore) + err = asyncOperation.Poll() + if err != nil { + resp.Diagnostics.AddError( + "error deleting datastore", + "delete failed with: "+err.Error(), + ) - if resp.Diagnostics.HasError() { return } } diff --git a/internal/resources/hypervisorcluster/resource.go b/internal/resources/hypervisorcluster/resource.go index b45b3aa..c6bb682 100644 --- a/internal/resources/hypervisorcluster/resource.go +++ b/internal/resources/hypervisorcluster/resource.go @@ -7,9 +7,9 @@ import ( "fmt" "path" + "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/async" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/client" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/constants" - "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/poll" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/sdk/systems/privatecloudbusiness" "github.com/HewlettPackard/hpegl-pcbe-terraform-resources/internal/sdk/virt/virtualization" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -290,59 +290,33 @@ func doCreate( location := sysHeaderOpts.GetResponseHeaders().Get("Location")[0] sysHeaderOpts.ResponseHeaders.Clear() operationID := path.Base(location) - opResp := poll.AsyncOperation(ctx, client, operationID, diagsP) - if (*diagsP).HasError() { - return - } - - if opResp == nil { - (*diagsP).AddError( - "error creating hypervisorcluster", - "async operation did not return a source uri", - ) - - return - } - - if len(opResp.GetAssociatedResources()) != 1 { - (*diagsP).AddError( - "error creating hypervisorcluster", - fmt.Sprintf("could not parse async operation. "+ - "Unexpected length of associatedResources (%d)", - len(opResp.GetAssociatedResources()), - ), - ) - - return - - } - - if opResp.GetAssociatedResources()[0].GetTypeEscaped() == nil { + asyncOperation := async.New( + ctx, + client, + operationID, + constants.TaskHypervisorCluster, + ) + err = asyncOperation.Poll() + if err != nil { (*diagsP).AddError( "error creating hypervisorcluster", - fmt.Sprintf("could not parse async operation. "+ - "associatedResources is nil", - ), + "unexpected poll error: "+err.Error(), ) return } - if *(opResp.GetAssociatedResources()[0].GetTypeEscaped()) != - constants.TaskHypervisorCluster { + uri, err := asyncOperation.GetAssociatedResourceURI() + if err != nil { (*diagsP).AddError( "error creating hypervisorcluster", - fmt.Sprintf("could not parse async operation. "+ - "Unexpected type for associatedResources (%s)", - *(opResp.GetAssociatedResources()[0].GetTypeEscaped()), - ), + "unexpected associated resource error: "+err.Error(), ) return } - // Allow setting id in state as early as possible - hypervisorClusterID := path.Base(*(opResp.GetAssociatedResources()[0].GetResourceUri())) + hypervisorClusterID := path.Base(uri) (*dataP).Id = types.StringValue(hypervisorClusterID) } @@ -451,10 +425,25 @@ func (r *Resource) Delete( location := sysHeaderOpts.GetResponseHeaders().Get("Location")[0] sysHeaderOpts.ResponseHeaders.Clear() operationID := path.Base(location) + // If resp.Diagnostics is not empty, then Delete is // considered to have failed; the hypervisor cluster entry // in the tfstate file will not be removed - poll.AsyncOperation(ctx, client, operationID, &resp.Diagnostics) + asyncOperation := async.New( + ctx, + client, + operationID, + constants.TaskHypervisorCluster, + ) + err = asyncOperation.Poll() + if err != nil { + resp.Diagnostics.AddError( + "error deleting hypervisorcluster "+id, + "unexpected error: "+err.Error(), + ) + + return + } } func (r *Resource) ImportState( diff --git a/internal/simulator/datastore_create.go b/internal/simulator/datastore_create.go index 8e6dc80..67e41d2 100644 --- a/internal/simulator/datastore_create.go +++ b/internal/simulator/datastore_create.go @@ -26,16 +26,12 @@ var dsAsync4 string //go:embed fixtures/datastores/create/async5.json var dsAsync5 string -//go:embed fixtures/datastores/create/filter.json -var dsFilter string - //go:embed fixtures/datastores/create/get.json var dsGet string func datastoreCreate() { taskID := "be55685c-f84f-4ad5-a3d1-2d7692ed47b1" datastoreID := "698de955-87b5-5fe6-b683-78c3948beede" - datastoreName := "mclaren-ds19" hypervisorClusterID := "126fd201-9e6e-5e31-9ffb-a766265b1fd3" // nolint goconst clusterName := "5305-CL" @@ -82,13 +78,6 @@ func datastoreCreate() { SetHeader("Content-Type", "application/json"). BodyString(dsAsync5) - gock.New("http://localhost"). - Get("/virtualization/v1beta1/datastores"). - MatchParam("filter", "name eq "+datastoreName). - Reply(200). - SetHeader("Content-Type", "application/json"). - BodyString(dsFilter) - gock.New("http://localhost"). Get("/virtualization/v1beta1/datastores/"+datastoreID). Reply(200). diff --git a/internal/simulator/fixtures/datastores/create/async5.json b/internal/simulator/fixtures/datastores/create/async5.json index 78da97a..76faf1d 100644 --- a/internal/simulator/fixtures/datastores/create/async5.json +++ b/internal/simulator/fixtures/datastores/create/async5.json @@ -1 +1 @@ -{"associatedResources":[],"createdAt":"2024-07-29T12:42:46.443256231Z","customerId":"bb20052ac81011ed895f7a6e2fba2c57","displayName":"Create Datastore: 'mclaren-ds19'","endedAt":"2024-07-29T12:43:31.699429406Z","error":null,"estimatedRunningDurationMinutes":300,"generation":6,"groups":[{"id":"bb20052ac81011ed895f7a6e2fba2c57","name":"Default Group"}],"hasChildOperations":true,"healthStatus":"OK","id":"be55685c-f84f-4ad5-a3d1-2d7692ed47b1","logMessages":[{"message":"Create Datastore task is created","timestamp":"2024-07-29T12:42:46.44326043Z"},{"message":"Create Datastore task is running","timestamp":"2024-07-29T12:42:46.443261567Z"},{"message":"Preparing the parameters","timestamp":"2024-07-29T12:42:47.176131397Z"},{"message":"Attempting to create the datastore","timestamp":"2024-07-29T12:42:50.15339285Z"},{"message":"Create Datastore task completed","timestamp":"2024-07-29T12:43:31.699430993Z"}],"name":"Create-Datastore: '%!s(MISSING)'mclaren-ds19","parent":null,"progressPercent":100,"recommendations":[],"resourceUri":"/data-services/v1beta1/async-operations/be55685c-f84f-4ad5-a3d1-2d7692ed47b1","rootOperation":{"id":"be55685c-f84f-4ad5-a3d1-2d7692ed47b1","name":"","resourceUri":"/data-services/v1beta1/async-operations/be55685c-f84f-4ad5-a3d1-2d7692ed47b1","type":"task"},"services":["private-cloud-business-edition"],"sourceResourceUri":null,"startedAt":"2024-07-29T12:42:46.443256695Z","state":"SUCCEEDED","subtreeOperationCount":2,"suggestedPollingIntervalSeconds":30,"type":"task","updatedAt":"2024-07-29T12:43:31.854681196Z","userId":"stuart.mclaren@hpe.com"} +{"associatedResources":[],"createdAt":"2024-07-29T12:42:46.443256231Z","customerId":"bb20052ac81011ed895f7a6e2fba2c57","displayName":"Create Datastore: 'mclaren-ds19'","endedAt":"2024-07-29T12:43:31.699429406Z","error":null,"estimatedRunningDurationMinutes":300,"generation":6,"groups":[{"id":"bb20052ac81011ed895f7a6e2fba2c57","name":"Default Group"}],"hasChildOperations":true,"healthStatus":"OK","id":"be55685c-f84f-4ad5-a3d1-2d7692ed47b1","logMessages":[{"message":"Create Datastore task is created","timestamp":"2024-07-29T12:42:46.44326043Z"},{"message":"Create Datastore task is running","timestamp":"2024-07-29T12:42:46.443261567Z"},{"message":"Preparing the parameters","timestamp":"2024-07-29T12:42:47.176131397Z"},{"message":"Attempting to create the datastore","timestamp":"2024-07-29T12:42:50.15339285Z"},{"message":"Create Datastore task completed","timestamp":"2024-07-29T12:43:31.699430993Z"}],"name":"Create-Datastore: '%!s(MISSING)'mclaren-ds19","parent":null,"progressPercent":100,"recommendations":[],"resourceUri":"/data-services/v1beta1/async-operations/be55685c-f84f-4ad5-a3d1-2d7692ed47b1","rootOperation":{"id":"be55685c-f84f-4ad5-a3d1-2d7692ed47b1","name":"","resourceUri":"/data-services/v1beta1/async-operations/be55685c-f84f-4ad5-a3d1-2d7692ed47b1","type":"task"},"services":["private-cloud-business-edition"],"sourceResourceUri":"/virtualization/v1beta1/datastores/698de955-87b5-5fe6-b683-78c3948beede","startedAt":"2024-07-29T12:42:46.443256695Z","state":"SUCCEEDED","subtreeOperationCount":2,"suggestedPollingIntervalSeconds":30,"type":"task","updatedAt":"2024-07-29T12:43:31.854681196Z","userId":"stuart.mclaren@hpe.com"} diff --git a/internal/simulator/fixtures/datastores/create/filter.json b/internal/simulator/fixtures/datastores/create/filter.json deleted file mode 100644 index 7f386c7..0000000 --- a/internal/simulator/fixtures/datastores/create/filter.json +++ /dev/null @@ -1 +0,0 @@ -{"items":[{"allowedOperations":["DATASTORE_CREATE","DATASTORE_DELETE"],"appType":"VMWARE","capacityFree":15670968320,"capacityInBytes":17179869184,"capacityUncommitted":0,"clusterInfo":{"displayName":"5305-CL","id":"acd4daea-e5e3-5f35-8be3-ce4a4b6d946c","name":"5305-CL","resourceUri":"/virtualization/v1beta1/hypervisor-clusters/acd4daea-e5e3-5f35-8be3-ce4a4b6d946c","type":"virtualization/hypervisor-cluster"},"createdAt":"2024-07-29T12:43:02Z","customerId":"bb20052ac81011ed895f7a6e2fba2c57","datacentersInfo":[{"displayName":"","id":"7d8051ab-4224-5075-93be-c1c0fa645e7d","moref":"datacenter-3","name":"5305-DC"}],"datastoreClassification":"","datastoreType":"VMFS","displayName":"mclaren-ds19","folderInfo":{"displayName":"datastore","id":"980201dd-c93a-5861-b0d1-d2d632696003","name":"datastore","resourceUri":"/virtualization/v1beta1/hypervisor-managers/c47b89e9-3a66-4fb9-9a22-cdec3c51604d/folders/980201dd-c93a-5861-b0d1-d2d632696003","type":"virtualization/hypervisor-folder"},"generation":1,"hciClusterUuid":"126fd201-9e6e-5e31-9ffb-a766265b1fd3","hostsInfo":[{"displayName":"16.182.105.217","id":"530b1894-9bd0-5627-9362-565aff1e5cbd","name":"16.182.105.217","resourceUri":"/virtualization/v1beta1/hypervisor-hosts/530b1894-9bd0-5627-9362-565aff1e5cbd","type":"virtualization/hypervisor-host"}],"hypervisorManagerInfo":{"displayName":"16.182.107.157","id":"c47b89e9-3a66-4fb9-9a22-cdec3c51604d","name":"16.182.107.157","resourceUri":"/virtualization/v1beta1/hypervisor-managers/c47b89e9-3a66-4fb9-9a22-cdec3c51604d","type":"virtualization/hypervisor-manager"},"id":"698de955-87b5-5fe6-b683-78c3948beede","moref":"datastore-43","name":"mclaren-ds19","protected":false,"protectionJobInfo":{"id":"","protectionPolicyInfo":{"id":"","name":"","resourceUri":"","type":""},"resourceUri":"","type":""},"protectionPolicyAppliedAtInfo":{"id":"","name":"","resourceUri":"","type":""},"provisioningPolicyInfo":{"id":"","name":"","resourceUri":"","type":""},"replicationInfo":{"id":"","name":"","partnerDetails":null,"resourceUri":""},"resourceUri":"/virtualization/v1beta1/datastores/698de955-87b5-5fe6-b683-78c3948beede","services":["hci-manager"],"state":"OK","stateReason":"OK","status":"OK","type":"virtualization/datastore","uid":"66a78e56-bc8d57b4-f50e-bc97e1e94000","updatedAt":"2024-07-29T12:43:02Z","vmCount":0,"vmProtectionGroupsInfo":null,"volumesInfo":[{"displayName":"mclaren-ds19","id":"0672c65541fc2058ba00000000000000000000000a","name":"mclaren-ds19","resourceUri":"/api/v1/storage-systems//0072c65541fc2058ba000000000000000000000001/volumes/0672c65541fc2058ba00000000000000000000000a","scsiIdentifier":"956387610ec1aae56c9ce900fb0de68e","sizeInBytes":17448304640,"storageFolderInfo":{"displayName":"","id":"","name":"","resourceUri":"","type":"storage-fleet/folder"},"storagePoolInfo":{"displayName":"default","id":"0a72c65541fc2058ba000000000000000000000001","name":"default","resourceUri":"/api/v1/storage-systems//0072c65541fc2058ba000000000000000000000001/storage-pools/0a72c65541fc2058ba000000000000000000000001","type":"storage-fleet/storage-pool"},"storageSystemInfo":{"displayName":"array-5305-grp","id":"fb0de68e","managed":true,"name":"array-5305-grp","resourceUri":"/api/v1/storage-systems/0072c65541fc2058ba000000000000000000000001","serialNumber":"AF-836032","storageSystemType":"HPE Alletra 6000","type":"storage-fleet/storage-system","vendorName":"hpe"},"type":"storage-fleet/volume","volumeSetInfo":{"displayName":"","id":"","name":"","resourceUri":"","type":""}}]}],"count":1,"offset":0,"total":1}