Skip to content

Add GPU support: gpu_card / vgpu_profile data sources and GPU on service offerings - #315

Open
poddm wants to merge 10 commits into
apache:mainfrom
poddm:mp/service_gpus_clean
Open

Add GPU support: gpu_card / vgpu_profile data sources and GPU on service offerings#315
poddm wants to merge 10 commits into
apache:mainfrom
poddm:mp/service_gpus_clean

Conversation

@poddm

@poddm poddm commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds GPU support to the provider, aligned with the GPU/vGPU APIs introduced in
recent CloudStack releases. This lets operators discover GPU cards and vGPU
profiles and attach GPUs to service offerings.

What's included

New data sources

  • cloudstack_gpu_card — looks up a GPU card via the listGpuCards API.
    Exposes id, name, device_id, device_name, vendor_id, vendor_name.

  • cloudstack_vgpu_profile — looks up a vGPU profile via the listVgpuProfiles
    API. Exposes id, name, description, device_id, device_name,
    gpu_card_id, gpu_card_name, max_heads, max_resolution_x,
    max_resolution_y, max_vgpu_per_physical_gpu, vendor_id, vendor_name,
    video_ram.

    Both data sources support the standard filter block (regex matching on
    returned fields).

Service offering GPU configuration

  • Adds an optional gpu nested block to the service offering resources
    (constrained / fixed / unconstrained), with:
    • vgpu_profile_id (required) — vGPU profile to associate with the offering
    • count (optional) — number of GPUs assigned to the guest VM
    • display (optional) — whether the GPU is presented as a display device
    • All three force replacement when changed.

Docs

  • Website documentation for gpu_card and vgpu_profile data sources.

Dependency

  • Bumps github.com/apache/cloudstack-go/v2 from v2.18.1 to v2.19.1 for the
    GPU API bindings.

Example usage

data "cloudstack_gpu_card" "main" {
  filter {
    name  = "name"
    value = "Example Corp EX100GL \\[ExampleGPU 32GB\\]"
  }
}

data "cloudstack_vgpu_profile" "main" {
  filter {
    name  = "gpu_card_id"
    value = data.cloudstack_gpu_card.main.id
  }
  filter {
    name  = "name"
    value = "passthrough"
  }
}

resource "cloudstack_service_offering_constrained" "example" {
  name         = "example.gpu.offering"
  display_text = "Example GPU offering, vCPU 2-32, Memory 2G-128G"

  // compute
  cpu_speed      = 1024
  max_cpu_number = 32
  min_cpu_number = 2
  max_memory     = 131072
  min_memory     = 2048
  network_rate   = 10000

  // other
  disk_offering_id = var.disk_offering_id
  zone_ids         = var.zone_ids
  tags             = "EXAMPLE_STORAGE"
  host_tags        = "EXAMPLE_GPU"

  // Feature flags
  dynamic_scaling_enabled = true
  is_volatile             = false
  limit_cpu_use           = false
  offer_ha                = true

  gpu = {
    vgpu_profile_id = data.cloudstack_vgpu_profile.main.id
    count           = 1
    display         = true
  }
}

poddm added 2 commits August 12, 2026 12:57
(cherry picked from commit 0f8167f)
(cherry picked from commit b6de735)
@vishesh92
vishesh92 requested a lite review from Copilot August 17, 2026 09:56
},
"count": schema.Int32Attribute{
Description: "the number of GPUs to assign to the guest VM",
Optional: true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we set a default value or computed here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

"display": schema.BoolAttribute{
Description: "whether the GPU is presented as a display device to the guest VM",
Optional: true,
PlanModifiers: []planmodifier.Bool{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we set a default value or computed here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@vishesh92

Copy link
Copy Markdown
Member

@poddm can you resolve the conflicts?
Can you also update the service offering documentation?
For the datasources, we seem to be filtering on the client side for some fields. It would be better to do the filtering on server side instead.
Ref:
https://cloudstack.apache.org/api/apidocs-4.22/apis/listGpuCards.html
https://cloudstack.apache.org/api/apidocs-4.22/apis/listVgpuProfiles.html

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds provider support for CloudStack GPU/vGPU discovery and service offering GPU configuration, aligning with newer CloudStack GPU APIs.

Changes:

  • Introduces cloudstack_gpu_card and cloudstack_vgpu_profile data sources with filter support.
  • Adds an optional gpu nested block to service offering resources (constrained/fixed/unconstrained).
  • Updates docs and bumps cloudstack-go dependency to include GPU API bindings.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
website/docs/d/vgpu_profile.html.markdown Adds documentation for the vGPU profile data source and its exported attributes.
website/docs/d/gpu_card.html.markdown Adds documentation for the GPU card data source and its exported attributes.
go.mod Bumps github.com/apache/cloudstack-go/v2 to a version that includes GPU bindings.
go.sum Updates module checksums for the dependency bump.
cloudstack/provider.go Registers the new GPU-related data sources in the provider.
cloudstack/data_source_cloudstack_vgpu_profile.go Implements the cloudstack_vgpu_profile data source and filtering logic.
cloudstack/data_source_cloudstack_vgpu_profile_test.go Adds acceptance test coverage for the vGPU profile data source.
cloudstack/data_source_cloudstack_gpu_card.go Implements the cloudstack_gpu_card data source and filtering logic.
cloudstack/data_source_cloudstack_gpu_card_test.go Adds acceptance test coverage for the GPU card data source.
cloudstack/service_offering_schema.go Adds the gpu nested block schema shared by service offering resources.
cloudstack/service_offering_models.go Adds the ServiceOfferingGpu model and wires it into the common resource model.
cloudstack/service_offering_util.go Adds common read/create param helpers for the service offering gpu block.
cloudstack/service_offering_constrained_resource.go Wires gpu block into constrained service offering create/read flows.
cloudstack/service_offering_constrained_resource_test.go Adds acceptance test coverage for constrained service offering GPU configuration.
cloudstack/service_offering_fixed_resource.go Wires gpu block into fixed service offering create/read flows.
cloudstack/service_offering_fixed_resource_test.go Adds acceptance test coverage for fixed service offering GPU configuration.
cloudstack/service_offering_unconstrained_resource.go Wires gpu block into unconstrained service offering create/read flows.
cloudstack/service_offering_unconstrained_resource_test.go Adds acceptance test coverage for unconstrained service offering GPU configuration.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +166 to +187
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
}
}
Comment on lines +118 to +139
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
}
}
Comment on lines +175 to +178
state.VgpuProfileId = types.StringValue(cs.Vgpuprofileid)
}
if cs.Gpucount > 0 {
state.Count = types.Int32Value(int32(cs.Gpucount))
}
}

