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/2] 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/2] 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.