From 76051c4fd4d39df2820ab4ef0e2ce8cb3e5a1cc6 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:00:20 -0700 Subject: [PATCH 1/9] Adding gpu to service offerings (cherry picked from commit 0f8167fa6e5f799e277fa23e55bf5dd04ea668f6) --- .../service_offering_constrained_resource.go | 10 +++++ ...vice_offering_constrained_resource_test.go | 42 +++++++++++++++++++ cloudstack/service_offering_fixed_resource.go | 10 +++++ .../service_offering_fixed_resource_test.go | 37 ++++++++++++++++ cloudstack/service_offering_models.go | 7 ++++ cloudstack/service_offering_schema.go | 26 ++++++++++++ ...service_offering_unconstrained_resource.go | 10 +++++ ...ce_offering_unconstrained_resource_test.go | 31 ++++++++++++++ cloudstack/service_offering_util.go | 24 +++++++++++ 9 files changed, 197 insertions(+) diff --git a/cloudstack/service_offering_constrained_resource.go b/cloudstack/service_offering_constrained_resource.go index 92c80779..865674d9 100644 --- a/cloudstack/service_offering_constrained_resource.go +++ b/cloudstack/service_offering_constrained_resource.go @@ -93,6 +93,7 @@ func (r *serviceOfferingConstrainedResource) Create(ctx context.Context, req res var planDiskQosHypervisor ServiceOfferingDiskQosHypervisor var planDiskOffering ServiceOfferingDiskOffering var planDiskQosStorage ServiceOfferingDiskQosStorage + var planGpu ServiceOfferingGpu resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if !plan.ServiceOfferingDiskQosHypervisor.IsNull() { @@ -104,6 +105,9 @@ func (r *serviceOfferingConstrainedResource) Create(ctx context.Context, req res if !plan.ServiceOfferingDiskQosStorage.IsNull() { resp.Diagnostics.Append(plan.ServiceOfferingDiskQosStorage.As(ctx, &planDiskQosStorage, basetypes.ObjectAsOptions{})...) } + if !plan.ServiceOfferingGpu.IsNull() { + resp.Diagnostics.Append(plan.ServiceOfferingGpu.As(ctx, &planGpu, basetypes.ObjectAsOptions{})...) + } if resp.Diagnostics.HasError() { return } @@ -114,6 +118,7 @@ func (r *serviceOfferingConstrainedResource) Create(ctx context.Context, req res planDiskQosHypervisor.commonCreateParams(ctx, params) planDiskOffering.commonCreateParams(ctx, params) planDiskQosStorage.commonCreateParams(ctx, params) + planGpu.commonCreateParams(ctx, params) // resource specific params if !plan.CpuSpeed.IsNull() { @@ -154,6 +159,7 @@ func (r *serviceOfferingConstrainedResource) Read(ctx context.Context, req resou var stateDiskQosHypervisor ServiceOfferingDiskQosHypervisor var stateDiskOffering ServiceOfferingDiskOffering var stateDiskQosStorage ServiceOfferingDiskQosStorage + var stateGpu ServiceOfferingGpu resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if !state.ServiceOfferingDiskQosHypervisor.IsNull() { @@ -165,6 +171,9 @@ func (r *serviceOfferingConstrainedResource) Read(ctx context.Context, req resou if !state.ServiceOfferingDiskQosStorage.IsNull() { resp.Diagnostics.Append(state.ServiceOfferingDiskQosStorage.As(ctx, &stateDiskQosStorage, basetypes.ObjectAsOptions{})...) } + if !state.ServiceOfferingGpu.IsNull() { + resp.Diagnostics.Append(state.ServiceOfferingGpu.As(ctx, &stateGpu, basetypes.ObjectAsOptions{})...) + } if resp.Diagnostics.HasError() { return } @@ -231,6 +240,7 @@ func (r *serviceOfferingConstrainedResource) Read(ctx context.Context, req resou stateDiskQosHypervisor.commonRead(ctx, cs) stateDiskOffering.commonRead(ctx, cs) stateDiskQosStorage.commonRead(ctx, cs) + stateGpu.commonRead(ctx, cs) if resp.Diagnostics.HasError() { return } diff --git a/cloudstack/service_offering_constrained_resource_test.go b/cloudstack/service_offering_constrained_resource_test.go index 5368db7b..ba03a1ad 100644 --- a/cloudstack/service_offering_constrained_resource_test.go +++ b/cloudstack/service_offering_constrained_resource_test.go @@ -72,6 +72,15 @@ func TestAccServiceOfferingConstrained(t *testing.T) { resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.disk_storage", "name", "disk_storage"), ), }, + { + Config: testAccServiceOfferingCustomConstrained_gpu, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "name", "gpu"), + resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "gpu.vgpu_profile_id", "a6000-8a-profile"), + resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "gpu.count", "1"), + resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "gpu.display", "true"), + ), + }, }, }) } @@ -274,6 +283,39 @@ resource "cloudstack_service_offering_constrained" "disk_hypervisor" { } ` +const testAccServiceOfferingCustomConstrained_gpu = ` +resource "cloudstack_service_offering_constrained" "gpu" { + display_text = "gpu" + name = "gpu" + + // compute + cpu_speed = 2500 + max_cpu_number = 10 + min_cpu_number = 2 + + // memory + max_memory = 4096 + min_memory = 1024 + + // other + host_tags = "test0101,test0202" + network_rate = 1024 + deployment_planner = "UserDispersingPlanner" + + // Feature flags + dynamic_scaling_enabled = false + is_volatile = false + limit_cpu_use = false + offer_ha = false + + gpu = { + vgpu_profile_id = "a6000-8a-profile" + count = 1 + display = true + } +} +` + const testAccServiceOfferingCustomConstrained_disk_storage = ` resource "cloudstack_service_offering_constrained" "disk_storage" { display_text = "disk_storage" diff --git a/cloudstack/service_offering_fixed_resource.go b/cloudstack/service_offering_fixed_resource.go index 6b500fd2..182b1c01 100644 --- a/cloudstack/service_offering_fixed_resource.go +++ b/cloudstack/service_offering_fixed_resource.go @@ -78,6 +78,7 @@ func (r *serviceOfferingFixedResource) Create(ctx context.Context, req resource. var planDiskQosHypervisor ServiceOfferingDiskQosHypervisor var planDiskOffering ServiceOfferingDiskOffering var planDiskQosStorage ServiceOfferingDiskQosStorage + var planGpu ServiceOfferingGpu resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if !plan.ServiceOfferingDiskQosHypervisor.IsNull() { @@ -89,6 +90,9 @@ func (r *serviceOfferingFixedResource) Create(ctx context.Context, req resource. if !plan.ServiceOfferingDiskQosStorage.IsNull() { resp.Diagnostics.Append(plan.ServiceOfferingDiskQosStorage.As(ctx, &planDiskQosStorage, basetypes.ObjectAsOptions{})...) } + if !plan.ServiceOfferingGpu.IsNull() { + resp.Diagnostics.Append(plan.ServiceOfferingGpu.As(ctx, &planGpu, basetypes.ObjectAsOptions{})...) + } if resp.Diagnostics.HasError() { return } @@ -99,6 +103,7 @@ func (r *serviceOfferingFixedResource) Create(ctx context.Context, req resource. planDiskQosHypervisor.commonCreateParams(ctx, params) planDiskOffering.commonCreateParams(ctx, params) planDiskQosStorage.commonCreateParams(ctx, params) + planGpu.commonCreateParams(ctx, params) // resource specific params if !plan.CpuNumber.IsNull() { @@ -131,6 +136,7 @@ func (r *serviceOfferingFixedResource) Read(ctx context.Context, req resource.Re var stateDiskQosHypervisor ServiceOfferingDiskQosHypervisor var stateDiskOffering ServiceOfferingDiskOffering var stateDiskQosStorage ServiceOfferingDiskQosStorage + var stateGpu ServiceOfferingGpu resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if !state.ServiceOfferingDiskQosHypervisor.IsNull() { @@ -142,6 +148,9 @@ func (r *serviceOfferingFixedResource) Read(ctx context.Context, req resource.Re if !state.ServiceOfferingDiskQosStorage.IsNull() { resp.Diagnostics.Append(state.ServiceOfferingDiskQosStorage.As(ctx, &stateDiskQosStorage, basetypes.ObjectAsOptions{})...) } + if !state.ServiceOfferingGpu.IsNull() { + resp.Diagnostics.Append(state.ServiceOfferingGpu.As(ctx, &stateGpu, basetypes.ObjectAsOptions{})...) + } if resp.Diagnostics.HasError() { return } @@ -170,6 +179,7 @@ func (r *serviceOfferingFixedResource) Read(ctx context.Context, req resource.Re stateDiskQosHypervisor.commonRead(ctx, cs) stateDiskOffering.commonRead(ctx, cs) stateDiskQosStorage.commonRead(ctx, cs) + stateGpu.commonRead(ctx, cs) if resp.Diagnostics.HasError() { return } diff --git a/cloudstack/service_offering_fixed_resource_test.go b/cloudstack/service_offering_fixed_resource_test.go index 438740e0..0f0151a0 100644 --- a/cloudstack/service_offering_fixed_resource_test.go +++ b/cloudstack/service_offering_fixed_resource_test.go @@ -66,6 +66,15 @@ func TestAccServiceOfferingFixed(t *testing.T) { resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.disk_storage", "name", "disk_storage"), ), }, + { + Config: testAccServiceOfferingFixed_gpu, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "name", "gpu"), + resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "gpu.vgpu_profile_id", "a6000-8a-profile"), + resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "gpu.count", "1"), + resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "gpu.display", "true"), + ), + }, }, }) } @@ -237,3 +246,31 @@ resource "cloudstack_service_offering_fixed" "disk_storage" { } } ` + +const testAccServiceOfferingFixed_gpu = ` +resource "cloudstack_service_offering_fixed" "gpu" { + display_text = "gpu" + name = "gpu" + + // compute + cpu_number = 2 + cpu_speed = 2500 + memory = 2048 + + // other + host_tags = "test0101, test0202" + network_rate = 1024 + deployment_planner = "UserDispersingPlanner" + + dynamic_scaling_enabled = false + is_volatile = false + limit_cpu_use = false + offer_ha = false + + gpu = { + vgpu_profile_id = "a6000-8a-profile" + count = 1 + display = true + } +} +` diff --git a/cloudstack/service_offering_models.go b/cloudstack/service_offering_models.go index a93ffa48..35c6dd3c 100644 --- a/cloudstack/service_offering_models.go +++ b/cloudstack/service_offering_models.go @@ -58,6 +58,7 @@ type serviceOfferingCommonResourceModel struct { ServiceOfferingDiskQosHypervisor types.Object `tfsdk:"disk_hypervisor"` ServiceOfferingDiskOffering types.Object `tfsdk:"disk_offering"` ServiceOfferingDiskQosStorage types.Object `tfsdk:"disk_storage"` + ServiceOfferingGpu types.Object `tfsdk:"gpu"` } type ServiceOfferingDiskQosHypervisor struct { @@ -84,3 +85,9 @@ type ServiceOfferingDiskQosStorage struct { MaxIops types.Int64 `tfsdk:"max_iops"` MinIops types.Int64 `tfsdk:"min_iops"` } + +type ServiceOfferingGpu struct { + VgpuProfileId types.String `tfsdk:"vgpu_profile_id"` + Count types.Int32 `tfsdk:"count"` + Display types.Bool `tfsdk:"display"` +} diff --git a/cloudstack/service_offering_schema.go b/cloudstack/service_offering_schema.go index 9586d513..e0d47dfb 100644 --- a/cloudstack/service_offering_schema.go +++ b/cloudstack/service_offering_schema.go @@ -237,6 +237,32 @@ func serviceOfferingMergeCommonSchema(s1 map[string]schema.Attribute) map[string }, }, }, + "gpu": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "vgpu_profile_id": schema.StringAttribute{ + Description: "the ID of the vGPU profile to associate with the service offering", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "count": schema.Int32Attribute{ + Description: "the number of GPUs to assign to the guest VM", + Optional: true, + PlanModifiers: []planmodifier.Int32{ + int32planmodifier.RequiresReplace(), + }, + }, + "display": schema.BoolAttribute{ + Description: "whether the GPU is presented as a display device to the guest VM", + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + }, + }, } for key, value := range s1 { diff --git a/cloudstack/service_offering_unconstrained_resource.go b/cloudstack/service_offering_unconstrained_resource.go index 98b937cd..a75b257b 100644 --- a/cloudstack/service_offering_unconstrained_resource.go +++ b/cloudstack/service_offering_unconstrained_resource.go @@ -55,6 +55,7 @@ func (r *serviceOfferingUnconstrainedResource) Create(ctx context.Context, req r var planDiskQosHypervisor ServiceOfferingDiskQosHypervisor var planDiskOffering ServiceOfferingDiskOffering var planDiskQosStorage ServiceOfferingDiskQosStorage + var planGpu ServiceOfferingGpu resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if !plan.ServiceOfferingDiskQosHypervisor.IsNull() { @@ -66,6 +67,9 @@ func (r *serviceOfferingUnconstrainedResource) Create(ctx context.Context, req r if !plan.ServiceOfferingDiskQosStorage.IsNull() { resp.Diagnostics.Append(plan.ServiceOfferingDiskQosStorage.As(ctx, &planDiskQosStorage, basetypes.ObjectAsOptions{})...) } + if !plan.ServiceOfferingGpu.IsNull() { + resp.Diagnostics.Append(plan.ServiceOfferingGpu.As(ctx, &planGpu, basetypes.ObjectAsOptions{})...) + } if resp.Diagnostics.HasError() { return } @@ -76,6 +80,7 @@ func (r *serviceOfferingUnconstrainedResource) Create(ctx context.Context, req r planDiskQosHypervisor.commonCreateParams(ctx, params) planDiskOffering.commonCreateParams(ctx, params) planDiskQosStorage.commonCreateParams(ctx, params) + planGpu.commonCreateParams(ctx, params) // create offering cs, err := r.client.ServiceOffering.CreateServiceOffering(params) @@ -97,6 +102,7 @@ func (r *serviceOfferingUnconstrainedResource) Read(ctx context.Context, req res var stateDiskQosHypervisor ServiceOfferingDiskQosHypervisor var stateDiskOffering ServiceOfferingDiskOffering var stateDiskQosStorage ServiceOfferingDiskQosStorage + var stateGpu ServiceOfferingGpu resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if !state.ServiceOfferingDiskQosHypervisor.IsNull() { @@ -108,6 +114,9 @@ func (r *serviceOfferingUnconstrainedResource) Read(ctx context.Context, req res if !state.ServiceOfferingDiskQosStorage.IsNull() { resp.Diagnostics.Append(state.ServiceOfferingDiskQosStorage.As(ctx, &stateDiskQosStorage, basetypes.ObjectAsOptions{})...) } + if !state.ServiceOfferingGpu.IsNull() { + resp.Diagnostics.Append(state.ServiceOfferingGpu.As(ctx, &stateGpu, basetypes.ObjectAsOptions{})...) + } if resp.Diagnostics.HasError() { return } @@ -125,6 +134,7 @@ func (r *serviceOfferingUnconstrainedResource) Read(ctx context.Context, req res stateDiskQosHypervisor.commonRead(ctx, cs) stateDiskOffering.commonRead(ctx, cs) stateDiskQosStorage.commonRead(ctx, cs) + stateGpu.commonRead(ctx, cs) if resp.Diagnostics.HasError() { return } diff --git a/cloudstack/service_offering_unconstrained_resource_test.go b/cloudstack/service_offering_unconstrained_resource_test.go index 5aba779f..b5bc5366 100644 --- a/cloudstack/service_offering_unconstrained_resource_test.go +++ b/cloudstack/service_offering_unconstrained_resource_test.go @@ -66,6 +66,15 @@ func TestAccServiceOfferingUnconstrained(t *testing.T) { resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.disk_storage", "name", "disk_storage"), ), }, + { + Config: testAccServiceOfferingUnconstrained_gpu, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "name", "gpu"), + resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "gpu.vgpu_profile_id", "a6000-8a-profile"), + resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "gpu.count", "1"), + resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "gpu.display", "true"), + ), + }, }, }) } @@ -205,3 +214,25 @@ resource "cloudstack_service_offering_unconstrained" "disk_storage" { } } ` + +const testAccServiceOfferingUnconstrained_gpu = ` +resource "cloudstack_service_offering_unconstrained" "gpu" { + display_text = "gpu" + name = "gpu" + + host_tags = "test0101,test0202" + network_rate = 1024 + deployment_planner = "UserDispersingPlanner" + + dynamic_scaling_enabled = true + is_volatile = true + limit_cpu_use = true + offer_ha = true + + gpu = { + vgpu_profile_id = "a6000-8a-profile" + count = 1 + display = true + } +} +` diff --git a/cloudstack/service_offering_util.go b/cloudstack/service_offering_util.go index 666e0fce..9f540446 100644 --- a/cloudstack/service_offering_util.go +++ b/cloudstack/service_offering_util.go @@ -170,6 +170,16 @@ func (state *ServiceOfferingDiskQosStorage) commonRead(ctx context.Context, cs * } +func (state *ServiceOfferingGpu) commonRead(ctx context.Context, cs *cloudstack.ServiceOffering) { + if cs.Vgpuprofileid != "" { + state.VgpuProfileId = types.StringValue(cs.Vgpuprofileid) + } + if cs.Gpucount > 0 { + state.Count = types.Int32Value(int32(cs.Gpucount)) + } + state.Display = types.BoolValue(cs.Gpudisplay) +} + // ------------------------------------------------------------------------------------------------------------------------------ // common Create methods // - @@ -278,3 +288,17 @@ func (plan *ServiceOfferingDiskQosStorage) commonCreateParams(ctx context.Contex return p } + +func (plan *ServiceOfferingGpu) commonCreateParams(ctx context.Context, p *cloudstack.CreateServiceOfferingParams) *cloudstack.CreateServiceOfferingParams { + if !plan.VgpuProfileId.IsNull() { + p.SetVgpuprofileid(plan.VgpuProfileId.ValueString()) + } + if !plan.Count.IsNull() { + p.SetGpucount(int(plan.Count.ValueInt32())) + } + if !plan.Display.IsNull() { + p.SetGpudisplay(plan.Display.ValueBool()) + } + + return p +} From 8096583f7a9c20b13fd799a4054b72ac2ce63e46 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:39:40 -0700 Subject: [PATCH 2/9] gpu (cherry picked from commit b6de7353de249c2b99d0323cb4f8f2a385119632) --- cloudstack/data_source_cloudstack_gpu_card.go | 142 +++++++++++++ .../data_source_cloudstack_gpu_card_test.go | 50 +++++ .../data_source_cloudstack_vgpu_profile.go | 190 ++++++++++++++++++ ...ata_source_cloudstack_vgpu_profile_test.go | 50 +++++ cloudstack/provider.go | 2 + go.mod | 2 +- go.sum | 4 +- website/docs/d/gpu_card.html.markdown | 47 +++++ website/docs/d/vgpu_profile.html.markdown | 55 +++++ 9 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 cloudstack/data_source_cloudstack_gpu_card.go create mode 100644 cloudstack/data_source_cloudstack_gpu_card_test.go create mode 100644 cloudstack/data_source_cloudstack_vgpu_profile.go create mode 100644 cloudstack/data_source_cloudstack_vgpu_profile_test.go create mode 100644 website/docs/d/gpu_card.html.markdown create mode 100644 website/docs/d/vgpu_profile.html.markdown diff --git a/cloudstack/data_source_cloudstack_gpu_card.go b/cloudstack/data_source_cloudstack_gpu_card.go new file mode 100644 index 00000000..e3e497ba --- /dev/null +++ b/cloudstack/data_source_cloudstack_gpu_card.go @@ -0,0 +1,142 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 cloudstack + +import ( + "fmt" + "log" + "reflect" + "regexp" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackGpuCard() *schema.Resource { + return &schema.Resource{ + Read: datasourceCloudStackGpuCardRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + + //Computed values + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Description: "the name of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "device_id": { + Description: "the device id of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "device_name": { + Description: "the device name of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "vendor_id": { + Description: "the vendor id of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "vendor_name": { + Description: "the vendor name of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func datasourceCloudStackGpuCardRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + p := cs.GPU.NewListGpuCardsParams() + + csGpuCards, err := cs.GPU.ListGpuCards(p) + if err != nil { + return fmt.Errorf("failed to list GPU cards: %s", err) + } + + filters := d.Get("filter") + + for _, card := range csGpuCards.GpuCards { + match, err := applyGpuCardFilters(card, filters.(*schema.Set)) + if err != nil { + return err + } + if match { + return gpuCardDescriptionAttributes(d, card) + } + } + + return fmt.Errorf("no GPU cards found") +} + +func gpuCardDescriptionAttributes(d *schema.ResourceData, card *cloudstack.GpuCard) error { + d.SetId(card.Id) + + fields := map[string]interface{}{ + "id": card.Id, + "name": card.Name, + "device_id": card.Deviceid, + "device_name": card.Devicename, + "vendor_id": card.Vendorid, + "vendor_name": card.Vendorname, + } + + for k, v := range fields { + if err := d.Set(k, v); err != nil { + log.Printf("[WARN] Error setting %s: %s", k, err) + } + } + + return nil +} + +func applyGpuCardFilters(card *cloudstack.GpuCard, filters *schema.Set) (bool, error) { + val := reflect.ValueOf(card).Elem() + + for _, f := range filters.List() { + filter := f.(map[string]interface{}) + r, err := regexp.Compile(filter["value"].(string)) + if err != nil { + return false, fmt.Errorf("invalid regex: %s", err) + } + updatedName := strings.ReplaceAll(filter["name"].(string), "_", "") + cardField := val.FieldByNameFunc(func(fieldName string) bool { + if strings.EqualFold(fieldName, updatedName) { + updatedName = fieldName + return true + } + return false + }).String() + + if !r.MatchString(cardField) { + return false, nil + } + } + + return true, nil +} diff --git a/cloudstack/data_source_cloudstack_gpu_card_test.go b/cloudstack/data_source_cloudstack_gpu_card_test.go new file mode 100644 index 00000000..9d8df63d --- /dev/null +++ b/cloudstack/data_source_cloudstack_gpu_card_test.go @@ -0,0 +1,50 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 cloudstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGpuCardDataSource_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testGpuCardDataSourceConfig_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.cloudstack_gpu_card.test", "id"), + ), + }, + }, + }) +} + +const testGpuCardDataSourceConfig_basic = ` +data "cloudstack_gpu_card" "test" { + filter { + name = "name" + value = "NVIDIA.*" + } +} +` diff --git a/cloudstack/data_source_cloudstack_vgpu_profile.go b/cloudstack/data_source_cloudstack_vgpu_profile.go new file mode 100644 index 00000000..956ac7d0 --- /dev/null +++ b/cloudstack/data_source_cloudstack_vgpu_profile.go @@ -0,0 +1,190 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 cloudstack + +import ( + "fmt" + "log" + "reflect" + "regexp" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackVgpuProfile() *schema.Resource { + return &schema.Resource{ + Read: datasourceCloudStackVgpuProfileRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + + //Computed values + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Description: "the name of the vGPU profile", + Type: schema.TypeString, + Computed: true, + }, + "description": { + Description: "the description of the vGPU profile", + Type: schema.TypeString, + Computed: true, + }, + "device_id": { + Description: "the device id of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "device_name": { + Description: "the device name of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "gpu_card_id": { + Description: "the GPU card id of the vGPU profile", + Type: schema.TypeString, + Computed: true, + }, + "gpu_card_name": { + Description: "the GPU card name of the vGPU profile", + Type: schema.TypeString, + Computed: true, + }, + "max_heads": { + Description: "the maximum displays per vGPU instance", + Type: schema.TypeInt, + Computed: true, + }, + "max_resolution_x": { + Description: "the maximum X resolution per display", + Type: schema.TypeInt, + Computed: true, + }, + "max_resolution_y": { + Description: "the maximum Y resolution per display", + Type: schema.TypeInt, + Computed: true, + }, + "max_vgpu_per_physical_gpu": { + Description: "the maximum number of vGPU instances per physical GPU", + Type: schema.TypeInt, + Computed: true, + }, + "vendor_id": { + Description: "the vendor id of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "vendor_name": { + Description: "the vendor name of the GPU card", + Type: schema.TypeString, + Computed: true, + }, + "video_ram": { + Description: "the video RAM size in MB for the vGPU profile", + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func datasourceCloudStackVgpuProfileRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + p := cs.GPU.NewListVgpuProfilesParams() + + csVgpuProfiles, err := cs.GPU.ListVgpuProfiles(p) + if err != nil { + return fmt.Errorf("failed to list vGPU profiles: %s", err) + } + + filters := d.Get("filter") + + for _, profile := range csVgpuProfiles.VgpuProfiles { + match, err := applyVgpuProfileFilters(profile, filters.(*schema.Set)) + if err != nil { + return err + } + if match { + return vgpuProfileDescriptionAttributes(d, profile) + } + } + + return fmt.Errorf("no vGPU profiles found") +} + +func vgpuProfileDescriptionAttributes(d *schema.ResourceData, profile *cloudstack.VgpuProfile) error { + d.SetId(profile.Id) + + fields := map[string]interface{}{ + "id": profile.Id, + "name": profile.Name, + "description": profile.Description, + "device_id": profile.Deviceid, + "device_name": profile.Devicename, + "gpu_card_id": profile.Gpucardid, + "gpu_card_name": profile.Gpucardname, + "max_heads": profile.Maxheads, + "max_resolution_x": profile.Maxresolutionx, + "max_resolution_y": profile.Maxresolutiony, + "max_vgpu_per_physical_gpu": profile.Maxvgpuperphysicalgpu, + "vendor_id": profile.Vendorid, + "vendor_name": profile.Vendorname, + "video_ram": profile.Videoram, + } + + for k, v := range fields { + if err := d.Set(k, v); err != nil { + log.Printf("[WARN] Error setting %s: %s", k, err) + } + } + + return nil +} + +func applyVgpuProfileFilters(profile *cloudstack.VgpuProfile, filters *schema.Set) (bool, error) { + val := reflect.ValueOf(profile).Elem() + + for _, f := range filters.List() { + filter := f.(map[string]interface{}) + r, err := regexp.Compile(filter["value"].(string)) + if err != nil { + return false, fmt.Errorf("invalid regex: %s", err) + } + updatedName := strings.ReplaceAll(filter["name"].(string), "_", "") + profileField := val.FieldByNameFunc(func(fieldName string) bool { + if strings.EqualFold(fieldName, updatedName) { + updatedName = fieldName + return true + } + return false + }).String() + + if !r.MatchString(profileField) { + return false, nil + } + } + + return true, nil +} diff --git a/cloudstack/data_source_cloudstack_vgpu_profile_test.go b/cloudstack/data_source_cloudstack_vgpu_profile_test.go new file mode 100644 index 00000000..20d4aa73 --- /dev/null +++ b/cloudstack/data_source_cloudstack_vgpu_profile_test.go @@ -0,0 +1,50 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 cloudstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccVgpuProfileDataSource_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testVgpuProfileDataSourceConfig_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.cloudstack_vgpu_profile.test", "name", "passthrough"), + ), + }, + }, + }) +} + +const testVgpuProfileDataSourceConfig_basic = ` +data "cloudstack_vgpu_profile" "test" { + filter { + name = "name" + value = "passthrough" + } +} +` diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 72090147..a33a736f 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -105,6 +105,8 @@ func Provider() *schema.Provider { "cloudstack_quota_enabled": dataSourceCloudStackQuotaEnabled(), "cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(), "cloudstack_user_data": dataSourceCloudstackUserData(), + "cloudstack_vgpu_profile": dataSourceCloudstackVgpuProfile(), + "cloudstack_gpu_card": dataSourceCloudstackGpuCard(), }, ResourcesMap: map[string]*schema.Resource{ diff --git a/go.mod b/go.mod index 6e69966d..1a2a7fc8 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ module github.com/terraform-providers/terraform-provider-cloudstack require ( - github.com/apache/cloudstack-go/v2 v2.18.1 + github.com/apache/cloudstack-go/v2 v2.19.1 github.com/go-ini/ini v1.67.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/terraform-plugin-framework v1.12.0 diff --git a/go.sum b/go.sum index d60bbe41..39c478eb 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/apache/cloudstack-go/v2 v2.18.1 h1:SgdRUEj5x17wSPfwAacjWgTqbtS/u7iaqnbpILWzE1c= -github.com/apache/cloudstack-go/v2 v2.18.1/go.mod h1:p/YBUwIEkQN6CQxFhw8Ff0wzf1MY0qRRRuGYNbcb1F8= +github.com/apache/cloudstack-go/v2 v2.19.1 h1:1K5O4NZpdWzOZUN6XuaVNsdX+QnoFRc5VE/oc1nUckQ= +github.com/apache/cloudstack-go/v2 v2.19.1/go.mod h1:p/YBUwIEkQN6CQxFhw8Ff0wzf1MY0qRRRuGYNbcb1F8= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= diff --git a/website/docs/d/gpu_card.html.markdown b/website/docs/d/gpu_card.html.markdown new file mode 100644 index 00000000..d48d18c9 --- /dev/null +++ b/website/docs/d/gpu_card.html.markdown @@ -0,0 +1,47 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_gpu_card" +description: |- + Gets information about a GPU card. +--- + +# cloudstack_gpu_card + +Use this data source to get information about a GPU card for use in other resources. + +## Example Usage + +```hcl +data "cloudstack_gpu_card" "card" { + filter { + name = "name" + value = "NVIDIA.*" + } +} + +output "gpu_card_id" { + value = data.cloudstack_gpu_card.card.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `filter` - (Required) One or more name/value pairs to filter off of. See detailed documentation below. + +### Filter Arguments + +* `name` - (Required) The name of the field to filter on. This can be any of the fields returned by the CloudStack API. +* `value` - (Required) The value to filter on. This should be a regular expression. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the GPU card. +* `name` - The name of the GPU card. +* `device_id` - The device id of the GPU card. +* `device_name` - The device name of the GPU card. +* `vendor_id` - The vendor id of the GPU card. +* `vendor_name` - The vendor name of the GPU card. diff --git a/website/docs/d/vgpu_profile.html.markdown b/website/docs/d/vgpu_profile.html.markdown new file mode 100644 index 00000000..02bb4ea4 --- /dev/null +++ b/website/docs/d/vgpu_profile.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_vgpu_profile" +description: |- + Gets information about a vGPU profile. +--- + +# cloudstack_vgpu_profile + +Use this data source to get information about a vGPU profile for use in other resources. + +## Example Usage + +```hcl +data "cloudstack_vgpu_profile" "profile" { + filter { + name = "name" + value = "passthrough" + } +} + +output "vgpu_profile_id" { + value = data.cloudstack_vgpu_profile.profile.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `filter` - (Required) One or more name/value pairs to filter off of. See detailed documentation below. + +### Filter Arguments + +* `name` - (Required) The name of the field to filter on. This can be any of the fields returned by the CloudStack API. +* `value` - (Required) The value to filter on. This should be a regular expression. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the vGPU profile. +* `name` - The name of the vGPU profile. +* `description` - The description of the vGPU profile. +* `device_id` - The device id of the GPU card. +* `device_name` - The device name of the GPU card. +* `gpu_card_id` - The GPU card id of the vGPU profile. +* `gpu_card_name` - The GPU card name of the vGPU profile. +* `max_heads` - The maximum displays per vGPU instance. +* `max_resolution_x` - The maximum X resolution per display. +* `max_resolution_y` - The maximum Y resolution per display. +* `max_vgpu_per_physical_gpu` - The maximum number of vGPU instances per physical GPU. +* `vendor_id` - The vendor id of the GPU card. +* `vendor_name` - The vendor name of the GPU card. +* `video_ram` - The video RAM size in MB for the vGPU profile. From b6d25072ca80d3792ff42f3158271d8e680a5c57 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Tue, 18 Aug 2026 18:02:20 +0530 Subject: [PATCH 3/9] Address review comments - Add Computed: true to count field to indicate server-managed attribute - Add Computed: true and Default: false to display field for consistency with other boolean attributes in the schema --- cloudstack/service_offering_schema.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloudstack/service_offering_schema.go b/cloudstack/service_offering_schema.go index e0d47dfb..19c019f5 100644 --- a/cloudstack/service_offering_schema.go +++ b/cloudstack/service_offering_schema.go @@ -250,6 +250,7 @@ func serviceOfferingMergeCommonSchema(s1 map[string]schema.Attribute) map[string "count": schema.Int32Attribute{ Description: "the number of GPUs to assign to the guest VM", Optional: true, + Computed: true, PlanModifiers: []planmodifier.Int32{ int32planmodifier.RequiresReplace(), }, @@ -257,9 +258,11 @@ func serviceOfferingMergeCommonSchema(s1 map[string]schema.Attribute) map[string "display": schema.BoolAttribute{ Description: "whether the GPU is presented as a display device to the guest VM", Optional: true, + Computed: true, PlanModifiers: []planmodifier.Bool{ boolplanmodifier.RequiresReplace(), }, + Default: booldefault.StaticBool(false), }, }, }, From dd56318937709c0de06fdb554d833762aff971af Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Tue, 18 Aug 2026 18:18:07 +0530 Subject: [PATCH 4/9] Fix GPU datasource filters and service offering state drift Review Fixes: 1. Fix filter panic in vGPU profile datasource - Safely validate field exists before accessing - Use fmt.Sprintf for safe type conversion - Return clear error for unknown filter fields 2. Fix filter panic in GPU card datasource - Same safety improvements as vGPU profile filters - Prevents panics on non-string fields or invalid names 3. Fix state drift detection in service offering GPU block - Explicitly set VgpuProfileId to null when empty - Explicitly set Count to null when 0 - Allows drift detection when GPU config changes out-of-band 4. Update service offering documentation - Add GPU block examples to fixed/constrained/unconstrained offerings - Document GPU block attributes and defaults Note: Client-side filtering remains; API doesn't expose server-side filter params for GPU cards/vGPU profiles as suggested in review. --- cloudstack/data_source_cloudstack_gpu_card.go | 27 ++++++++++++++----- .../data_source_cloudstack_vgpu_profile.go | 26 +++++++++++++----- cloudstack/service_offering_util.go | 4 +++ ...service_offering_constrained.html.markdown | 12 +++++++++ .../r/service_offering_fixed.html.markdown | 11 ++++++++ ...rvice_offering_unconstrained.html.markdown | 11 ++++++++ 6 files changed, 79 insertions(+), 12 deletions(-) diff --git a/cloudstack/data_source_cloudstack_gpu_card.go b/cloudstack/data_source_cloudstack_gpu_card.go index e3e497ba..bb3471f5 100644 --- a/cloudstack/data_source_cloudstack_gpu_card.go +++ b/cloudstack/data_source_cloudstack_gpu_card.go @@ -79,10 +79,11 @@ func datasourceCloudStackGpuCardRead(d *schema.ResourceData, meta interface{}) e return fmt.Errorf("failed to list GPU cards: %s", err) } - filters := d.Get("filter") + filters := d.Get("filter").(*schema.Set) + // Client-side filtering for fields not supported server-side for _, card := range csGpuCards.GpuCards { - match, err := applyGpuCardFilters(card, filters.(*schema.Set)) + match, err := applyGpuCardFilters(card, filters) if err != nil { return err } @@ -124,16 +125,30 @@ func applyGpuCardFilters(card *cloudstack.GpuCard, filters *schema.Set) (bool, e if err != nil { return false, fmt.Errorf("invalid regex: %s", err) } - updatedName := strings.ReplaceAll(filter["name"].(string), "_", "") - cardField := val.FieldByNameFunc(func(fieldName string) bool { + + filterName := filter["name"].(string) + updatedName := strings.ReplaceAll(filterName, "_", "") + + // Find the field with case-insensitive matching + var cardField reflect.Value + val.FieldByNameFunc(func(fieldName string) bool { if strings.EqualFold(fieldName, updatedName) { updatedName = fieldName + cardField = val.FieldByName(fieldName) return true } return false - }).String() + }) + + // Validate field was found + if !cardField.IsValid() { + return false, fmt.Errorf("unknown filter field '%s'", filterName) + } + + // Safely convert field value to string + fieldStr := fmt.Sprintf("%v", cardField.Interface()) - if !r.MatchString(cardField) { + if !r.MatchString(fieldStr) { return false, nil } } diff --git a/cloudstack/data_source_cloudstack_vgpu_profile.go b/cloudstack/data_source_cloudstack_vgpu_profile.go index 956ac7d0..bf41d970 100644 --- a/cloudstack/data_source_cloudstack_vgpu_profile.go +++ b/cloudstack/data_source_cloudstack_vgpu_profile.go @@ -119,10 +119,10 @@ func datasourceCloudStackVgpuProfileRead(d *schema.ResourceData, meta interface{ return fmt.Errorf("failed to list vGPU profiles: %s", err) } - filters := d.Get("filter") + filters := d.Get("filter").(*schema.Set) for _, profile := range csVgpuProfiles.VgpuProfiles { - match, err := applyVgpuProfileFilters(profile, filters.(*schema.Set)) + match, err := applyVgpuProfileFilters(profile, filters) if err != nil { return err } @@ -172,16 +172,30 @@ func applyVgpuProfileFilters(profile *cloudstack.VgpuProfile, filters *schema.Se if err != nil { return false, fmt.Errorf("invalid regex: %s", err) } - updatedName := strings.ReplaceAll(filter["name"].(string), "_", "") - profileField := val.FieldByNameFunc(func(fieldName string) bool { + + filterName := filter["name"].(string) + updatedName := strings.ReplaceAll(filterName, "_", "") + + // Find the field with case-insensitive matching + var profileField reflect.Value + val.FieldByNameFunc(func(fieldName string) bool { if strings.EqualFold(fieldName, updatedName) { updatedName = fieldName + profileField = val.FieldByName(fieldName) return true } return false - }).String() + }) + + // Validate field was found + if !profileField.IsValid() { + return false, fmt.Errorf("unknown filter field '%s'", filterName) + } + + // Safely convert field value to string + fieldStr := fmt.Sprintf("%v", profileField.Interface()) - if !r.MatchString(profileField) { + if !r.MatchString(fieldStr) { return false, nil } } diff --git a/cloudstack/service_offering_util.go b/cloudstack/service_offering_util.go index 9f540446..a21231a1 100644 --- a/cloudstack/service_offering_util.go +++ b/cloudstack/service_offering_util.go @@ -173,9 +173,13 @@ func (state *ServiceOfferingDiskQosStorage) commonRead(ctx context.Context, cs * func (state *ServiceOfferingGpu) commonRead(ctx context.Context, cs *cloudstack.ServiceOffering) { if cs.Vgpuprofileid != "" { state.VgpuProfileId = types.StringValue(cs.Vgpuprofileid) + } else { + state.VgpuProfileId = types.StringNull() } if cs.Gpucount > 0 { state.Count = types.Int32Value(int32(cs.Gpucount)) + } else { + state.Count = types.Int32Null() } state.Display = types.BoolValue(cs.Gpudisplay) } diff --git a/website/docs/r/service_offering_constrained.html.markdown b/website/docs/r/service_offering_constrained.html.markdown index 20a33b52..8ff5bc7c 100644 --- a/website/docs/r/service_offering_constrained.html.markdown +++ b/website/docs/r/service_offering_constrained.html.markdown @@ -37,6 +37,12 @@ resource "cloudstack_service_offering_constrained" "example" { provisioning_type = "thin" storage_type = "local" } + + gpu { + vgpu_profile_id = "gpu-profile-uuid" + count = 1 + display = false + } } ``` @@ -89,6 +95,12 @@ The following arguments are supported: - `max_iops` (Int, Optional) - Max IOPS of the compute offering. - `min_iops` (Int, Optional) - Min IOPS of the compute offering. +#### `gpu` (Block, Optional) + +- `vgpu_profile_id` (String, Required) - The ID of the vGPU profile to associate with the service offering. +- `count` (Int, Optional, Computed) - The number of GPUs to assign to the guest VM. +- `display` (Bool, Optional, Computed, Default: false) - Whether the GPU is presented as a display device to the guest VM. + ## Attributes Reference In addition to the arguments above, the following attributes are exported: diff --git a/website/docs/r/service_offering_fixed.html.markdown b/website/docs/r/service_offering_fixed.html.markdown index 448baa4c..1c868051 100644 --- a/website/docs/r/service_offering_fixed.html.markdown +++ b/website/docs/r/service_offering_fixed.html.markdown @@ -32,6 +32,11 @@ resource "cloudstack_service_offering_fixed" "fixed1" { # disk_offering { ... } # disk_hypervisor { ... } # disk_storage { ... } + # gpu { + # vgpu_profile_id = "..." + # count = 1 + # display = false + # } } ``` @@ -86,6 +91,12 @@ The following arguments are supported: - `max_iops` (Optional) - Max IOPS of the compute offering. - `min_iops` (Optional) - Min IOPS of the compute offering. +#### `gpu` (Optional) + +- `vgpu_profile_id` (Required) - The ID of the vGPU profile to associate with the service offering. +- `count` (Optional, Computed) - The number of GPUs to assign to the guest VM. +- `display` (Optional, Computed) - Whether the GPU is presented as a display device to the guest VM. Defaults to `false`. + ## Attributes Reference In addition to the arguments above, the following attributes are exported: diff --git a/website/docs/r/service_offering_unconstrained.html.markdown b/website/docs/r/service_offering_unconstrained.html.markdown index 4a71140c..5d4bb022 100644 --- a/website/docs/r/service_offering_unconstrained.html.markdown +++ b/website/docs/r/service_offering_unconstrained.html.markdown @@ -29,6 +29,11 @@ resource "cloudstack_service_offering_unconstrained" "unconstrained1" { # disk_offering { ... } # disk_hypervisor { ... } # disk_storage { ... } + # gpu { + # vgpu_profile_id = "..." + # count = 1 + # display = false + # } } ``` @@ -80,6 +85,12 @@ The following arguments are supported: - `max_iops` (Optional) - Max IOPS of the compute offering. - `min_iops` (Optional) - Min IOPS of the compute offering. +#### `gpu` (Optional) + +- `vgpu_profile_id` (Required) - The ID of the vGPU profile to associate with the service offering. +- `count` (Optional, Computed) - The number of GPUs to assign to the guest VM. +- `display` (Optional, Computed) - Whether the GPU is presented as a display device to the guest VM. Defaults to `false`. + ## Attributes Reference In addition to the arguments above, the following attributes are exported: From 718cf0b4ad07370df16c41a602b1347c52431ce6 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Anaparti Date: Tue, 18 Aug 2026 18:24:48 +0530 Subject: [PATCH 5/9] Add CloudStack 4.22+ version check for GPU tests - Add testAccPreCheckGPU to provider_test.go for version validation - Update GPU datasource tests to use testAccPreCheckGPU - Create separate GPU test functions for service offerings - TestAccServiceOfferingFixed_GPU - TestAccServiceOfferingConstrained_GPU - TestAccServiceOfferingUnconstrained_GPU - Tests will skip if CloudStack version < 4.22.0.0 This ensures GPU tests only run on CloudStack versions that support the GPU API (4.22.0+). --- cloudstack/data_source_cloudstack_gpu_card_test.go | 2 +- cloudstack/data_source_cloudstack_vgpu_profile_test.go | 2 +- cloudstack/provider_test.go | 8 ++++++++ cloudstack/service_offering_constrained_resource_test.go | 9 +++++++++ cloudstack/service_offering_fixed_resource_test.go | 9 +++++++++ .../service_offering_unconstrained_resource_test.go | 9 +++++++++ 6 files changed, 37 insertions(+), 2 deletions(-) diff --git a/cloudstack/data_source_cloudstack_gpu_card_test.go b/cloudstack/data_source_cloudstack_gpu_card_test.go index 9d8df63d..db8c850f 100644 --- a/cloudstack/data_source_cloudstack_gpu_card_test.go +++ b/cloudstack/data_source_cloudstack_gpu_card_test.go @@ -27,7 +27,7 @@ import ( func TestAccGpuCardDataSource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheckGPU(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { diff --git a/cloudstack/data_source_cloudstack_vgpu_profile_test.go b/cloudstack/data_source_cloudstack_vgpu_profile_test.go index 20d4aa73..ba369125 100644 --- a/cloudstack/data_source_cloudstack_vgpu_profile_test.go +++ b/cloudstack/data_source_cloudstack_vgpu_profile_test.go @@ -27,7 +27,7 @@ import ( func TestAccVgpuProfileDataSource_basic(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheckGPU(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ { diff --git a/cloudstack/provider_test.go b/cloudstack/provider_test.go index ca47cbfb..8c0f99bb 100644 --- a/cloudstack/provider_test.go +++ b/cloudstack/provider_test.go @@ -206,6 +206,14 @@ func testAccPreCheckStaticRouteNexthop(t *testing.T) { requireMinimumCloudStackVersion(t, minVersionNum, "Static route nexthop parameter") } +// testAccPreCheckGPU checks if the CloudStack version supports GPU features (requires 4.22.0+) +func testAccPreCheckGPU(t *testing.T) { + testAccPreCheck(t) + + const minVersionNum = 4022 // 4.22.0 + requireMinimumCloudStackVersion(t, minVersionNum, "GPU card and vGPU profile support") +} + // newTestClient creates a CloudStack client from environment variables for use in test PreCheck functions. // This is needed because PreCheck functions run before the test framework configures the provider, // so testAccProvider.Meta() is nil at that point. diff --git a/cloudstack/service_offering_constrained_resource_test.go b/cloudstack/service_offering_constrained_resource_test.go index ba03a1ad..042493f7 100644 --- a/cloudstack/service_offering_constrained_resource_test.go +++ b/cloudstack/service_offering_constrained_resource_test.go @@ -72,6 +72,15 @@ func TestAccServiceOfferingConstrained(t *testing.T) { resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.disk_storage", "name", "disk_storage"), ), }, + }, + }) +} + +func TestAccServiceOfferingConstrained_GPU(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckGPU(t) }, + ProtoV6ProviderFactories: testAccMuxProvider, + Steps: []resource.TestStep{ { Config: testAccServiceOfferingCustomConstrained_gpu, Check: resource.ComposeTestCheckFunc( diff --git a/cloudstack/service_offering_fixed_resource_test.go b/cloudstack/service_offering_fixed_resource_test.go index 0f0151a0..a733725e 100644 --- a/cloudstack/service_offering_fixed_resource_test.go +++ b/cloudstack/service_offering_fixed_resource_test.go @@ -66,6 +66,15 @@ func TestAccServiceOfferingFixed(t *testing.T) { resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.disk_storage", "name", "disk_storage"), ), }, + }, + }) +} + +func TestAccServiceOfferingFixed_GPU(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckGPU(t) }, + ProtoV6ProviderFactories: testAccMuxProvider, + Steps: []resource.TestStep{ { Config: testAccServiceOfferingFixed_gpu, Check: resource.ComposeTestCheckFunc( diff --git a/cloudstack/service_offering_unconstrained_resource_test.go b/cloudstack/service_offering_unconstrained_resource_test.go index b5bc5366..e78be2f8 100644 --- a/cloudstack/service_offering_unconstrained_resource_test.go +++ b/cloudstack/service_offering_unconstrained_resource_test.go @@ -66,6 +66,15 @@ func TestAccServiceOfferingUnconstrained(t *testing.T) { resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.disk_storage", "name", "disk_storage"), ), }, + }, + }) +} + +func TestAccServiceOfferingUnconstrained_GPU(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckGPU(t) }, + ProtoV6ProviderFactories: testAccMuxProvider, + Steps: []resource.TestStep{ { Config: testAccServiceOfferingUnconstrained_gpu, Check: resource.ComposeTestCheckFunc( From 2fd96278fb08c5e7c470f48f19285128a59beb5b Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Tue, 18 Aug 2026 07:42:02 -0700 Subject: [PATCH 6/9] fix service offering commonread state --- .gitignore | 3 ++ .../service_offering_constrained_resource.go | 22 +--------- cloudstack/service_offering_fixed_resource.go | 22 +--------- ...service_offering_unconstrained_resource.go | 22 +--------- cloudstack/service_offering_util.go | 41 ++++++++++++++++++- 5 files changed, 46 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index 2c88f78d..0980d2ad 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ website/node_modules *.iml *.test *.iml +dist/ +.gocache/ +.gomodcache/ website/vendor diff --git a/cloudstack/service_offering_constrained_resource.go b/cloudstack/service_offering_constrained_resource.go index 865674d9..4ada5b6e 100644 --- a/cloudstack/service_offering_constrained_resource.go +++ b/cloudstack/service_offering_constrained_resource.go @@ -156,24 +156,8 @@ func (r *serviceOfferingConstrainedResource) Create(ctx context.Context, req res func (r *serviceOfferingConstrainedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state serviceOfferingConstrainedResourceModel - var stateDiskQosHypervisor ServiceOfferingDiskQosHypervisor - var stateDiskOffering ServiceOfferingDiskOffering - var stateDiskQosStorage ServiceOfferingDiskQosStorage - var stateGpu ServiceOfferingGpu resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - if !state.ServiceOfferingDiskQosHypervisor.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskQosHypervisor.As(ctx, &stateDiskQosHypervisor, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingDiskOffering.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskOffering.As(ctx, &stateDiskOffering, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingDiskQosStorage.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskQosStorage.As(ctx, &stateDiskQosStorage, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingGpu.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingGpu.As(ctx, &stateGpu, basetypes.ObjectAsOptions{})...) - } if resp.Diagnostics.HasError() { return } @@ -236,11 +220,7 @@ func (r *serviceOfferingConstrainedResource) Read(ctx context.Context, req resou state.MinMemory = types.Int32Value(int32(i)) } - state.commonRead(ctx, cs) - stateDiskQosHypervisor.commonRead(ctx, cs) - stateDiskOffering.commonRead(ctx, cs) - stateDiskQosStorage.commonRead(ctx, cs) - stateGpu.commonRead(ctx, cs) + resp.Diagnostics.Append(state.commonRead(ctx, cs)...) if resp.Diagnostics.HasError() { return } diff --git a/cloudstack/service_offering_fixed_resource.go b/cloudstack/service_offering_fixed_resource.go index 182b1c01..3bbfd282 100644 --- a/cloudstack/service_offering_fixed_resource.go +++ b/cloudstack/service_offering_fixed_resource.go @@ -133,24 +133,8 @@ func (r *serviceOfferingFixedResource) Create(ctx context.Context, req resource. func (r *serviceOfferingFixedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state serviceOfferingFixedResourceModel - var stateDiskQosHypervisor ServiceOfferingDiskQosHypervisor - var stateDiskOffering ServiceOfferingDiskOffering - var stateDiskQosStorage ServiceOfferingDiskQosStorage - var stateGpu ServiceOfferingGpu resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - if !state.ServiceOfferingDiskQosHypervisor.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskQosHypervisor.As(ctx, &stateDiskQosHypervisor, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingDiskOffering.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskOffering.As(ctx, &stateDiskOffering, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingDiskQosStorage.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskQosStorage.As(ctx, &stateDiskQosStorage, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingGpu.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingGpu.As(ctx, &stateGpu, basetypes.ObjectAsOptions{})...) - } if resp.Diagnostics.HasError() { return } @@ -175,11 +159,7 @@ func (r *serviceOfferingFixedResource) Read(ctx context.Context, req resource.Re state.Memory = types.Int32Value(int32(cs.Memory)) } - state.commonRead(ctx, cs) - stateDiskQosHypervisor.commonRead(ctx, cs) - stateDiskOffering.commonRead(ctx, cs) - stateDiskQosStorage.commonRead(ctx, cs) - stateGpu.commonRead(ctx, cs) + resp.Diagnostics.Append(state.commonRead(ctx, cs)...) if resp.Diagnostics.HasError() { return } diff --git a/cloudstack/service_offering_unconstrained_resource.go b/cloudstack/service_offering_unconstrained_resource.go index a75b257b..fc8a3192 100644 --- a/cloudstack/service_offering_unconstrained_resource.go +++ b/cloudstack/service_offering_unconstrained_resource.go @@ -99,24 +99,8 @@ func (r *serviceOfferingUnconstrainedResource) Create(ctx context.Context, req r func (r *serviceOfferingUnconstrainedResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state serviceOfferingUnconstrainedResourceModel - var stateDiskQosHypervisor ServiceOfferingDiskQosHypervisor - var stateDiskOffering ServiceOfferingDiskOffering - var stateDiskQosStorage ServiceOfferingDiskQosStorage - var stateGpu ServiceOfferingGpu resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - if !state.ServiceOfferingDiskQosHypervisor.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskQosHypervisor.As(ctx, &stateDiskQosHypervisor, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingDiskOffering.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskOffering.As(ctx, &stateDiskOffering, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingDiskQosStorage.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingDiskQosStorage.As(ctx, &stateDiskQosStorage, basetypes.ObjectAsOptions{})...) - } - if !state.ServiceOfferingGpu.IsNull() { - resp.Diagnostics.Append(state.ServiceOfferingGpu.As(ctx, &stateGpu, basetypes.ObjectAsOptions{})...) - } if resp.Diagnostics.HasError() { return } @@ -130,11 +114,7 @@ func (r *serviceOfferingUnconstrainedResource) Read(ctx context.Context, req res return } - state.commonRead(ctx, cs) - stateDiskQosHypervisor.commonRead(ctx, cs) - stateDiskOffering.commonRead(ctx, cs) - stateDiskQosStorage.commonRead(ctx, cs) - stateGpu.commonRead(ctx, cs) + resp.Diagnostics.Append(state.commonRead(ctx, cs)...) if resp.Diagnostics.HasError() { return } diff --git a/cloudstack/service_offering_util.go b/cloudstack/service_offering_util.go index a21231a1..469536b4 100644 --- a/cloudstack/service_offering_util.go +++ b/cloudstack/service_offering_util.go @@ -22,7 +22,9 @@ import ( "strings" "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // ------------------------------------------------------------------------------------------------------------------------------ @@ -72,7 +74,9 @@ func (plan *serviceOfferingCommonResourceModel) commonUpdateParams(ctx context.C // ------------------------------------------------------------------------------------------------------------------------------ // common Read methods // - -func (state *serviceOfferingCommonResourceModel) commonRead(ctx context.Context, cs *cloudstack.ServiceOffering) { +func (state *serviceOfferingCommonResourceModel) commonRead(ctx context.Context, cs *cloudstack.ServiceOffering) diag.Diagnostics { + var diags diag.Diagnostics + state.Id = types.StringValue(cs.Id) if cs.Deploymentplanner != "" { @@ -108,6 +112,41 @@ func (state *serviceOfferingCommonResourceModel) commonRead(ctx context.Context, state.LimitCpuUse = types.BoolValue(cs.Limitcpuuse) state.OfferHa = types.BoolValue(cs.Offerha) + // Refresh the nested blocks and encode them back into state so drift is detected + if !state.ServiceOfferingDiskQosHypervisor.IsNull() { + var v ServiceOfferingDiskQosHypervisor + diags.Append(state.ServiceOfferingDiskQosHypervisor.As(ctx, &v, basetypes.ObjectAsOptions{})...) + v.commonRead(ctx, cs) + obj, d := types.ObjectValueFrom(ctx, state.ServiceOfferingDiskQosHypervisor.AttributeTypes(ctx), v) + diags.Append(d...) + state.ServiceOfferingDiskQosHypervisor = obj + } + if !state.ServiceOfferingDiskOffering.IsNull() { + var v ServiceOfferingDiskOffering + diags.Append(state.ServiceOfferingDiskOffering.As(ctx, &v, basetypes.ObjectAsOptions{})...) + v.commonRead(ctx, cs) + obj, d := types.ObjectValueFrom(ctx, state.ServiceOfferingDiskOffering.AttributeTypes(ctx), v) + diags.Append(d...) + state.ServiceOfferingDiskOffering = obj + } + if !state.ServiceOfferingDiskQosStorage.IsNull() { + var v ServiceOfferingDiskQosStorage + diags.Append(state.ServiceOfferingDiskQosStorage.As(ctx, &v, basetypes.ObjectAsOptions{})...) + v.commonRead(ctx, cs) + obj, d := types.ObjectValueFrom(ctx, state.ServiceOfferingDiskQosStorage.AttributeTypes(ctx), v) + diags.Append(d...) + state.ServiceOfferingDiskQosStorage = obj + } + if !state.ServiceOfferingGpu.IsNull() { + var v ServiceOfferingGpu + diags.Append(state.ServiceOfferingGpu.As(ctx, &v, basetypes.ObjectAsOptions{})...) + v.commonRead(ctx, cs) + obj, d := types.ObjectValueFrom(ctx, state.ServiceOfferingGpu.AttributeTypes(ctx), v) + diags.Append(d...) + state.ServiceOfferingGpu = obj + } + + return diags } func (state *ServiceOfferingDiskQosHypervisor) commonRead(ctx context.Context, cs *cloudstack.ServiceOffering) { From 60816b6ea89ac74fc732ac5321e7ea44e9451420 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:41:22 -0700 Subject: [PATCH 7/9] update gpu data source filtering --- cloudstack/data_source_cloudstack_gpu_card.go | 90 +++++++++---------- .../data_source_cloudstack_gpu_card_test.go | 4 +- .../data_source_cloudstack_vgpu_profile.go | 85 ++++++++---------- website/docs/d/gpu_card.html.markdown | 12 ++- website/docs/d/vgpu_profile.html.markdown | 7 +- 5 files changed, 97 insertions(+), 101 deletions(-) diff --git a/cloudstack/data_source_cloudstack_gpu_card.go b/cloudstack/data_source_cloudstack_gpu_card.go index bb3471f5..b7224bb9 100644 --- a/cloudstack/data_source_cloudstack_gpu_card.go +++ b/cloudstack/data_source_cloudstack_gpu_card.go @@ -22,9 +22,7 @@ package cloudstack import ( "fmt" "log" - "reflect" - "regexp" - "strings" + "strconv" "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -74,25 +72,24 @@ func datasourceCloudStackGpuCardRead(d *schema.ResourceData, meta interface{}) e cs := meta.(*cloudstack.CloudStackClient) p := cs.GPU.NewListGpuCardsParams() + if err := applyGpuCardFilters(p, d.Get("filter").(*schema.Set)); err != nil { + return err + } + csGpuCards, err := cs.GPU.ListGpuCards(p) if err != nil { return fmt.Errorf("failed to list GPU cards: %s", err) } - filters := d.Get("filter").(*schema.Set) - - // Client-side filtering for fields not supported server-side - for _, card := range csGpuCards.GpuCards { - match, err := applyGpuCardFilters(card, filters) - if err != nil { - return err - } - if match { - return gpuCardDescriptionAttributes(d, card) - } + switch len(csGpuCards.GpuCards) { + case 0: + return fmt.Errorf("no GPU cards found") + case 1: + return gpuCardDescriptionAttributes(d, csGpuCards.GpuCards[0]) + default: + return fmt.Errorf("%d GPU cards matched the given filters; "+ + "refine the filters (e.g. add device_id or vendor_id) to match exactly one card", len(csGpuCards.GpuCards)) } - - return fmt.Errorf("no GPU cards found") } func gpuCardDescriptionAttributes(d *schema.ResourceData, card *cloudstack.GpuCard) error { @@ -116,42 +113,41 @@ func gpuCardDescriptionAttributes(d *schema.ResourceData, card *cloudstack.GpuCa return nil } -func applyGpuCardFilters(card *cloudstack.GpuCard, filters *schema.Set) (bool, error) { - val := reflect.ValueOf(card).Elem() - +func applyGpuCardFilters(p *cloudstack.ListGpuCardsParams, filters *schema.Set) error { + seen := make(map[string]bool) for _, f := range filters.List() { filter := f.(map[string]interface{}) - r, err := regexp.Compile(filter["value"].(string)) - if err != nil { - return false, fmt.Errorf("invalid regex: %s", err) - } + name := filter["name"].(string) + value := filter["value"].(string) - filterName := filter["name"].(string) - updatedName := strings.ReplaceAll(filterName, "_", "") - - // Find the field with case-insensitive matching - var cardField reflect.Value - val.FieldByNameFunc(func(fieldName string) bool { - if strings.EqualFold(fieldName, updatedName) { - updatedName = fieldName - cardField = val.FieldByName(fieldName) - return true - } - return false - }) - - // Validate field was found - if !cardField.IsValid() { - return false, fmt.Errorf("unknown filter field '%s'", filterName) + if seen[name] { + return fmt.Errorf("duplicate filter %q; each filter name may only be specified once", name) } - - // Safely convert field value to string - fieldStr := fmt.Sprintf("%v", cardField.Interface()) - - if !r.MatchString(fieldStr) { - return false, nil + seen[name] = true + + switch name { + case "id": + p.SetId(value) + case "device_id": + p.SetDeviceid(value) + case "device_name": + p.SetDevicename(value) + case "vendor_id": + p.SetVendorid(value) + case "vendor_name": + p.SetVendorname(value) + case "keyword": + p.SetKeyword(value) + case "active_only": + b, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean value %q for filter %q: %s", value, name, err) + } + p.SetActiveonly(b) + default: + return fmt.Errorf("unsupported filter %q; supported filters: id, device_id, device_name, vendor_id, vendor_name, keyword, active_only", name) } } - return true, nil + return nil } diff --git a/cloudstack/data_source_cloudstack_gpu_card_test.go b/cloudstack/data_source_cloudstack_gpu_card_test.go index db8c850f..d2545b46 100644 --- a/cloudstack/data_source_cloudstack_gpu_card_test.go +++ b/cloudstack/data_source_cloudstack_gpu_card_test.go @@ -43,8 +43,8 @@ func TestAccGpuCardDataSource_basic(t *testing.T) { const testGpuCardDataSourceConfig_basic = ` data "cloudstack_gpu_card" "test" { filter { - name = "name" - value = "NVIDIA.*" + name = "keyword" + value = "NVIDIA" } } ` diff --git a/cloudstack/data_source_cloudstack_vgpu_profile.go b/cloudstack/data_source_cloudstack_vgpu_profile.go index bf41d970..453af4fb 100644 --- a/cloudstack/data_source_cloudstack_vgpu_profile.go +++ b/cloudstack/data_source_cloudstack_vgpu_profile.go @@ -22,9 +22,7 @@ package cloudstack import ( "fmt" "log" - "reflect" - "regexp" - "strings" + "strconv" "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -114,24 +112,24 @@ func datasourceCloudStackVgpuProfileRead(d *schema.ResourceData, meta interface{ cs := meta.(*cloudstack.CloudStackClient) p := cs.GPU.NewListVgpuProfilesParams() + if err := applyVgpuProfileFilters(p, d.Get("filter").(*schema.Set)); err != nil { + return err + } + csVgpuProfiles, err := cs.GPU.ListVgpuProfiles(p) if err != nil { return fmt.Errorf("failed to list vGPU profiles: %s", err) } - filters := d.Get("filter").(*schema.Set) - - for _, profile := range csVgpuProfiles.VgpuProfiles { - match, err := applyVgpuProfileFilters(profile, filters) - if err != nil { - return err - } - if match { - return vgpuProfileDescriptionAttributes(d, profile) - } + switch len(csVgpuProfiles.VgpuProfiles) { + case 0: + return fmt.Errorf("no vGPU profiles found") + case 1: + return vgpuProfileDescriptionAttributes(d, csVgpuProfiles.VgpuProfiles[0]) + default: + return fmt.Errorf("%d vGPU profiles matched the given filters; "+ + "refine the filters (e.g. add gpu_card_id) to match exactly one profile", len(csVgpuProfiles.VgpuProfiles)) } - - return fmt.Errorf("no vGPU profiles found") } func vgpuProfileDescriptionAttributes(d *schema.ResourceData, profile *cloudstack.VgpuProfile) error { @@ -163,42 +161,37 @@ func vgpuProfileDescriptionAttributes(d *schema.ResourceData, profile *cloudstac return nil } -func applyVgpuProfileFilters(profile *cloudstack.VgpuProfile, filters *schema.Set) (bool, error) { - val := reflect.ValueOf(profile).Elem() - +func applyVgpuProfileFilters(p *cloudstack.ListVgpuProfilesParams, filters *schema.Set) error { + seen := make(map[string]bool) for _, f := range filters.List() { filter := f.(map[string]interface{}) - r, err := regexp.Compile(filter["value"].(string)) - if err != nil { - return false, fmt.Errorf("invalid regex: %s", err) - } + name := filter["name"].(string) + value := filter["value"].(string) - filterName := filter["name"].(string) - updatedName := strings.ReplaceAll(filterName, "_", "") - - // Find the field with case-insensitive matching - var profileField reflect.Value - val.FieldByNameFunc(func(fieldName string) bool { - if strings.EqualFold(fieldName, updatedName) { - updatedName = fieldName - profileField = val.FieldByName(fieldName) - return true - } - return false - }) - - // Validate field was found - if !profileField.IsValid() { - return false, fmt.Errorf("unknown filter field '%s'", filterName) + if seen[name] { + return fmt.Errorf("duplicate filter %q; each filter name may only be specified once", name) } - - // Safely convert field value to string - fieldStr := fmt.Sprintf("%v", profileField.Interface()) - - if !r.MatchString(fieldStr) { - return false, nil + seen[name] = true + + switch name { + case "id": + p.SetId(value) + case "name": + p.SetName(value) + case "gpu_card_id": + p.SetGpucardid(value) + case "keyword": + p.SetKeyword(value) + case "active_only": + b, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid boolean value %q for filter %q: %s", value, name, err) + } + p.SetActiveonly(b) + default: + return fmt.Errorf("unsupported filter %q; supported filters: id, name, gpu_card_id, keyword, active_only", name) } } - return true, nil + return nil } diff --git a/website/docs/d/gpu_card.html.markdown b/website/docs/d/gpu_card.html.markdown index d48d18c9..087a5d8a 100644 --- a/website/docs/d/gpu_card.html.markdown +++ b/website/docs/d/gpu_card.html.markdown @@ -14,8 +14,8 @@ Use this data source to get information about a GPU card for use in other resour ```hcl data "cloudstack_gpu_card" "card" { filter { - name = "name" - value = "NVIDIA.*" + name = "keyword" + value = "NVIDIA" } } @@ -32,8 +32,12 @@ The following arguments are supported: ### Filter Arguments -* `name` - (Required) The name of the field to filter on. This can be any of the fields returned by the CloudStack API. -* `value` - (Required) The value to filter on. This should be a regular expression. +* `name` - (Required) The name of the field to filter on. Filtering is performed server-side by the + CloudStack API. Supported values are `id`, `device_id`, `device_name`, `vendor_id`, `vendor_name`, + `keyword`, and `active_only`. +* `value` - (Required) The value to filter on. This is passed directly to the CloudStack API and + matched exactly (not as a regular expression). The filters must narrow the result to a single GPU + card; if more than one card matches, an error is returned. ## Attributes Reference diff --git a/website/docs/d/vgpu_profile.html.markdown b/website/docs/d/vgpu_profile.html.markdown index 02bb4ea4..c266892d 100644 --- a/website/docs/d/vgpu_profile.html.markdown +++ b/website/docs/d/vgpu_profile.html.markdown @@ -32,8 +32,11 @@ The following arguments are supported: ### Filter Arguments -* `name` - (Required) The name of the field to filter on. This can be any of the fields returned by the CloudStack API. -* `value` - (Required) The value to filter on. This should be a regular expression. +* `name` - (Required) The name of the field to filter on. Filtering is performed server-side by the + CloudStack API. Supported values are `id`, `name`, `gpu_card_id`, `keyword`, and `active_only`. +* `value` - (Required) The value to filter on. This is passed directly to the CloudStack API and + matched exactly (not as a regular expression). The filters must narrow the result to a single + vGPU profile; if more than one profile matches, an error is returned. ## Attributes Reference From ee03153d6528e7a4dee45264df4a14e432d278b5 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:49:09 -0700 Subject: [PATCH 8/9] Fix gpu tests name vs. id --- .../service_offering_constrained_resource_test.go | 11 +++++++++-- cloudstack/service_offering_fixed_resource_test.go | 11 +++++++++-- .../service_offering_unconstrained_resource_test.go | 11 +++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/cloudstack/service_offering_constrained_resource_test.go b/cloudstack/service_offering_constrained_resource_test.go index 042493f7..6744a6aa 100644 --- a/cloudstack/service_offering_constrained_resource_test.go +++ b/cloudstack/service_offering_constrained_resource_test.go @@ -85,7 +85,7 @@ func TestAccServiceOfferingConstrained_GPU(t *testing.T) { Config: testAccServiceOfferingCustomConstrained_gpu, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "name", "gpu"), - resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "gpu.vgpu_profile_id", "a6000-8a-profile"), + resource.TestCheckResourceAttrPair("cloudstack_service_offering_constrained.gpu", "gpu.vgpu_profile_id", "data.cloudstack_vgpu_profile.test", "id"), resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "gpu.count", "1"), resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "gpu.display", "true"), ), @@ -293,6 +293,13 @@ resource "cloudstack_service_offering_constrained" "disk_hypervisor" { ` const testAccServiceOfferingCustomConstrained_gpu = ` +data "cloudstack_vgpu_profile" "test" { + filter { + name = "name" + value = "passthrough" + } +} + resource "cloudstack_service_offering_constrained" "gpu" { display_text = "gpu" name = "gpu" @@ -318,7 +325,7 @@ resource "cloudstack_service_offering_constrained" "gpu" { offer_ha = false gpu = { - vgpu_profile_id = "a6000-8a-profile" + vgpu_profile_id = data.cloudstack_vgpu_profile.test.id count = 1 display = true } diff --git a/cloudstack/service_offering_fixed_resource_test.go b/cloudstack/service_offering_fixed_resource_test.go index a733725e..d20da855 100644 --- a/cloudstack/service_offering_fixed_resource_test.go +++ b/cloudstack/service_offering_fixed_resource_test.go @@ -79,7 +79,7 @@ func TestAccServiceOfferingFixed_GPU(t *testing.T) { Config: testAccServiceOfferingFixed_gpu, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "name", "gpu"), - resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "gpu.vgpu_profile_id", "a6000-8a-profile"), + resource.TestCheckResourceAttrPair("cloudstack_service_offering_fixed.gpu", "gpu.vgpu_profile_id", "data.cloudstack_vgpu_profile.test", "id"), resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "gpu.count", "1"), resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "gpu.display", "true"), ), @@ -257,6 +257,13 @@ resource "cloudstack_service_offering_fixed" "disk_storage" { ` const testAccServiceOfferingFixed_gpu = ` +data "cloudstack_vgpu_profile" "test" { + filter { + name = "name" + value = "passthrough" + } +} + resource "cloudstack_service_offering_fixed" "gpu" { display_text = "gpu" name = "gpu" @@ -277,7 +284,7 @@ resource "cloudstack_service_offering_fixed" "gpu" { offer_ha = false gpu = { - vgpu_profile_id = "a6000-8a-profile" + vgpu_profile_id = data.cloudstack_vgpu_profile.test.id count = 1 display = true } diff --git a/cloudstack/service_offering_unconstrained_resource_test.go b/cloudstack/service_offering_unconstrained_resource_test.go index e78be2f8..37b2c5de 100644 --- a/cloudstack/service_offering_unconstrained_resource_test.go +++ b/cloudstack/service_offering_unconstrained_resource_test.go @@ -79,7 +79,7 @@ func TestAccServiceOfferingUnconstrained_GPU(t *testing.T) { Config: testAccServiceOfferingUnconstrained_gpu, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "name", "gpu"), - resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "gpu.vgpu_profile_id", "a6000-8a-profile"), + resource.TestCheckResourceAttrPair("cloudstack_service_offering_unconstrained.gpu", "gpu.vgpu_profile_id", "data.cloudstack_vgpu_profile.test", "id"), resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "gpu.count", "1"), resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "gpu.display", "true"), ), @@ -225,6 +225,13 @@ resource "cloudstack_service_offering_unconstrained" "disk_storage" { ` const testAccServiceOfferingUnconstrained_gpu = ` +data "cloudstack_vgpu_profile" "test" { + filter { + name = "name" + value = "passthrough" + } +} + resource "cloudstack_service_offering_unconstrained" "gpu" { display_text = "gpu" name = "gpu" @@ -239,7 +246,7 @@ resource "cloudstack_service_offering_unconstrained" "gpu" { offer_ha = true gpu = { - vgpu_profile_id = "a6000-8a-profile" + vgpu_profile_id = data.cloudstack_vgpu_profile.test.id count = 1 display = true } From 8384121b5c8a57cc92993471a1d4af9f7364cde1 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:47:45 -0700 Subject: [PATCH 9/9] fix computed gpu count --- cloudstack/service_offering_schema.go | 2 ++ cloudstack/service_offering_util.go | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cloudstack/service_offering_schema.go b/cloudstack/service_offering_schema.go index 19c019f5..d8ff49b1 100644 --- a/cloudstack/service_offering_schema.go +++ b/cloudstack/service_offering_schema.go @@ -27,6 +27,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -254,6 +255,7 @@ func serviceOfferingMergeCommonSchema(s1 map[string]schema.Attribute) map[string PlanModifiers: []planmodifier.Int32{ int32planmodifier.RequiresReplace(), }, + Default: int32default.StaticInt32(0), }, "display": schema.BoolAttribute{ Description: "whether the GPU is presented as a display device to the guest VM", diff --git a/cloudstack/service_offering_util.go b/cloudstack/service_offering_util.go index 469536b4..6b5de5a8 100644 --- a/cloudstack/service_offering_util.go +++ b/cloudstack/service_offering_util.go @@ -215,11 +215,7 @@ func (state *ServiceOfferingGpu) commonRead(ctx context.Context, cs *cloudstack. } else { state.VgpuProfileId = types.StringNull() } - if cs.Gpucount > 0 { - state.Count = types.Int32Value(int32(cs.Gpucount)) - } else { - state.Count = types.Int32Null() - } + state.Count = types.Int32Value(int32(cs.Gpucount)) state.Display = types.BoolValue(cs.Gpudisplay) }