return fmt.Errorf("no vGPU profiles found")
}
}

return fmt.Errorf("no GPU cards found")
Comment on lines +218 to +238
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
}
}
`
@sureshanaparti sureshanaparti added this to the v0.7.0 milestone Aug 17, 2026
@sudo87 sudo87 removed this from the v0.7.0 milestone Aug 18, 2026
- 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
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.
@sureshanaparti
sureshanaparti requested review from vishesh92 and a lite review from Copilot August 18, 2026 12:49
- 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+).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Suppressed comments (5)

cloudstack/data_source_cloudstack_vgpu_profile.go:174

  • The regex is compiled for every filter evaluation and (as written) would be recompiled for every item in the API response. Precompile the regex patterns once per read (e.g., build a slice/map of compiled filters before iterating the returned profiles) to avoid repeated compilation overhead, especially if listVgpuProfiles returns many entries.
	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)
		}

cloudstack/data_source_cloudstack_gpu_card.go:127

  • The regex is compiled inside the filter application loop, which is called once per returned GPU card. Consider compiling all filter regexes once (before iterating csGpuCards.GpuCards) and reusing them for each card to reduce CPU overhead.
	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)
		}

website/docs/r/service_offering_constrained.html.markdown:45

  • The docs use block syntax (gpu { ... }), but the acceptance tests and PR description use object assignment (gpu = { ... }). Since the schema is implemented as a schema.SingleNestedAttribute (object attribute), the docs should match the correct configuration style (or the schema should be changed to a nested block type if block syntax is intended). Please update the service offering docs consistently (constrained/fixed/unconstrained) to avoid user confusion.
	gpu {
		vgpu_profile_id = "gpu-profile-uuid"
		count           = 1
		display         = false
	}

cloudstack/service_offering_schema.go:257

  • gpu.count should be validated to prevent invalid values (e.g., 0 or negative), which CloudStack is unlikely to accept for a GPU assignment count. Add an Int32 validator (e.g., at least 1) so bad configs fail fast during planning.
				"count": schema.Int32Attribute{
					Description: "the number of GPUs to assign to the guest VM",
					Optional:    true,
					Computed:    true,
					PlanModifiers: []planmodifier.Int32{
						int32planmodifier.RequiresReplace(),
					},
				},

cloudstack/service_offering_schema.go:266

  • In terraform-plugin-framework schemas, combining Computed: true with Default: ... is typically invalid/unsupported because defaults apply to optional attributes while computed values are set by the provider. Consider removing Computed: true (keep Optional + Default) or removing the Default and handling unknowns via plan modifiers/state to avoid schema validation/runtime errors.
				"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),
				},

Comment on lines +180 to +193
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)
}
Comment on lines +133 to +147
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)
}

@sudo87

sudo87 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
  1. Dead code: stateGpu.commonRead() results are discarded — state.ServiceOfferingGpu is never written back before resp.State.Set, so the drift-detection fix does nothing.
  2. Host-scoped vGPU IDs: data source returns an arbitrary per-host profile UUID with no host/zone scope or ambiguity check, so offerings get IDs that won't match the deployment host.
  3. Name vs ID confusion: docs feed vgpu_profile_id from data.cloudstack_vgpu_profile.id, tests feed the profile name — undefined, contradictory contract.
  4. False justification: listVgpuProfiles/listGpuCards DO support server-side filters (name, gpucardid, vendorid, ...) — the reflection filter layer is unjustified.

@poddm
poddm force-pushed the mp/service_gpus_clean branch from e449cd9 to 2fd9627 Compare August 18, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants