diff --git a/cloudstack/data_source_cloudstack_limits.go b/cloudstack/data_source_cloudstack_limits.go new file mode 100644 index 00000000..ba3bc07b --- /dev/null +++ b/cloudstack/data_source_cloudstack_limits.go @@ -0,0 +1,210 @@ +// 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 ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceCloudStackLimits() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudStackLimitsRead, + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "instance", "ip", "volume", "snapshot", "template", "project", "network", "vpc", + "cpu", "memory", "primarystorage", "secondarystorage", + }, false), // false disables case-insensitive matching + Description: "The type of resource to list the limits. Available types are: " + + "instance, ip, volume, snapshot, template, project, network, vpc, cpu, memory, " + + "primarystorage, secondarystorage", + }, + "account": { + Type: schema.TypeString, + Optional: true, + Description: "List resources by account. Must be used with the domain_id parameter.", + }, + "domain_id": { + Type: schema.TypeString, + Optional: true, + Description: "List only resources belonging to the domain specified.", + }, + "project": { + Type: schema.TypeString, + Optional: true, + Description: "List resource limits by project.", + }, + "limits": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resourcetype": { + Type: schema.TypeString, + Computed: true, + }, + "resourcetypename": { + Type: schema.TypeString, + Computed: true, + }, + "account": { + Type: schema.TypeString, + Computed: true, + }, + "domain": { + Type: schema.TypeString, + Computed: true, + }, + "domain_id": { + Type: schema.TypeString, + Computed: true, + }, + "max": { + Type: schema.TypeInt, + Computed: true, + }, + "project": { + Type: schema.TypeString, + Computed: true, + }, + "projectid": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Limit.NewListResourceLimitsParams() + + // Set optional parameters + if v, ok := d.GetOk("type"); ok { + typeStr := v.(string) + if resourcetype, ok := resourceTypeMap[typeStr]; ok { + p.SetResourcetype(resourcetype) + } else { + return fmt.Errorf("invalid type value: %s", typeStr) + } + } + + if v, ok := d.GetOk("account"); ok { + p.SetAccount(v.(string)) + } + + if v, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(v.(string)) + } + + if v, ok := d.GetOk("project"); ok { + p.SetProjectid(v.(string)) + } + + // Retrieve the resource limits + l, err := cs.Limit.ListResourceLimits(p) + if err != nil { + return fmt.Errorf("Error retrieving resource limits: %s", err) + } + + // Generate a unique ID for this data source + id := generateDataSourceID(d) + d.SetId(id) + + limits := make([]map[string]interface{}, 0, len(l.ResourceLimits)) + + // Set the resource data + for _, limit := range l.ResourceLimits { + limitMap := map[string]interface{}{ + "resourcetype": limit.Resourcetype, + "resourcetypename": limit.Resourcetypename, + "max": limit.Max, + } + + if limit.Account != "" { + limitMap["account"] = limit.Account + } + + if limit.Domain != "" { + limitMap["domain"] = limit.Domain + } + + if limit.Domainid != "" { + limitMap["domainid"] = limit.Domainid + } + + if limit.Project != "" { + limitMap["project"] = limit.Project + } + + if limit.Projectid != "" { + limitMap["projectid"] = limit.Projectid + } + + limits = append(limits, limitMap) + } + + if err := d.Set("limits", limits); err != nil { + return fmt.Errorf("Error setting limits: %s", err) + } + + return nil +} + +// generateDataSourceID generates a unique ID for the data source based on its parameters +func generateDataSourceID(d *schema.ResourceData) string { + var buf bytes.Buffer + + if v, ok := d.GetOk("type"); ok { + typeStr := v.(string) + if resourcetype, ok := resourceTypeMap[typeStr]; ok { + buf.WriteString(fmt.Sprintf("%d-", resourcetype)) + } + } + + if v, ok := d.GetOk("account"); ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := d.GetOk("domain_id"); ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := d.GetOk("project"); ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + // Generate a SHA-256 hash of the buffer content + hash := sha256.Sum256(buf.Bytes()) + return fmt.Sprintf("limits-%s", hex.EncodeToString(hash[:])[:8]) +} diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 58c7fa54..3b8c4415 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -95,6 +95,7 @@ func Provider() *schema.Provider { "cloudstack_physical_network": dataSourceCloudStackPhysicalNetwork(), "cloudstack_role": dataSourceCloudstackRole(), "cloudstack_cluster": dataSourceCloudstackCluster(), + "cloudstack_limits": dataSourceCloudStackLimits(), }, ResourcesMap: map[string]*schema.Resource{ @@ -147,6 +148,7 @@ func Provider() *schema.Provider { "cloudstack_domain": resourceCloudStackDomain(), "cloudstack_network_service_provider": resourceCloudStackNetworkServiceProvider(), "cloudstack_role": resourceCloudStackRole(), + "cloudstack_limits": resourceCloudStackLimits(), }, ConfigureFunc: providerConfigure, diff --git a/cloudstack/resource_cloudstack_limits.go b/cloudstack/resource_cloudstack_limits.go new file mode 100644 index 00000000..be4a6899 --- /dev/null +++ b/cloudstack/resource_cloudstack_limits.go @@ -0,0 +1,471 @@ +// 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 ( + "context" + "fmt" + "log" + "strconv" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// resourceTypeMap maps string resource types to their integer values +var resourceTypeMap = map[string]int{ + "instance": 0, + "ip": 1, + "volume": 2, + "snapshot": 3, + "template": 4, + "project": 5, + "network": 6, + "vpc": 7, + "cpu": 8, + "memory": 9, + "primarystorage": 10, + "secondarystorage": 11, +} + +func resourceCloudStackLimits() *schema.Resource { + return &schema.Resource{ + Read: resourceCloudStackLimitsRead, + Update: resourceCloudStackLimitsUpdate, + Create: resourceCloudStackLimitsCreate, + Delete: resourceCloudStackLimitsDelete, + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "instance", "ip", "volume", "snapshot", "template", "project", "network", "vpc", + "cpu", "memory", "primarystorage", "secondarystorage", + }, false), // false disables case-insensitive matching + Description: "The type of resource to update the limits. Available types are: " + + "instance, ip, volume, snapshot, template, project, network, vpc, cpu, memory, " + + "primarystorage, secondarystorage", + }, + "account": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Update resource for a specified account. Must be used with the domain_id parameter.", + }, + "domain_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Update resource limits for all accounts in specified domain. If used with the account parameter, updates resource limits for a specified account in specified domain.", + }, + "max": { + Type: schema.TypeInt, + Optional: true, + Description: "Maximum resource limit. Use -1 for unlimited resource limit. A value of 0 means zero resources are allowed, though the CloudStack API may return -1 for a limit set to 0.", + }, + "configured_max": { + Type: schema.TypeInt, + Computed: true, + Description: "Internal field to track the originally configured max value to distinguish between 0 and -1 when CloudStack returns -1.", + }, + "project": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Update resource limits for project.", + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: resourceCloudStackLimitsImport, + }, + } +} + +// resourceCloudStackLimitsImport parses composite import IDs and sets resource fields accordingly. +func resourceCloudStackLimitsImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + // Expected formats: + // - type-account-accountname-domain_id (for account-specific limits) + // - type-project-projectid (for project-specific limits) + // - type-domain-domain_id (for domain-specific limits) + + log.Printf("[DEBUG] Importing resource with ID: %s", d.Id()) + + // First, extract the resource type which is always the first part + idParts := strings.SplitN(d.Id(), "-", 2) + if len(idParts) < 2 { + return nil, fmt.Errorf("unexpected import ID format (%q), expected type-account-accountname-domain_id, type-domain-domain_id, or type-project-projectid", d.Id()) + } + + // Parse the resource type + typeInt, err := strconv.Atoi(idParts[0]) + if err != nil { + return nil, fmt.Errorf("invalid type value in import ID: %s", idParts[0]) + } + + // Find the string representation for this numeric type + var typeStr string + for k, v := range resourceTypeMap { + if v == typeInt { + typeStr = k + break + } + } + if typeStr == "" { + return nil, fmt.Errorf("unknown type value in import ID: %d", typeInt) + } + if err := d.Set("type", typeStr); err != nil { + return nil, err + } + + // Get the original resource ID from the state + originalID := d.Id() + log.Printf("[DEBUG] Original import ID: %s", originalID) + + // Instead of trying to parse the complex ID, let's create a new resource + // and read it from the API to get the correct values + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct for listing resource limits + p := cs.Limit.NewListResourceLimitsParams() + p.SetResourcetype(typeInt) + + // Try to determine the resource scope from the ID format + remainingID := idParts[1] + + // Extract the resource scope from the ID + if strings.HasPrefix(remainingID, "domain-") { + // It's a domain-specific limit + log.Printf("[DEBUG] Detected domain-specific limit") + // We'll use the Read function to get the domain ID from the state + // after setting a temporary ID + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } else if strings.HasPrefix(remainingID, "project-") { + // It's a project-specific limit + log.Printf("[DEBUG] Detected project-specific limit") + // We'll use the Read function to get the project ID from the state + // after setting a temporary ID + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } else if strings.HasPrefix(remainingID, "account-") { + // It's an account-specific limit + log.Printf("[DEBUG] Detected account-specific limit") + // We'll use the Read function to get the account and domain ID from the state + // after setting a temporary ID + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } else { + // For backward compatibility, assume it's a global limit + log.Printf("[DEBUG] Detected global limit") + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } +} + +// getResourceType gets the resource type from the type field +func getResourceType(d *schema.ResourceData) (int, error) { + // Check if type is set + if v, ok := d.GetOk("type"); ok { + typeStr := v.(string) + if resourcetype, ok := resourceTypeMap[typeStr]; ok { + return resourcetype, nil + } + return 0, fmt.Errorf("invalid type value: %s", typeStr) + } + + return 0, fmt.Errorf("type must be specified") +} + +func resourceCloudStackLimitsCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + resourcetype, err := getResourceType(d) + if err != nil { + return err + } + + account := d.Get("account").(string) + domain_id := d.Get("domain_id").(string) + projectid := d.Get("project").(string) + + // Validate account and domain parameters + if account != "" && domain_id == "" { + return fmt.Errorf("domain_id is required when account is specified") + } + + // Create a new parameter struct + p := cs.Limit.NewUpdateResourceLimitParams(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domain_id != "" { + p.SetDomainid(domain_id) + } + // Check for max value - need to handle zero values explicitly + maxVal := d.Get("max") + if maxVal != nil { + maxIntVal := maxVal.(int) + log.Printf("[DEBUG] Setting max value to %d", maxIntVal) + p.SetMax(int64(maxIntVal)) + + // Store the original configured value for later reference + // This helps the Read function distinguish between 0 and -1 when CloudStack returns -1 + if err := d.Set("configured_max", maxIntVal); err != nil { + return fmt.Errorf("error storing configured max value: %w", err) + } + } else { + log.Printf("[DEBUG] No max value found in configuration during Create") + } + if projectid != "" { + p.SetProjectid(projectid) + } + + log.Printf("[DEBUG] Updating Resource Limit for type %d", resourcetype) + _, err = cs.Limit.UpdateResourceLimit(p) + + if err != nil { + return fmt.Errorf("Error creating resource limit: %s", err) + } + + // Generate a unique ID based on the parameters + id := generateResourceID(resourcetype, account, domain_id, projectid) + d.SetId(id) + + return resourceCloudStackLimitsRead(d, meta) +} + +// generateResourceID creates a unique ID for the resource based on its parameters +func generateResourceID(resourcetype int, account, domain_id, projectid string) string { + if projectid != "" { + return fmt.Sprintf("%d-project-%s", resourcetype, projectid) + } + + if account != "" && domain_id != "" { + return fmt.Sprintf("%d-account-%s-%s", resourcetype, account, domain_id) + } + + if domain_id != "" { + return fmt.Sprintf("%d-domain-%s", resourcetype, domain_id) + } + + return fmt.Sprintf("%d", resourcetype) +} + +func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the resourcetype from the type field + resourcetype, err := getResourceType(d) + if err != nil { + // If there's an error getting the type, try to extract it from the ID + idParts := strings.Split(d.Id(), "-") + if len(idParts) > 0 { + if rt, err := strconv.Atoi(idParts[0]); err == nil { + resourcetype = rt + // Find the string representation for this numeric type + for typeStr, typeVal := range resourceTypeMap { + if typeVal == rt { + if err := d.Set("type", typeStr); err != nil { + return fmt.Errorf("error setting type: %s", err) + } + break + } + } + + // Handle different ID formats + if len(idParts) >= 3 { + if idParts[1] == "domain" { + // Format: resourcetype-domain-domain_id + if err := d.Set("domain_id", idParts[2]); err != nil { + return fmt.Errorf("error setting domain_id: %s", err) + } + } else if idParts[1] == "project" { + // Format: resourcetype-project-projectid + if err := d.Set("project", idParts[2]); err != nil { + return fmt.Errorf("error setting project: %s", err) + } + } else if idParts[1] == "account" && len(idParts) >= 4 { + // Format: resourcetype-account-account-domain_id + if err := d.Set("account", idParts[2]); err != nil { + return fmt.Errorf("error setting account: %s", err) + } + if err := d.Set("domain_id", idParts[3]); err != nil { + return fmt.Errorf("error setting domain_id: %s", err) + } + } + } + } + } + } + + account := d.Get("account").(string) + domain_id := d.Get("domain_id").(string) + projectid := d.Get("project").(string) + + // Create a new parameter struct + p := cs.Limit.NewListResourceLimitsParams() + p.SetResourcetype(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domain_id != "" { + p.SetDomainid(domain_id) + } + if projectid != "" { + p.SetProjectid(projectid) + } + + // Retrieve the resource limits + l, err := cs.Limit.ListResourceLimits(p) + if err != nil { + return fmt.Errorf("error retrieving resource limits: %s", err) + } + + if l.Count == 0 { + log.Printf("[DEBUG] Resource limit not found") + d.SetId("") + return nil + } + + // Get the first (and should be only) limit from the results + limit := l.ResourceLimits[0] + + // Handle the max value - CloudStack may return -1 for both unlimited and zero limits + // We need to preserve the original value from the configuration when possible + log.Printf("[DEBUG] CloudStack returned max value: %d", limit.Max) + if limit.Max == -1 { + // CloudStack returns -1 for both unlimited and zero limits + // Check if we have the originally configured value stored + if configuredMax, hasConfiguredMax := d.GetOk("configured_max"); hasConfiguredMax { + configuredValue := configuredMax.(int) + log.Printf("[DEBUG] Found configured max value: %d, using it", configuredValue) + // Use the originally configured value (0 for zero limit, -1 for unlimited) + if err := d.Set("max", configuredValue); err != nil { + return fmt.Errorf("error setting max to configured value %d: %w", configuredValue, err) + } + } else { + log.Printf("[DEBUG] No configured max value found, treating -1 as unlimited") + // If no configured value is stored, treat -1 as unlimited + if err := d.Set("max", -1); err != nil { + return fmt.Errorf("error setting max to unlimited (-1): %w", err) + } + } + } else { + log.Printf("[DEBUG] Using positive max value from API: %d", limit.Max) + // For any positive value, use it directly from the API + if err := d.Set("max", int(limit.Max)); err != nil { + return fmt.Errorf("error setting max: %w", err) + } + } + + // Preserve original type configuration if it exists + if typeValue, ok := d.GetOk("type"); ok { + if err := d.Set("type", typeValue.(string)); err != nil { + return fmt.Errorf("error setting type: %w", err) + } + } + + return nil +} + +func resourceCloudStackLimitsUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + resourcetype, err := getResourceType(d) + if err != nil { + return err + } + + account := d.Get("account").(string) + domain_id := d.Get("domain_id").(string) + projectid := d.Get("project").(string) + + // Create a new parameter struct + p := cs.Limit.NewUpdateResourceLimitParams(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domain_id != "" { + p.SetDomainid(domain_id) + } + if maxVal, ok := d.GetOk("max"); ok { + maxIntVal := maxVal.(int) + log.Printf("[DEBUG] Setting max value to %d", maxIntVal) + p.SetMax(int64(maxIntVal)) + + // Store the original configured value for later reference + // This helps the Read function distinguish between 0 and -1 when CloudStack returns -1 + log.Printf("[DEBUG] Storing configured max value in update: %d", maxIntVal) + if err := d.Set("configured_max", maxIntVal); err != nil { + return fmt.Errorf("error storing configured max value: %w", err) + } + } + if projectid != "" { + p.SetProjectid(projectid) + } + + log.Printf("[DEBUG] Updating Resource Limit for type %d", resourcetype) + _, err = cs.Limit.UpdateResourceLimit(p) + + if err != nil { + return fmt.Errorf("Error updating resource limit: %s", err) + } + + return resourceCloudStackLimitsRead(d, meta) +} + +func resourceCloudStackLimitsDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + resourcetype, err := getResourceType(d) + if err != nil { + return err + } + + account := d.Get("account").(string) + domain_id := d.Get("domain_id").(string) + projectid := d.Get("project").(string) + + // Create a new parameter struct + p := cs.Limit.NewUpdateResourceLimitParams(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domain_id != "" { + p.SetDomainid(domain_id) + } + if projectid != "" { + p.SetProjectid(projectid) + } + p.SetMax(-1) // Set to -1 to remove the limit + + log.Printf("[DEBUG] Removing Resource Limit for type %d", resourcetype) + _, err = cs.Limit.UpdateResourceLimit(p) + + if err != nil { + return fmt.Errorf("Error removing Resource Limit: %s", err) + } + + d.SetId("") + + return nil +} diff --git a/cloudstack/resource_cloudstack_limits_test.go b/cloudstack/resource_cloudstack_limits_test.go new file mode 100644 index 00000000..be98fb16 --- /dev/null +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -0,0 +1,579 @@ +// 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" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackLimits_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "type", "instance"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "max", "10"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "max", "10"), + ), + }, + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "max", "20"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackLimitsExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Limits ID is set") + } + + return nil + } +} + +func testAccCheckCloudStackLimitsDestroy(s *terraform.State) error { + return nil +} + +const testAccCloudStackLimits_basic = ` +resource "cloudstack_limits" "foo" { + type = "instance" + max = 10 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_update = ` +resource "cloudstack_limits" "foo" { + type = "instance" + max = 20 + domain_id = cloudstack_domain.test_domain.id +} +` + +func TestAccCloudStackLimits_domain(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_domain_limit, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.domain_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.domain_limit", "type", "volume"), + resource.TestCheckResourceAttr( + "cloudstack_limits.domain_limit", "max", "50"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_account(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_account, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.account_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.account_limit", "type", "snapshot"), + resource.TestCheckResourceAttr( + "cloudstack_limits.account_limit", "max", "100"), + ), + }, + }, + }) +} + +// func TestAccCloudStackLimits_project(t *testing.T) { #TODO: Need project imported before this will do anything +// resource.Test(t, resource.TestCase{ +// PreCheck: func() { testAccPreCheck(t) }, +// Providers: testAccProviders, +// CheckDestroy: testAccCheckCloudStackLimitsDestroy, +// Steps: []resource.TestStep{ +// { +// Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_project, +// Check: resource.ComposeTestCheckFunc( +// testAccCheckCloudStackLimitsExists("cloudstack_limits.project_limit"), +// resource.TestCheckResourceAttr( +// "cloudstack_limits.project_limit", "type", "primarystorage"), +// resource.TestCheckResourceAttr( +// "cloudstack_limits.project_limit", "max", "1000"), +// ), +// }, +// }, +// }) +// } + +func TestAccCloudStackLimits_unlimited(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_unlimited, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.unlimited"), + resource.TestCheckResourceAttr( + "cloudstack_limits.unlimited", "type", "cpu"), + resource.TestCheckResourceAttr( + "cloudstack_limits.unlimited", "max", "-1"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_stringType(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_stringType, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.string_type"), + resource.TestCheckResourceAttr( + "cloudstack_limits.string_type", "type", "network"), + resource.TestCheckResourceAttr( + "cloudstack_limits.string_type", "max", "30"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_ip(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_ip, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.ip_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.ip_limit", "type", "ip"), + resource.TestCheckResourceAttr( + "cloudstack_limits.ip_limit", "max", "25"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_template(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_template, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.template_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.template_limit", "type", "template"), + resource.TestCheckResourceAttr( + "cloudstack_limits.template_limit", "max", "40"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_projectType(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_projectType, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.project_type_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.project_type_limit", "type", "project"), + resource.TestCheckResourceAttr( + "cloudstack_limits.project_type_limit", "max", "15"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_vpc(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_vpc, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.vpc_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.vpc_limit", "type", "vpc"), + resource.TestCheckResourceAttr( + "cloudstack_limits.vpc_limit", "max", "10"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_memory(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_memory, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.memory_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.memory_limit", "type", "memory"), + resource.TestCheckResourceAttr( + "cloudstack_limits.memory_limit", "max", "8192"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_zero(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_zero, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.zero_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.zero_limit", "type", "instance"), + resource.TestCheckResourceAttr( + "cloudstack_limits.zero_limit", "max", "0"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_secondarystorage(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_secondarystorage, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.secondarystorage_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.secondarystorage_limit", "type", "secondarystorage"), + resource.TestCheckResourceAttr( + "cloudstack_limits.secondarystorage_limit", "max", "2000"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_import(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + ), + }, + { + ResourceName: "cloudstack_limits.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domain_id", "type", "max", "configured_max"}, + }, + }, + }) +} + +// Test importing domain-specific limits +func TestAccCloudStackLimits_importDomain(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_domain_limit, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.domain_limit"), + ), + }, + { + ResourceName: "cloudstack_limits.domain_limit", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domain_id", "type", "max", "configured_max"}, + }, + }, + }) +} + +// Test importing account-specific limits +// Note: We're not verifying the state here because the account import is complex +// and we just want to make sure the import succeeds +func TestAccCloudStackLimits_importAccount(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_account, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.account_limit"), + ), + }, + { + ResourceName: "cloudstack_limits.account_limit", + ImportState: true, + ImportStateVerify: false, // Don't verify the state + }, + }, + }) +} + +// Test configurations for different resource types +const testAccCloudStackLimits_domain = ` +resource "cloudstack_domain" "test_domain" { + name = "test-domain-limits" +} +` + +const testAccCloudStackLimits_domain_limit = ` +resource "cloudstack_limits" "domain_limit" { + type = "volume" + max = 50 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_account = ` +resource "cloudstack_account" "test_account" { + username = "test-account-limits" + password = "password" + first_name = "Test" + last_name = "Account" + email = "test-account-limits@example.com" + account_type = 2 # Regular user account type + role_id = 4 # Regular user role + domainid = cloudstack_domain.test_domain.id +} + +resource "cloudstack_limits" "account_limit" { + type = "snapshot" + max = 100 + account = cloudstack_account.test_account.username + domain_id = cloudstack_domain.test_domain.id +} +` + +// const testAccCloudStackLimits_project = ` #TODO: Need project imported before this will do anything +// resource "cloudstack_project" "test_project" { +// name = "test-project-limits" +// display_text = "Test Project for Limits" +// domain_id = cloudstack_domain.test_domain.id +// } +// +// resource "cloudstack_limits" "project_limit" { +// type = "primarystorage" +// max = 1000 +// domain_id = cloudstack_domain.test_domain.id +// projectid = cloudstack_project.test_project.id +// } +// ` + +const testAccCloudStackLimits_unlimited = ` +resource "cloudstack_limits" "unlimited" { + type = "cpu" + max = -1 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_stringType = ` +resource "cloudstack_limits" "string_type" { + type = "network" + max = 30 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_ip = ` +resource "cloudstack_limits" "ip_limit" { + type = "ip" + max = 25 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_template = ` +resource "cloudstack_limits" "template_limit" { + type = "template" + max = 40 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_projectType = ` +resource "cloudstack_limits" "project_type_limit" { + type = "project" + max = 15 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_vpc = ` +resource "cloudstack_limits" "vpc_limit" { + type = "vpc" + max = 10 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_memory = ` +resource "cloudstack_limits" "memory_limit" { + type = "memory" + max = 8192 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_zero = ` +resource "cloudstack_limits" "zero_limit" { + type = "instance" + max = 0 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_secondarystorage = ` +resource "cloudstack_limits" "secondarystorage_limit" { + type = "secondarystorage" + max = 2000 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_zeroToPositive = ` +resource "cloudstack_limits" "zero_limit" { + type = "instance" + max = 5 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_positiveValue = ` +resource "cloudstack_limits" "positive_limit" { + type = "instance" + max = 15 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_positiveToZero = ` +resource "cloudstack_limits" "positive_limit" { + type = "instance" + max = 0 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_positiveToUnlimited = ` +resource "cloudstack_limits" "positive_limit" { + type = "instance" + max = -1 + domain_id = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_unlimitedToZero = ` +resource "cloudstack_limits" "unlimited" { + type = "cpu" + max = 0 + domain_id = cloudstack_domain.test_domain.id +} +` diff --git a/website/docs/d/limits.html.markdown b/website/docs/d/limits.html.markdown new file mode 100644 index 00000000..f68a2a35 --- /dev/null +++ b/website/docs/d/limits.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_limits" +sidebar_current: "docs-cloudstack-datasource-limits" +description: |- + Gets information about CloudStack resource limits. +--- + +# cloudstack_limits + +Use this data source to retrieve information about CloudStack resource limits for accounts, domains, and projects. + +## Example Usage + +```hcl +# Get all resource limits for a specific domain +data "cloudstack_limits" "domain_limits" { + domainid = "domain-uuid" +} + +# Get instance limits for a specific account +data "cloudstack_limits" "account_instance_limits" { + type = "instance" + account = "acct1" + domainid = "domain-uuid" +} + +# Get primary storage limits for a project +data "cloudstack_limits" "project_storage_limits" { + type = "primarystorage" + project = "project-uuid" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `type` - (Optional) The type of resource to list the limits. Available types are: + * `instance` + * `ip` + * `volume` + * `snapshot` + * `template` + * `project` + * `network` + * `vpc` + * `cpu` + * `memory` + * `primarystorage` + * `secondarystorage` +* `account` - (Optional) List resources by account. Must be used with the `domainid` parameter. +* `domainid` - (Optional) List only resources belonging to the domain specified. +* `project` - (Optional) List resource limits by project. + +## Attributes Reference + +The following attributes are exported: + +* `limits` - A list of resource limits. Each limit has the following attributes: + * `resourcetype` - The type of resource. + * `resourcetypename` - The name of the resource type. + * `max` - The maximum number of the resource. A value of `-1` indicates unlimited resources. A value of `0` means zero resources are allowed, though the CloudStack API may return `-1` for a limit set to `0`. + * `account` - The account of the resource limit. + * `domain` - The domain name of the resource limit. + * `domainid` - The domain ID of the resource limit. + * `project` - The project name of the resource limit. + * `projectid` - The project ID of the resource limit. diff --git a/website/docs/r/limits.html.markdown b/website/docs/r/limits.html.markdown new file mode 100644 index 00000000..344eb071 --- /dev/null +++ b/website/docs/r/limits.html.markdown @@ -0,0 +1,88 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_limits" +sidebar_current: "docs-cloudstack-limits" +description: |- + Provides a CloudStack limits resource. +--- + +# cloudstack_limits + +Provides a CloudStack limits resource. This can be used to manage resource limits for accounts, domains, and projects within CloudStack. + +## Example Usage + +```hcl +# Set instance limit for the root domain +resource "cloudstack_limits" "instance_limit" { + type = "instance" + max = 20 +} + +# Set volume limit for a specific account in a domain +resource "cloudstack_limits" "volume_limit" { + type = "volume" + max = 50 + account = "acct1" + domainid = "domain-uuid" +} + +# Set primary storage limit for a project +resource "cloudstack_limits" "storage_limit" { + type = "primarystorage" + max = 1000 # GB + project = "project-uuid" +} + +# Set unlimited CPU limit +resource "cloudstack_limits" "cpu_unlimited" { + type = "cpu" + max = -1 # Unlimited +} +``` + +## Argument Reference + +The following arguments are supported: + +* `type` - (Required, ForceNew) The type of resource to update. Available types are: + * `instance` + * `ip` + * `volume` + * `snapshot` + * `template` + * `project` + * `network` + * `vpc` + * `cpu` + * `memory` + * `primarystorage` + * `secondarystorage` + +* `account` - (Optional, ForceNew) Update resource for a specified account. Must be used with the `domainid` parameter. +* `domainid` - (Optional, ForceNew) Update resource limits for all accounts in specified domain. If used with the `account` parameter, updates resource limits for a specified account in specified domain. +* `max` - (Optional) Maximum resource limit. Use `-1` for unlimited resource limit. A value of `0` means zero resources are allowed, though the CloudStack API may return `-1` for a limit set to `0`. +* `project` - (Optional, ForceNew) Update resource limits for project. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the resource. +* `type` - The type of resource. +* `max` - The maximum number of the resource. +* `account` - The account of the resource limit. +* `domainid` - The domain ID of the resource limit. +* `project` - The project ID of the resource limit. + +## Import + +Resource limits can be imported using the resource type (numeric), account, domain ID, and project ID, e.g. + +```bash +terraform import cloudstack_limits.instance_limit 0 +terraform import cloudstack_limits.volume_limit 2-acct1-domain-uuid +terraform import cloudstack_limits.storage_limit 10-project-uuid +``` + +When importing, the numeric resource type is used in the import ID. The provider will automatically convert the numeric type to the corresponding string type after import.