From 9408600f9e3ebc2a8f7ca40bccca030630f7a9e2 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 27 Aug 2025 16:37:20 +0530 Subject: [PATCH 1/4] serviceoffering: add params for custom offering, storage tags, encryptroot Fixes #182 Adds params for Custom offerings. Constrained custom offerings allow min/max values for CPU and memory. Also adds params for encrypt root and storage tags. New params added, - customized - min_cpu_number - max_cpu_number - min_memory - max_memory - encrypt_root - storage_tags Signed-off-by: Abhishek Kumar --- .../resource_cloudstack_service_offering.go | 137 ++++++++++++++++++ ...source_cloudstack_service_offering_test.go | 39 +++++ website/docs/r/service_offering.html.markdown | 20 +++ 3 files changed, 196 insertions(+) diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index 170e05e0..86cbbf75 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -22,6 +22,7 @@ package cloudstack import ( "fmt" "log" + "strconv" "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -97,6 +98,49 @@ func resourceCloudStackServiceOffering() *schema.Resource { return }, }, + "customized": { + Description: "Whether service offering allows custom CPU/memory or not", + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + }, + "min_cpu_number": { + Description: "Minimum number of CPU cores allowed", + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "max_cpu_number": { + Description: "Maximum number of CPU cores allowed", + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "min_memory": { + Description: "Minimum memory allowed (MB)", + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "max_memory": { + Description: "Maximum memory allowed (MB)", + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "encrypt_root": { + Description: "Encrypt the root disk for VMs using this service offering", + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "storage_tags": { + Description: "Storage tags to associate with the service offering", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, }, } } @@ -136,6 +180,34 @@ func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interf p.SetStoragetype(v.(string)) } + if v, ok := d.GetOk("customized"); ok { + p.SetCustomized(v.(bool)) + } + + if v, ok := d.GetOk("min_cpu_number"); ok { + p.SetMincpunumber(v.(int)) + } + + if v, ok := d.GetOk("max_cpu_number"); ok { + p.SetMaxcpunumber(v.(int)) + } + + if v, ok := d.GetOk("min_memory"); ok { + p.SetMinmemory(v.(int)) + } + + if v, ok := d.GetOk("max_memory"); ok { + p.SetMaxmemory(v.(int)) + } + + if v, ok := d.GetOk("encrypt_root"); ok { + p.SetEncryptroot(v.(bool)) + } + + if v, ok := d.GetOk("storage_tags"); ok { + p.SetTags(v.(string)) + } + log.Printf("[DEBUG] Creating Service Offering %s", name) s, err := cs.ServiceOffering.CreateServiceOffering(p) @@ -177,6 +249,53 @@ func resourceCloudStackServiceOfferingRead(d *schema.ResourceData, meta interfac "memory": s.Memory, "offer_ha": s.Offerha, "storage_type": s.Storagetype, + "customized": s.Iscustomized, + "min_cpu_number": func() interface{} { + if s.Serviceofferingdetails == nil { + return nil + } + if v, ok := s.Serviceofferingdetails["mincpunumber"]; ok { + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + return nil + }(), + "max_cpu_number": func() interface{} { + if s.Serviceofferingdetails == nil { + return nil + } + if v, ok := s.Serviceofferingdetails["maxcpunumber"]; ok { + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + return nil + }(), + "min_memory": func() interface{} { + if s.Serviceofferingdetails == nil { + return nil + } + if v, ok := s.Serviceofferingdetails["minmemory"]; ok { + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + return nil + }(), + "max_memory": func() interface{} { + if s.Serviceofferingdetails == nil { + return nil + } + if v, ok := s.Serviceofferingdetails["maxmemory"]; ok { + if i, err := strconv.Atoi(v); err == nil { + return i + } + } + return nil + }(), + "encrypt_root": s.Encryptroot, + "storage_tags": s.Storagetags, } for k, v := range fields { @@ -247,6 +366,24 @@ func resourceCloudStackServiceOfferingUpdate(d *schema.ResourceData, meta interf } + if d.HasChange("tags") { + log.Printf("[DEBUG] Tags changed for %s, starting update", name) + + // Create a new parameter struct + p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) + + // Set the new tags + p.SetStoragetags(d.Get("tags").(string)) + + // Update the host tags + _, err := cs.ServiceOffering.UpdateServiceOffering(p) + if err != nil { + return fmt.Errorf( + "Error updating the storage tags for service offering %s: %s", name, err) + } + + } + return resourceCloudStackServiceOfferingRead(d, meta) } diff --git a/cloudstack/resource_cloudstack_service_offering_test.go b/cloudstack/resource_cloudstack_service_offering_test.go index fc29628c..d4634c9e 100644 --- a/cloudstack/resource_cloudstack_service_offering_test.go +++ b/cloudstack/resource_cloudstack_service_offering_test.go @@ -83,3 +83,42 @@ func testAccCheckCloudStackServiceOfferingExists(n string, so *cloudstack.Servic return nil } } + +func TestAccCloudStackServiceOffering_customized(t *testing.T) { + var so cloudstack.ServiceOffering + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackServiceOffering_customized, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackServiceOfferingExists("cloudstack_service_offering.custom", &so), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "customized", "true"), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "min_cpu_number", "1"), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "max_cpu_number", "8"), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "min_memory", "1024"), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "max_memory", "16384"), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "cpu_speed", "1000"), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "encrypt_root", "true"), + resource.TestCheckResourceAttr("cloudstack_service_offering.custom", "storage_tags", "production,ssd"), + ), + }, + }, + }) +} + +const testAccCloudStackServiceOffering_customized = ` +resource "cloudstack_service_offering" "custom" { + name = "custom_service_offering" + display_text = "Custom Test" + customized = true + min_cpu_number = 1 + max_cpu_number = 8 + min_memory = 1024 + max_memory = 16384 + cpu_speed = 1000 + encrypt_root = true + storage_tags = "production,ssd" +} +` diff --git a/website/docs/r/service_offering.html.markdown b/website/docs/r/service_offering.html.markdown index 94735eb8..33852cb4 100644 --- a/website/docs/r/service_offering.html.markdown +++ b/website/docs/r/service_offering.html.markdown @@ -49,6 +49,26 @@ The following arguments are supported: * `storage_type` - (Optional) The storage type of the service offering. Values are `local` and `shared`. Changing this forces a new resource to be created. +* `customized` - (Optional) Whether the service offering allows custom CPU and memory values. Set to `true` to enable users to specify CPU/memory within the min/max constraints for constrained offerings and any value for unconstrained offerings. + Changing this forces a new resource to be created. + +* `min_cpu_number` - (Optional) Minimum number of CPU cores allowed for customized offerings. + Changing this forces a new resource to be created. + +* `max_cpu_number` - (Optional) Maximum number of CPU cores allowed for customized offerings. + Changing this forces a new resource to be created. + +* `min_memory` - (Optional) Minimum memory (in MB) allowed for customized offerings. + Changing this forces a new resource to be created. + +* `max_memory` - (Optional) Maximum memory (in MB) allowed for customized offerings. + Changing this forces a new resource to be created. + +* `encrypt_root` - (Optional) Whether to encrypt the root disk for VMs using this service offering. + Changing this forces a new resource to be created. + +* `storage_tags` - (Optional) Storage tags to associate with the service offering. + ## Attributes Reference The following attributes are exported: From 2916c01f9f292773f525d1bc0a797244e09a6234 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 27 Aug 2025 17:02:15 +0530 Subject: [PATCH 2/4] storage tags don't force new Signed-off-by: Abhishek Kumar --- cloudstack/resource_cloudstack_service_offering.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index 86cbbf75..c7131fb7 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -139,7 +139,6 @@ func resourceCloudStackServiceOffering() *schema.Resource { Description: "Storage tags to associate with the service offering", Type: schema.TypeString, Optional: true, - ForceNew: true, }, }, } From a29245ed27e1beb83f3a9327655c8817a95d0278 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Wed, 27 Aug 2025 17:03:42 +0530 Subject: [PATCH 3/4] fix Signed-off-by: Abhishek Kumar --- cloudstack/resource_cloudstack_service_offering.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index c7131fb7..56fc7f9d 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -365,14 +365,14 @@ func resourceCloudStackServiceOfferingUpdate(d *schema.ResourceData, meta interf } - if d.HasChange("tags") { + if d.HasChange("storage_tags") { log.Printf("[DEBUG] Tags changed for %s, starting update", name) // Create a new parameter struct p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) // Set the new tags - p.SetStoragetags(d.Get("tags").(string)) + p.SetStoragetags(d.Get("storage_tags").(string)) // Update the host tags _, err := cs.ServiceOffering.UpdateServiceOffering(p) From db3961c221cc7e6611cc4d14b99bd7f4d09f7dcd Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 28 Aug 2025 11:45:08 +0530 Subject: [PATCH 4/4] fix Signed-off-by: Abhishek Kumar --- .../resource_cloudstack_service_offering.go | 110 ++++++++---------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index 56fc7f9d..107938da 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -103,7 +103,7 @@ func resourceCloudStackServiceOffering() *schema.Resource { Type: schema.TypeBool, Optional: true, ForceNew: true, - Default: false, + Computed: true, }, "min_cpu_number": { Description: "Minimum number of CPU cores allowed", @@ -151,12 +151,15 @@ func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interf // Create a new parameter struct p := cs.ServiceOffering.NewCreateServiceOfferingParams(display_text, name) - if v, ok := d.GetOk("cpu_number"); ok { - p.SetCpunumber(v.(int)) + + cpuNumber, cpuNumberOk := d.GetOk("cpu_number") + if cpuNumberOk { + p.SetCpunumber(cpuNumber.(int)) } - if v, ok := d.GetOk("cpu_speed"); ok { - p.SetCpuspeed(v.(int)) + cpuSpeed, cpuSpeedOk := d.GetOk("cpu_speed") + if cpuSpeedOk { + p.SetCpuspeed(cpuSpeed.(int)) } if v, ok := d.GetOk("host_tags"); ok { @@ -167,8 +170,9 @@ func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interf p.SetLimitcpuuse(v.(bool)) } - if v, ok := d.GetOk("memory"); ok { - p.SetMemory(v.(int)) + memory, memoryOk := d.GetOk("memory") + if memoryOk { + p.SetMemory(memory.(int)) } if v, ok := d.GetOk("offer_ha"); ok { @@ -179,9 +183,14 @@ func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interf p.SetStoragetype(v.(string)) } + customized := false if v, ok := d.GetOk("customized"); ok { - p.SetCustomized(v.(bool)) + customized = v.(bool) } + if !cpuNumberOk && !cpuSpeedOk && !memoryOk { + customized = true + } + p.SetCustomized(customized) if v, ok := d.GetOk("min_cpu_number"); ok { p.SetMincpunumber(v.(int)) @@ -239,62 +248,22 @@ func resourceCloudStackServiceOfferingRead(d *schema.ResourceData, meta interfac d.SetId(s.Id) fields := map[string]interface{}{ - "name": s.Name, - "display_text": s.Displaytext, - "cpu_number": s.Cpunumber, - "cpu_speed": s.Cpuspeed, - "host_tags": s.Hosttags, - "limit_cpu_use": s.Limitcpuuse, - "memory": s.Memory, - "offer_ha": s.Offerha, - "storage_type": s.Storagetype, - "customized": s.Iscustomized, - "min_cpu_number": func() interface{} { - if s.Serviceofferingdetails == nil { - return nil - } - if v, ok := s.Serviceofferingdetails["mincpunumber"]; ok { - if i, err := strconv.Atoi(v); err == nil { - return i - } - } - return nil - }(), - "max_cpu_number": func() interface{} { - if s.Serviceofferingdetails == nil { - return nil - } - if v, ok := s.Serviceofferingdetails["maxcpunumber"]; ok { - if i, err := strconv.Atoi(v); err == nil { - return i - } - } - return nil - }(), - "min_memory": func() interface{} { - if s.Serviceofferingdetails == nil { - return nil - } - if v, ok := s.Serviceofferingdetails["minmemory"]; ok { - if i, err := strconv.Atoi(v); err == nil { - return i - } - } - return nil - }(), - "max_memory": func() interface{} { - if s.Serviceofferingdetails == nil { - return nil - } - if v, ok := s.Serviceofferingdetails["maxmemory"]; ok { - if i, err := strconv.Atoi(v); err == nil { - return i - } - } - return nil - }(), - "encrypt_root": s.Encryptroot, - "storage_tags": s.Storagetags, + "name": s.Name, + "display_text": s.Displaytext, + "cpu_number": s.Cpunumber, + "cpu_speed": s.Cpuspeed, + "host_tags": s.Hosttags, + "limit_cpu_use": s.Limitcpuuse, + "memory": s.Memory, + "offer_ha": s.Offerha, + "storage_type": s.Storagetype, + "customized": s.Iscustomized, + "min_cpu_number": getIntFromDetails(s.Serviceofferingdetails, "mincpunumber"), + "max_cpu_number": getIntFromDetails(s.Serviceofferingdetails, "maxcpunumber"), + "min_memory": getIntFromDetails(s.Serviceofferingdetails, "minmemory"), + "max_memory": getIntFromDetails(s.Serviceofferingdetails, "maxmemory"), + "encrypt_root": s.Encryptroot, + "storage_tags": s.Storagetags, } for k, v := range fields { @@ -399,3 +368,16 @@ func resourceCloudStackServiceOfferingDelete(d *schema.ResourceData, meta interf return nil } + +// getIntFromDetails extracts an integer value from the service offering details map. +func getIntFromDetails(details map[string]string, key string) interface{} { + if details == nil { + return nil + } + if val, ok := details[key]; ok { + if i, err := strconv.Atoi(val); err == nil { + return i + } + } + return nil +}