From 350325c0dfc86e7afbf65a02cddf6ac869820357 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:43:00 -0700 Subject: [PATCH 1/8] disk offering updates --- .../resource_cloudstack_disk_offering.go | 415 +++++++++++++++--- 1 file changed, 346 insertions(+), 69 deletions(-) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 1d3c5d45..4cfdb2fb 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -22,6 +22,7 @@ package cloudstack import ( "fmt" "log" + "strings" "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -47,49 +48,89 @@ func resourceCloudStackDiskOffering() *schema.Resource { Type: schema.TypeString, Required: true, }, + "cache_mode": { + Description: "The cache mode to use for this disk offering. Values are none, writeback or writethrough", + Type: schema.TypeString, + Optional: true, + }, "disk_size": { - Description: "The size of the disk offering in GB", - Type: schema.TypeInt, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"customized"}, - }, - "customized": { - Description: "Whether the disk offering allows a custom disk size at deployment time", - Type: schema.TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"disk_size"}, + Description: "The size of the disk offering in GB. When omitted the offering is created as customizable", + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, }, - "storage_type": { - Description: "The storage type of the disk offering. Values are local and shared", + "disk_offering_strictness": { + Description: "Whether the disk offering size is strictly enforced and cannot be changed at deployment time", + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "domain_id": { + Description: "The IDs of the domains that can use this disk offering", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "iops_read_rate": { + Description: "The IOPS read rate of the disk offering", + Type: schema.TypeInt, + Optional: true, + }, + "iops_read_rate_max": { + Description: "The maximum IOPS read rate of the disk offering", + Type: schema.TypeInt, + Optional: true, + }, + "iops_read_rate_max_length": { + Description: "The length (in seconds) of the maximum IOPS read rate burst", + Type: schema.TypeInt, + Optional: true, + }, + "iops_write_rate": { + Description: "The IOPS write rate of the disk offering", + Type: schema.TypeInt, + Optional: true, + }, + "iops_write_rate_max": { + Description: "The maximum IOPS write rate of the disk offering", + Type: schema.TypeInt, + Optional: true, + }, + "iops_write_rate_max_length": { + Description: "The length (in seconds) of the maximum IOPS write rate burst", + Type: schema.TypeInt, + Optional: true, + }, + "provisioning_type": { + Description: "Provisioning type used to create volumes. Values are thin, sparse and fat", Type: schema.TypeString, Optional: true, ForceNew: true, - Default: "shared", + Default: "thin", ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { v := val.(string) - if v == "local" || v == "shared" { + if v == "thin" || v == "sparse" || v == "fat" { return } - errs = append(errs, fmt.Errorf("storage type should be either local or shared, got %s", v)) + errs = append(errs, fmt.Errorf("provisioning type should be one of thin, sparse or fat, got %s", v)) return }, }, - "provisioning_type": { - Description: "Provisioning type used to create volumes. Values are thin, sparse and fat", + "storage_type": { + Description: "The storage type of the disk offering. Values are local and shared", Type: schema.TypeString, Optional: true, ForceNew: true, - Default: "thin", + Default: "shared", ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { v := val.(string) - if v == "thin" || v == "sparse" || v == "fat" { + if v == "local" || v == "shared" { return } - errs = append(errs, fmt.Errorf("provisioning type should be one of thin, sparse or fat, got %s", v)) + errs = append(errs, fmt.Errorf("storage type should be either local or shared, got %s", v)) return }, }, @@ -104,53 +145,210 @@ func resourceCloudStackDiskOffering() *schema.Resource { Optional: true, Default: true, }, + "zone_id": { + Description: "The IDs of the zones that this disk offering belongs to", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "hypervisor": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bytes_read_rate": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "bytes_read_rate_max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "bytes_read_rate_max_length": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "bytes_write_rate": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "bytes_write_rate_max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "bytes_write_rate_max_length": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "storage": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "min_iops": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "max_iops": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "customized_iops": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + ForceNew: true, + }, + "hypervisor_snapshot_reserve": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + }, + }, + }, }, } } func resourceCloudStackDiskOfferingCreate(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) + name := d.Get("name").(string) displayText := d.Get("display_text").(string) - // Create a new parameter struct + // NewCreateDiskOfferingParams expects (displaytext, name) in that order p := cs.DiskOffering.NewCreateDiskOfferingParams(displayText, name) + if v, ok := d.GetOk("cache_mode"); ok { + p.SetCachemode(v.(string)) + } if v, ok := d.GetOk("disk_size"); ok { p.SetDisksize(int64(v.(int))) + p.SetCustomized(false) + } else { + p.SetCustomized(true) } - - customized := false - if v, ok := d.GetOk("customized"); ok { - customized = v.(bool) + if v, ok := d.GetOk("disk_offering_strictness"); ok { + p.SetDisksizestrictness(v.(bool)) } - if _, ok := d.GetOk("disk_size"); !ok { - customized = true + if v, ok := d.GetOk("domain_id"); ok { + domainIDs := v.([]interface{}) + items := make([]string, len(domainIDs)) + for i, raw := range domainIDs { + items[i] = raw.(string) + } + p.SetDomainid(items) } - p.SetCustomized(customized) - - if v, ok := d.GetOk("storage_type"); ok { - p.SetStoragetype(v.(string)) + if v, ok := d.GetOk("iops_read_rate"); ok { + p.SetIopsreadrate(int64(v.(int))) + } + if v, ok := d.GetOk("iops_read_rate_max"); ok { + p.SetIopsreadratemax(int64(v.(int))) + } + if v, ok := d.GetOk("iops_read_rate_max_length"); ok { + p.SetIopsreadratemaxlength(int64(v.(int))) + } + if v, ok := d.GetOk("iops_write_rate"); ok { + p.SetIopswriterate(int64(v.(int))) + } + if v, ok := d.GetOk("iops_write_rate_max"); ok { + p.SetIopswriteratemax(int64(v.(int))) + } + if v, ok := d.GetOk("iops_write_rate_max_length"); ok { + p.SetIopswriteratemaxlength(int64(v.(int))) } - if v, ok := d.GetOk("provisioning_type"); ok { p.SetProvisioningtype(v.(string)) } - + if v, ok := d.GetOk("storage_type"); ok { + p.SetStoragetype(v.(string)) + } if v, ok := d.GetOk("tags"); ok { p.SetTags(v.(string)) } - // display_offering defaults to true, so read it directly rather than via GetOk p.SetDisplayoffering(d.Get("display_offering").(bool)) + if v, ok := d.GetOk("zone_id"); ok { + zoneIDs := v.([]interface{}) + items := make([]string, len(zoneIDs)) + for i, raw := range zoneIDs { + items[i] = raw.(string) + } + p.SetZoneid(items) + } + + // storage qos + if v, ok := d.GetOk("storage"); ok { + storageList := v.([]interface{}) + if len(storageList) > 0 && storageList[0] != nil { + storage := storageList[0].(map[string]interface{}) + + if v2, ok2 := storage["min_iops"]; ok2 { + p.SetMiniops(int64(v2.(int))) + } + if v2, ok2 := storage["max_iops"]; ok2 { + p.SetMaxiops(int64(v2.(int))) + } + if v2, ok2 := storage["customized_iops"]; ok2 { + p.SetCustomizediops(v2.(bool)) + } + if v2, ok2 := storage["hypervisor_snapshot_reserve"]; ok2 { + p.SetHypervisorsnapshotreserve(v2.(int)) + } + } + } + + // hypervisor qos + if v, ok := d.GetOk("hypervisor"); ok { + hypervisorList := v.([]interface{}) + if len(hypervisorList) > 0 && hypervisorList[0] != nil { + hypervisor := hypervisorList[0].(map[string]interface{}) + + if v2, ok2 := hypervisor["bytes_read_rate"]; ok2 { + p.SetBytesreadrate(int64(v2.(int))) + } + if v2, ok2 := hypervisor["bytes_read_rate_max"]; ok2 { + p.SetBytesreadratemax(int64(v2.(int))) + } + if v2, ok2 := hypervisor["bytes_read_rate_max_length"]; ok2 { + p.SetBytesreadratemaxlength(int64(v2.(int))) + } + if v2, ok2 := hypervisor["bytes_write_rate"]; ok2 { + p.SetByteswriterate(int64(v2.(int))) + } + if v2, ok2 := hypervisor["bytes_write_rate_max"]; ok2 { + p.SetByteswriteratemax(int64(v2.(int))) + } + if v2, ok2 := hypervisor["bytes_write_rate_max_length"]; ok2 { + p.SetByteswriteratemaxlength(int64(v2.(int))) + } + } + } log.Printf("[DEBUG] Creating Disk Offering %s", name) diskOff, err := cs.DiskOffering.CreateDiskOffering(p) if err != nil { - return err + return fmt.Errorf("Error creating Disk Offering %s: %s", name, err) } - log.Printf("[DEBUG] Disk Offering %s successfully created", name) d.SetId(diskOff.Id) return resourceCloudStackDiskOfferingRead(d, meta) @@ -161,49 +359,131 @@ func resourceCloudStackDiskOfferingRead(d *schema.ResourceData, meta interface{} log.Printf("[DEBUG] Retrieving Disk Offering %s", d.Id()) - // Get the Disk Offering details - diskOff, count, err := cs.DiskOffering.GetDiskOfferingByID(d.Id()) + r, count, err := cs.DiskOffering.GetDiskOfferingByID(d.Id()) if err != nil { if count == 0 { - log.Printf("[DEBUG] Disk Offering %s does no longer exist", d.Get("name").(string)) + log.Printf("[DEBUG] Disk Offering %s does no longer exist", d.Id()) d.SetId("") return nil } return err } - d.Set("name", diskOff.Name) - d.Set("display_text", diskOff.Displaytext) - d.Set("disk_size", int(diskOff.Disksize)) - d.Set("customized", diskOff.Iscustomized) - d.Set("storage_type", diskOff.Storagetype) - d.Set("provisioning_type", diskOff.Provisioningtype) - d.Set("tags", diskOff.Tags) - d.Set("display_offering", diskOff.Displayoffering) + d.Set("name", r.Name) + d.Set("display_text", r.Displaytext) + d.Set("cache_mode", r.CacheMode) + d.Set("disk_size", int(r.Disksize)) + d.Set("disk_offering_strictness", r.Disksizestrictness) + d.Set("iops_read_rate", r.DiskIopsReadRate) + d.Set("iops_read_rate_max", r.DiskIopsReadRateMax) + d.Set("iops_read_rate_max_length", r.DiskIopsReadRateMaxLength) + d.Set("iops_write_rate", r.DiskIopsWriteRate) + d.Set("iops_write_rate_max", r.DiskIopsWriteRateMax) + d.Set("iops_write_rate_max_length", r.DiskIopsWriteRateMaxLength) + d.Set("provisioning_type", r.Provisioningtype) + d.Set("storage_type", r.Storagetype) + d.Set("tags", r.Tags) + d.Set("display_offering", r.Displayoffering) + + // domainid and zoneid are returned as comma-separated strings + if r.Domainid != "" { + d.Set("domain_id", strings.Split(r.Domainid, ",")) + } else { + d.Set("domain_id", []string{}) + } + if r.Zoneid != "" { + d.Set("zone_id", strings.Split(r.Zoneid, ",")) + } else { + d.Set("zone_id", []string{}) + } + + // Only emit the hypervisor block when the API returns non-default QoS values, + // otherwise leave it null so configs that omit the block don't show perpetual drift. + if r.DiskBytesReadRate > 0 || r.DiskBytesReadRateMax > 0 || r.DiskBytesReadRateMaxLength > 0 || + r.DiskBytesWriteRate > 0 || r.DiskBytesWriteRateMax > 0 || r.DiskBytesWriteRateMaxLength > 0 { + hypervisor := make(map[string]interface{}) + hypervisor["bytes_read_rate"] = r.DiskBytesReadRate + hypervisor["bytes_read_rate_max"] = r.DiskBytesReadRateMax + hypervisor["bytes_read_rate_max_length"] = r.DiskBytesReadRateMaxLength + hypervisor["bytes_write_rate"] = r.DiskBytesWriteRate + hypervisor["bytes_write_rate_max"] = r.DiskBytesWriteRateMax + hypervisor["bytes_write_rate_max_length"] = r.DiskBytesWriteRateMaxLength + d.Set("hypervisor", []interface{}{hypervisor}) + } else { + d.Set("hypervisor", []interface{}{}) + } + + // Only emit the storage block when the API returns non-default QoS values. + if r.Miniops > 0 || r.Maxiops > 0 || r.Iscustomizediops || r.Hypervisorsnapshotreserve > 0 { + storage := make(map[string]interface{}) + storage["min_iops"] = r.Miniops + storage["max_iops"] = r.Maxiops + storage["customized_iops"] = r.Iscustomizediops + storage["hypervisor_snapshot_reserve"] = r.Hypervisorsnapshotreserve + d.Set("storage", []interface{}{storage}) + } else { + d.Set("storage", []interface{}{}) + } return nil } - func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) - name := d.Get("name").(string) - if d.HasChange("name") || d.HasChange("display_text") || - d.HasChange("tags") || d.HasChange("display_offering") { + name := d.Get("name").(string) - // Create a new parameter struct - p := cs.DiskOffering.NewUpdateDiskOfferingParams(d.Id()) + // Create a new parameter struct + p := cs.DiskOffering.NewUpdateDiskOfferingParams(d.Id()) - p.SetName(d.Get("name").(string)) - p.SetDisplaytext(d.Get("display_text").(string)) - p.SetTags(d.Get("tags").(string)) - p.SetDisplayoffering(d.Get("display_offering").(bool)) + p.SetName(name) + p.SetDisplaytext(d.Get("display_text").(string)) + p.SetDisplayoffering(d.Get("display_offering").(bool)) - log.Printf("[DEBUG] Updating Disk Offering %s", name) - _, err := cs.DiskOffering.UpdateDiskOffering(p) - if err != nil { - return fmt.Errorf("Error updating Disk Offering %s: %s", name, err) + if v, ok := d.GetOk("cache_mode"); ok { + p.SetCachemode(v.(string)) + } + if v, ok := d.GetOk("domain_id"); ok { + domainIDs := v.([]interface{}) + items := make([]string, len(domainIDs)) + for i, raw := range domainIDs { + items[i] = raw.(string) } + p.SetDomainid(strings.Join(items, ",")) + } + if v, ok := d.GetOk("zone_id"); ok { + zoneIDs := v.([]interface{}) + items := make([]string, len(zoneIDs)) + for i, raw := range zoneIDs { + items[i] = raw.(string) + } + p.SetZoneid(strings.Join(items, ",")) + } + if v, ok := d.GetOk("iops_read_rate"); ok { + p.SetIopsreadrate(int64(v.(int))) + } + if v, ok := d.GetOk("iops_read_rate_max"); ok { + p.SetIopsreadratemax(int64(v.(int))) + } + if v, ok := d.GetOk("iops_read_rate_max_length"); ok { + p.SetIopsreadratemaxlength(int64(v.(int))) + } + if v, ok := d.GetOk("iops_write_rate"); ok { + p.SetIopswriterate(int64(v.(int))) + } + if v, ok := d.GetOk("iops_write_rate_max"); ok { + p.SetIopswriteratemax(int64(v.(int))) + } + if v, ok := d.GetOk("iops_write_rate_max_length"); ok { + p.SetIopswriteratemaxlength(int64(v.(int))) + } + if v, ok := d.GetOk("tags"); ok { + p.SetTags(v.(string)) + } + + log.Printf("[DEBUG] Updating Disk Offering %s", name) + _, err := cs.DiskOffering.UpdateDiskOffering(p) + if err != nil { + return fmt.Errorf("Error updating Disk Offering %s: %s", name, err) } return resourceCloudStackDiskOfferingRead(d, meta) @@ -212,11 +492,8 @@ func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface func resourceCloudStackDiskOfferingDelete(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) - // Create a new parameter struct - p := cs.DiskOffering.NewDeleteDiskOfferingParams(d.Id()) - log.Printf("[DEBUG] Deleting Disk Offering %s", d.Get("name").(string)) - _, err := cs.DiskOffering.DeleteDiskOffering(p) + _, err := cs.DiskOffering.DeleteDiskOffering(cs.DiskOffering.NewDeleteDiskOfferingParams(d.Id())) if err != nil { return fmt.Errorf("Error deleting Disk Offering: %s", err) } From f63c223a4bcff6e306862c10de0e89896c028153 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:45:00 -0700 Subject: [PATCH 2/8] updates --- cloudstack/resource_cloudstack_disk_offering.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 4cfdb2fb..ffdd051b 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -293,6 +293,8 @@ func resourceCloudStackDiskOfferingCreate(d *schema.ResourceData, meta interface items[i] = raw.(string) } p.SetZoneid(items) + } else { + p.SetZoneid([]string{"all"}) } // storage qos @@ -391,7 +393,7 @@ func resourceCloudStackDiskOfferingRead(d *schema.ResourceData, meta interface{} } else { d.Set("domain_id", []string{}) } - if r.Zoneid != "" { + if r.Zoneid != "" && r.Zoneid != "all" { d.Set("zone_id", strings.Split(r.Zoneid, ",")) } else { d.Set("zone_id", []string{}) @@ -457,6 +459,8 @@ func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface items[i] = raw.(string) } p.SetZoneid(strings.Join(items, ",")) + } else { + p.SetZoneid("all") } if v, ok := d.GetOk("iops_read_rate"); ok { p.SetIopsreadrate(int64(v.(int))) From 64f9531676954445f11f7e324a370b0bbcee880b Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:27:00 -0700 Subject: [PATCH 3/8] updates --- cloudstack/resource_cloudstack_disk_offering.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index ffdd051b..ead2f6a8 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -293,8 +293,6 @@ func resourceCloudStackDiskOfferingCreate(d *schema.ResourceData, meta interface items[i] = raw.(string) } p.SetZoneid(items) - } else { - p.SetZoneid([]string{"all"}) } // storage qos @@ -490,6 +488,11 @@ func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface return fmt.Errorf("Error updating Disk Offering %s: %s", name, err) } + _, err := cs.DiskOffering.UpdateDiskOffering(p) + if err != nil { + return err + } + return resourceCloudStackDiskOfferingRead(d, meta) } From 769d809d1f29a08eee1792e14554c8665fdf5374 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:09:22 -0700 Subject: [PATCH 4/8] updates --- cloudstack/resource_cloudstack_disk_offering.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index ead2f6a8..10e468e0 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -108,8 +108,8 @@ func resourceCloudStackDiskOffering() *schema.Resource { Description: "Provisioning type used to create volumes. Values are thin, sparse and fat", Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, - Default: "thin", ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { v := val.(string) if v == "thin" || v == "sparse" || v == "fat" { @@ -123,8 +123,8 @@ func resourceCloudStackDiskOffering() *schema.Resource { Description: "The storage type of the disk offering. Values are local and shared", Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, - Default: "shared", ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { v := val.(string) if v == "local" || v == "shared" { @@ -488,11 +488,6 @@ func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface return fmt.Errorf("Error updating Disk Offering %s: %s", name, err) } - _, err := cs.DiskOffering.UpdateDiskOffering(p) - if err != nil { - return err - } - return resourceCloudStackDiskOfferingRead(d, meta) } From bd778012e0d8df441d61f120f7cfc1e82fb314a7 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Wed, 9 Sep 2026 08:46:56 -0700 Subject: [PATCH 5/8] rebase --- .../resource_cloudstack_disk_offering.go | 4 +- .../resource_cloudstack_disk_offering_test.go | 167 +++++++++++++++++- 2 files changed, 164 insertions(+), 7 deletions(-) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 10e468e0..26ae1824 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -52,6 +52,7 @@ func resourceCloudStackDiskOffering() *schema.Resource { Description: "The cache mode to use for this disk offering. Values are none, writeback or writethrough", Type: schema.TypeString, Optional: true, + Computed: true, }, "disk_size": { Description: "The size of the disk offering in GB. When omitted the offering is created as customizable", @@ -146,7 +147,7 @@ func resourceCloudStackDiskOffering() *schema.Resource { Default: true, }, "zone_id": { - Description: "The IDs of the zones that this disk offering belongs to", + Description: "The IDs of the zones the disk offering is restricted to. Leave empty to make the offering available in all zones.", Type: schema.TypeList, Optional: true, Elem: &schema.Schema{ @@ -458,6 +459,7 @@ func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface } p.SetZoneid(strings.Join(items, ",")) } else { + // Empty config means all zones; send "all" so update clears any prior zone restriction. p.SetZoneid("all") } if v, ok := d.GetOk("iops_read_rate"); ok { diff --git a/cloudstack/resource_cloudstack_disk_offering_test.go b/cloudstack/resource_cloudstack_disk_offering_test.go index 53811eaf..628bf0d2 100644 --- a/cloudstack/resource_cloudstack_disk_offering_test.go +++ b/cloudstack/resource_cloudstack_disk_offering_test.go @@ -73,7 +73,7 @@ func TestAccCloudStackDiskOffering_customized(t *testing.T) { Config: testAccCloudStackDiskOffering_customized, Check: resource.ComposeTestCheckFunc( testAccCheckCloudStackDiskOfferingExists("cloudstack_disk_offering.custom", &do), - resource.TestCheckResourceAttr("cloudstack_disk_offering.custom", "customized", "true"), + testAccCheckCloudStackDiskOfferingCustomized(&do, true), resource.TestCheckResourceAttr("cloudstack_disk_offering.custom", "storage_type", "local"), resource.TestCheckResourceAttr("cloudstack_disk_offering.custom", "provisioning_type", "thin"), resource.TestCheckResourceAttr("cloudstack_disk_offering.custom", "tags", "ssd"), @@ -87,7 +87,6 @@ const testAccCloudStackDiskOffering_customized = ` resource "cloudstack_disk_offering" "custom" { name = "custom_disk_offering" display_text = "Custom Test" - customized = true storage_type = "local" provisioning_type = "thin" tags = "ssd" @@ -115,6 +114,8 @@ func TestAccCloudStackDiskOffering_update(t *testing.T) { resource.TestCheckResourceAttr("cloudstack_disk_offering.test1", "name", "disk_offering_1_updated"), resource.TestCheckResourceAttr("cloudstack_disk_offering.test1", "display_text", "Test Updated"), resource.TestCheckResourceAttr("cloudstack_disk_offering.test1", "tags", "gold"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.test1", "cache_mode", "writeback"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.test1", "display_offering", "false"), ), }, }, @@ -123,10 +124,153 @@ func TestAccCloudStackDiskOffering_update(t *testing.T) { const testAccCloudStackDiskOffering_update = ` resource "cloudstack_disk_offering" "test1" { - name = "disk_offering_1_updated" - display_text = "Test Updated" - disk_size = 10 - tags = "gold" + name = "disk_offering_1_updated" + display_text = "Test Updated" + disk_size = 10 + tags = "gold" + cache_mode = "writeback" + display_offering = false +} +` + +func TestAccCloudStackDiskOffering_qos(t *testing.T) { + var do cloudstack.DiskOffering + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskOfferingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackDiskOffering_qos, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskOfferingExists("cloudstack_disk_offering.qos", &do), + resource.TestCheckResourceAttr("cloudstack_disk_offering.qos", "iops_read_rate", "1000"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.qos", "iops_read_rate_max", "2000"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.qos", "iops_read_rate_max_length", "60"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.qos", "iops_write_rate", "500"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.qos", "hypervisor.0.bytes_read_rate", "1048576"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.qos", "hypervisor.0.bytes_write_rate", "524288"), + ), + }, + }, + }) +} + +const testAccCloudStackDiskOffering_qos = ` +resource "cloudstack_disk_offering" "qos" { + name = "qos_disk_offering" + display_text = "QoS Test" + disk_size = 5 + + iops_read_rate = 1000 + iops_read_rate_max = 2000 + iops_read_rate_max_length = 60 + iops_write_rate = 500 + + hypervisor { + bytes_read_rate = 1048576 + bytes_write_rate = 524288 + } +} +` + +func TestAccCloudStackDiskOffering_options(t *testing.T) { + var do cloudstack.DiskOffering + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskOfferingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackDiskOffering_options, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskOfferingExists("cloudstack_disk_offering.options", &do), + resource.TestCheckResourceAttr("cloudstack_disk_offering.options", "provisioning_type", "sparse"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.options", "cache_mode", "writeback"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.options", "disk_offering_strictness", "true"), + resource.TestCheckResourceAttr("cloudstack_disk_offering.options", "display_offering", "false"), + ), + }, + }, + }) +} + +const testAccCloudStackDiskOffering_options = ` +resource "cloudstack_disk_offering" "options" { + name = "options_disk_offering" + display_text = "Options Test" + disk_size = 8 + provisioning_type = "sparse" + cache_mode = "writeback" + disk_offering_strictness = true + display_offering = false +} +` + +func TestAccCloudStackDiskOffering_domain(t *testing.T) { + var do cloudstack.DiskOffering + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskOfferingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackDiskOffering_domain, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskOfferingExists("cloudstack_disk_offering.domain", &do), + resource.TestCheckResourceAttr("cloudstack_disk_offering.domain", "domain_id.#", "1"), + resource.TestCheckResourceAttrPair("cloudstack_disk_offering.domain", "domain_id.0", "cloudstack_domain.do_domain", "id"), + ), + }, + }, + }) +} + +const testAccCloudStackDiskOffering_domain = ` +resource "cloudstack_domain" "do_domain" { + name = "disk-offering-domain" +} + +resource "cloudstack_disk_offering" "domain" { + name = "domain_disk_offering" + display_text = "Domain Test" + disk_size = 5 + domain_id = [cloudstack_domain.do_domain.id] +} +` + +func TestAccCloudStackDiskOffering_zone(t *testing.T) { + var do cloudstack.DiskOffering + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskOfferingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackDiskOffering_zone, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskOfferingExists("cloudstack_disk_offering.zone", &do), + resource.TestCheckResourceAttr("cloudstack_disk_offering.zone", "zone_id.#", "1"), + resource.TestCheckResourceAttrPair("cloudstack_disk_offering.zone", "zone_id.0", "data.cloudstack_zone.zone", "id"), + ), + }, + }, + }) +} + +const testAccCloudStackDiskOffering_zone = ` +data "cloudstack_zone" "zone" { + filter { + name = "name" + value = "Sandbox-simulator" + } +} + +resource "cloudstack_disk_offering" "zone" { + name = "zone_disk_offering" + display_text = "Zone Test" + disk_size = 5 + zone_id = [data.cloudstack_zone.zone.id] } ` @@ -157,6 +301,17 @@ func testAccCheckCloudStackDiskOfferingExists(n string, do *cloudstack.DiskOffer } } +// testAccCheckCloudStackDiskOfferingCustomized verifies the implicit coupling: an +// offering created without disk_size must come back as customizable. +func testAccCheckCloudStackDiskOfferingCustomized(do *cloudstack.DiskOffering, expected bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if do.Iscustomized != expected { + return fmt.Errorf("expected disk offering customized=%t, got %t", expected, do.Iscustomized) + } + return nil + } +} + func testAccCheckCloudStackDiskOfferingDestroy(s *terraform.State) error { cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) From a97fa691941ad3e8ffea27a0cf8258425e5b5411 Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:05:01 -0700 Subject: [PATCH 6/8] update --- cloudstack/resource_cloudstack_disk_offering.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 26ae1824..8e459f9c 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -55,7 +55,7 @@ func resourceCloudStackDiskOffering() *schema.Resource { Computed: true, }, "disk_size": { - Description: "The size of the disk offering in GB. When omitted the offering is created as customizable", + Description: "The size of the disk offering in GB.", Type: schema.TypeInt, Optional: true, Computed: true, From 1191412405c2b5c9a7f0189d1c9794822197733d Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:24:10 -0700 Subject: [PATCH 7/8] update documentation --- website/docs/r/disk_offering.html.markdown | 95 ++++++++++++++++++---- 1 file changed, 79 insertions(+), 16 deletions(-) diff --git a/website/docs/r/disk_offering.html.markdown b/website/docs/r/disk_offering.html.markdown index 99950139..a0a510e1 100644 --- a/website/docs/r/disk_offering.html.markdown +++ b/website/docs/r/disk_offering.html.markdown @@ -14,9 +14,21 @@ A `cloudstack_disk_offering` resource manages a disk offering within CloudStack. ```hcl resource "cloudstack_disk_offering" "example" { - name = "example-disk-offering" + name = "example-disk-offering" display_text = "Example Disk Offering" - disk_size = 100 + disk_size = 100 + + storage { + min_iops = 1000 + max_iops = 5000 + customized_iops = false + hypervisor_snapshot_reserve = 25 + } + + hypervisor { + bytes_read_rate = 1048576 + bytes_write_rate = 1048576 + } } ``` @@ -27,23 +39,62 @@ The following arguments are supported: * `name` - (Required) The name of the disk offering. * `display_text` - (Required) The display text of the disk offering. -* `disk_size` - (Optional) The size of the disk offering in GB. Conflicts with - `customized`. If neither `disk_size` nor `customized` is set, the offering is - created as customized. Changing this forces a new resource to be created. -* `customized` - (Optional) Whether the disk offering allows a custom disk size - to be specified at deployment time. Conflicts with `disk_size`, and is - implied when `disk_size` is omitted. Defaults to `false` when `disk_size` - is specified and `true` otherwise. Changing this forces a new resource to - be created. -* `storage_type` - (Optional) The storage type of the disk offering. Values are - `local` and `shared`. Defaults to `shared`. Changing this forces a new - resource to be created. +* `cache_mode` - (Optional) The cache mode to use for this disk offering. Values + are `none`, `writeback` or `writethrough`. Computed when not set. +* `disk_size` - (Optional) The size of the disk offering in GB. When set, the + offering uses a fixed size; when omitted, the offering allows a custom disk + size to be specified at deployment time. Changing this forces a new resource + to be created. +* `disk_offering_strictness` - (Optional) Whether the disk offering size is + strictly enforced and cannot be resized at deployment time. When `true`, + resize is not allowed. Changing this forces a new resource to be created. +* `domain_id` - (Optional) The list of domain IDs that can use this disk + offering. Leave empty to make the offering public. +* `iops_read_rate` - (Optional) The IO requests read rate of the disk offering. +* `iops_read_rate_max` - (Optional) The burst IO requests read rate of the disk + offering. +* `iops_read_rate_max_length` - (Optional) The length (in seconds) of the IOPS + read rate burst. +* `iops_write_rate` - (Optional) The IO requests write rate of the disk offering. +* `iops_write_rate_max` - (Optional) The burst IO requests write rate of the disk + offering. +* `iops_write_rate_max_length` - (Optional) The length (in seconds) of the IOPS + write rate burst. * `provisioning_type` - (Optional) The provisioning type used to create volumes. - Values are `thin`, `sparse` and `fat`. Defaults to `thin`. Changing this + Values are `thin`, `sparse` and `fat`. Computed when not set. Changing this forces a new resource to be created. +* `storage_type` - (Optional) The storage type of the disk offering. Values are + `local` and `shared`. Computed when not set. Changing this forces a new + resource to be created. * `tags` - (Optional) The storage tags for the disk offering. * `display_offering` - (Optional) Whether the disk offering is displayed to the end user. Defaults to `true`. +* `zone_id` - (Optional) The list of zone IDs the disk offering is restricted to. + Leave empty to make the offering available in all zones. +* `hypervisor` - (Optional) A hypervisor-side QoS block (throughput limits). + Only one block is supported. The structure is documented below. +* `storage` - (Optional) A storage-side QoS block (IOPS limits). Only one block + is supported. The structure is documented below. + +The `hypervisor` block supports the following (all optional; changing any forces +a new resource to be created): + +* `bytes_read_rate` - The bytes read rate of the disk offering. +* `bytes_read_rate_max` - The burst bytes read rate of the disk offering. +* `bytes_read_rate_max_length` - The length (in seconds) of the read rate burst. +* `bytes_write_rate` - The bytes write rate of the disk offering. +* `bytes_write_rate_max` - The burst bytes write rate of the disk offering. +* `bytes_write_rate_max_length` - The length (in seconds) of the write rate burst. + +The `storage` block supports the following (all optional; changing any forces a +new resource to be created): + +* `min_iops` - The minimum IOPS of the disk offering. +* `max_iops` - The maximum IOPS of the disk offering. +* `customized_iops` - Whether the disk offering IOPS are customizable at + deployment time. +* `hypervisor_snapshot_reserve` - Hypervisor snapshot reserve space as a percent + of a volume (for managed storage using Xen or VMware). ## Attributes Reference @@ -52,12 +103,24 @@ The following attributes are exported: * `id` - The ID of the disk offering. * `name` - The name of the disk offering. * `display_text` - The display text of the disk offering. +* `cache_mode` - The cache mode of the disk offering. * `disk_size` - The size of the disk offering in GB. -* `customized` - Whether the disk offering allows a custom disk size. -* `storage_type` - The storage type of the disk offering. +* `disk_offering_strictness` - Whether the disk offering size is strictly + enforced. +* `domain_id` - The list of domain IDs that can use this disk offering. +* `iops_read_rate` - The IO requests read rate of the disk offering. +* `iops_read_rate_max` - The burst IO requests read rate of the disk offering. +* `iops_read_rate_max_length` - The length (in seconds) of the IOPS read burst. +* `iops_write_rate` - The IO requests write rate of the disk offering. +* `iops_write_rate_max` - The burst IO requests write rate of the disk offering. +* `iops_write_rate_max_length` - The length (in seconds) of the IOPS write burst. * `provisioning_type` - The provisioning type of the disk offering. +* `storage_type` - The storage type of the disk offering. * `tags` - The storage tags for the disk offering. * `display_offering` - Whether the disk offering is displayed to the end user. +* `zone_id` - The list of zone IDs the disk offering is restricted to. +* `hypervisor` - The hypervisor QoS block, as documented above. +* `storage` - The storage QoS block, as documented above. ## Import From 4cc6fd2a448ecaa31dd042de76be06730433b52c Mon Sep 17 00:00:00 2001 From: poddm <8801231+poddm@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:31:54 -0700 Subject: [PATCH 8/8] update --- cloudstack/resource_cloudstack_disk_offering.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 8e459f9c..8b876df2 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -235,7 +235,7 @@ func resourceCloudStackDiskOfferingCreate(d *schema.ResourceData, meta interface name := d.Get("name").(string) displayText := d.Get("display_text").(string) - // NewCreateDiskOfferingParams expects (displaytext, name) in that order + // Create a new parameter struct p := cs.DiskOffering.NewCreateDiskOfferingParams(displayText, name) if v, ok := d.GetOk("cache_mode"); ok {