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/data_source_cloudstack_gpu_card.go b/cloudstack/data_source_cloudstack_gpu_card.go new file mode 100644 index 00000000..b7224bb9 --- /dev/null +++ b/cloudstack/data_source_cloudstack_gpu_card.go @@ -0,0 +1,153 @@ +// +// 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" + "strconv" + + "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() + + 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) + } + + 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)) + } +} + +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(p *cloudstack.ListGpuCardsParams, filters *schema.Set) error { + seen := make(map[string]bool) + for _, f := range filters.List() { + filter := f.(map[string]interface{}) + name := filter["name"].(string) + value := filter["value"].(string) + + if seen[name] { + return fmt.Errorf("duplicate filter %q; each filter name may only be specified once", name) + } + 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 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..d2545b46 --- /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() { testAccPreCheckGPU(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 = "keyword" + 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..453af4fb --- /dev/null +++ b/cloudstack/data_source_cloudstack_vgpu_profile.go @@ -0,0 +1,197 @@ +// +// 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" + "strconv" + + "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() + + 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) + } + + 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)) + } +} + +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(p *cloudstack.ListVgpuProfilesParams, filters *schema.Set) error { + seen := make(map[string]bool) + for _, f := range filters.List() { + filter := f.(map[string]interface{}) + name := filter["name"].(string) + value := filter["value"].(string) + + if seen[name] { + return fmt.Errorf("duplicate filter %q; each filter name may only be specified once", name) + } + 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 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..ba369125 --- /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() { testAccPreCheckGPU(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 88756f69..73308da3 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -107,6 +107,8 @@ func Provider() *schema.Provider { "cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(), "cloudstack_user_data": dataSourceCloudstackUserData(), "cloudstack_kubernetes_cluster_config": dataSourceCloudstackKubernetesClusterConfig(), + "cloudstack_vgpu_profile": dataSourceCloudstackVgpuProfile(), + "cloudstack_gpu_card": dataSourceCloudstackGpuCard(), }, ResourcesMap: map[string]*schema.Resource{ 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.go b/cloudstack/service_offering_constrained_resource.go index 92c80779..4ada5b6e 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() { @@ -151,20 +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 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 resp.Diagnostics.HasError() { return } @@ -227,10 +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) + resp.Diagnostics.Append(state.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..6744a6aa 100644 --- a/cloudstack/service_offering_constrained_resource_test.go +++ b/cloudstack/service_offering_constrained_resource_test.go @@ -76,6 +76,24 @@ func TestAccServiceOfferingConstrained(t *testing.T) { }) } +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( + resource.TestCheckResourceAttr("cloudstack_service_offering_constrained.gpu", "name", "gpu"), + 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"), + ), + }, + }, + }) +} + const testAccServiceOfferingCustomConstrained1 = ` resource "cloudstack_zone" "test" { name = "acctest" @@ -274,6 +292,46 @@ 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" + + // 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 = data.cloudstack_vgpu_profile.test.id + 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..3bbfd282 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() { @@ -128,20 +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 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 resp.Diagnostics.HasError() { return } @@ -166,10 +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) + resp.Diagnostics.Append(state.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..d20da855 100644 --- a/cloudstack/service_offering_fixed_resource_test.go +++ b/cloudstack/service_offering_fixed_resource_test.go @@ -70,6 +70,24 @@ func TestAccServiceOfferingFixed(t *testing.T) { }) } +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( + resource.TestCheckResourceAttr("cloudstack_service_offering_fixed.gpu", "name", "gpu"), + 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"), + ), + }, + }, + }) +} + const testAccServiceOfferingFixed1 = ` resource "cloudstack_service_offering_fixed" "fixed1" { display_text = "fixed1" @@ -237,3 +255,38 @@ 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" + + // 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 = data.cloudstack_vgpu_profile.test.id + 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..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" @@ -237,6 +238,36 @@ 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, + Computed: true, + 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", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), + }, + }, + }, } for key, value := range s1 { diff --git a/cloudstack/service_offering_unconstrained_resource.go b/cloudstack/service_offering_unconstrained_resource.go index 98b937cd..fc8a3192 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) @@ -94,20 +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 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 resp.Diagnostics.HasError() { return } @@ -121,10 +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) + resp.Diagnostics.Append(state.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..37b2c5de 100644 --- a/cloudstack/service_offering_unconstrained_resource_test.go +++ b/cloudstack/service_offering_unconstrained_resource_test.go @@ -70,6 +70,24 @@ func TestAccServiceOfferingUnconstrained(t *testing.T) { }) } +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( + resource.TestCheckResourceAttr("cloudstack_service_offering_unconstrained.gpu", "name", "gpu"), + 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"), + ), + }, + }, + }) +} + const testAccServiceOfferingUnconstrained1 = ` resource "cloudstack_service_offering_unconstrained" "unconstrained1" { display_text = "unconstrained1" @@ -205,3 +223,32 @@ 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" + + 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 = data.cloudstack_vgpu_profile.test.id + count = 1 + display = true + } +} +` diff --git a/cloudstack/service_offering_util.go b/cloudstack/service_offering_util.go index 666e0fce..6b5de5a8 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) { @@ -170,6 +209,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) + } else { + state.VgpuProfileId = types.StringNull() + } + state.Count = types.Int32Value(int32(cs.Gpucount)) + state.Display = types.BoolValue(cs.Gpudisplay) +} + // ------------------------------------------------------------------------------------------------------------------------------ // common Create methods // - @@ -278,3 +327,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 +} diff --git a/website/docs/d/gpu_card.html.markdown b/website/docs/d/gpu_card.html.markdown new file mode 100644 index 00000000..087a5d8a --- /dev/null +++ b/website/docs/d/gpu_card.html.markdown @@ -0,0 +1,51 @@ +--- +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 = "keyword" + 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. 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 + +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..c266892d --- /dev/null +++ b/website/docs/d/vgpu_profile.html.markdown @@ -0,0 +1,58 @@ +--- +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. 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 + +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. 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: