diff --git a/cloudstack/data_source_cloudstack_cluster.go b/cloudstack/data_source_cloudstack_cluster.go new file mode 100644 index 00000000..ee1adcde --- /dev/null +++ b/cloudstack/data_source_cloudstack_cluster.go @@ -0,0 +1,255 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + "reflect" + "regexp" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackCluster() *schema.Resource { + return &schema.Resource{ + Read: datasourceCloudStackClusterRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + + //Computed values + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "cluster_type": { + Type: schema.TypeString, + Computed: true, + }, + "hypervisor": { + Type: schema.TypeString, + Computed: true, + }, + "pod_id": { + Type: schema.TypeString, + Computed: true, + }, + "pod_name": { + Type: schema.TypeString, + Computed: true, + }, + "zone_id": { + Type: schema.TypeString, + Computed: true, + }, + "zone_name": { + Type: schema.TypeString, + Computed: true, + }, + "allocation_state": { + Type: schema.TypeString, + Computed: true, + }, + "managed_state": { + Type: schema.TypeString, + Computed: true, + }, + "cpu_overcommit_ratio": { + Type: schema.TypeString, + Computed: true, + }, + "memory_overcommit_ratio": { + Type: schema.TypeString, + Computed: true, + }, + "arch": { + Type: schema.TypeString, + Computed: true, + }, + "ovm3vip": { + Type: schema.TypeString, + Computed: true, + }, + "capacity": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "capacity_allocated": { + Type: schema.TypeInt, + Computed: true, + }, + "capacity_total": { + Type: schema.TypeInt, + Computed: true, + }, + "capacity_used": { + Type: schema.TypeInt, + Computed: true, + }, + "cluster_id": { + Type: schema.TypeString, + Computed: true, + }, + "cluster_name": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "percent_used": { + Type: schema.TypeInt, + Computed: true, + }, + "pod_id": { + Type: schema.TypeString, + Computed: true, + }, + "pod_name": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "zone_id": { + Type: schema.TypeString, + Computed: true, + }, + "zone_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dsFlattenClusterCapacity(capacity []cloudstack.ClusterCapacity) []map[string]interface{} { + cap := make([]map[string]interface{}, len(capacity)) + for i, c := range capacity { + cap[i] = map[string]interface{}{ + "capacity_allocated": c.Capacityallocated, + "capacity_total": c.Capacitytotal, + "capacity_used": c.Capacityused, + "cluster_id": c.Clusterid, + "cluster_name": c.Clustername, + "name": c.Name, + "percent_used": c.Percentused, + "pod_id": c.Podid, + "pod_name": c.Podname, + "type": c.Type, + "zone_id": c.Zoneid, + "zone_name": c.Zonename, + } + } + return cap +} + +func datasourceCloudStackClusterRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + p := cs.Cluster.NewListClustersParams() + + csClusters, err := cs.Cluster.ListClusters(p) + if err != nil { + return fmt.Errorf("failed to list clusters: %s", err) + } + + filters := d.Get("filter") + + for _, cluster := range csClusters.Clusters { + match, err := applyClusterFilters(cluster, filters.(*schema.Set)) + if err != nil { + return err + } + if match { + return clusterDescriptionAttributes(d, cluster) + } + } + + return fmt.Errorf("no clusters found") +} + +func clusterDescriptionAttributes(d *schema.ResourceData, cluster *cloudstack.Cluster) error { + d.SetId(cluster.Id) + + fields := map[string]interface{}{ + "id": cluster.Id, + "name": cluster.Name, + "cluster_type": cluster.Clustertype, + "hypervisor": cluster.Hypervisortype, + "pod_id": cluster.Podid, + "pod_name": cluster.Podname, + "zone_id": cluster.Zoneid, + "zone_name": cluster.Zonename, + "allocation_state": cluster.Allocationstate, + "managed_state": cluster.Managedstate, + "cpu_overcommit_ratio": cluster.Cpuovercommitratio, + "memory_overcommit_ratio": cluster.Memoryovercommitratio, + "arch": cluster.Arch, + "ovm3vip": cluster.Ovm3vip, + "capacity": dsFlattenClusterCapacity(cluster.Capacity), + } + + for k, v := range fields { + if err := d.Set(k, v); err != nil { + log.Printf("[WARN] Error setting %s: %s", k, err) + } + } + + return nil +} + +func applyClusterFilters(cluster *cloudstack.Cluster, filters *schema.Set) (bool, error) { + val := reflect.ValueOf(cluster).Elem() + + for _, f := range filters.List() { + filter := f.(map[string]interface{}) + r, err := regexp.Compile(filter["value"].(string)) + if err != nil { + return false, fmt.Errorf("invalid regex: %s", err) + } + updatedName := strings.ReplaceAll(filter["name"].(string), "_", "") + clusterField := val.FieldByNameFunc(func(fieldName string) bool { + if strings.EqualFold(fieldName, updatedName) { + updatedName = fieldName + return true + } + return false + }).String() + + if r.MatchString(clusterField) { + return true, nil + } + } + + return false, nil +} diff --git a/cloudstack/data_source_cloudstack_cluster_test.go b/cloudstack/data_source_cloudstack_cluster_test.go new file mode 100644 index 00000000..e25b4d16 --- /dev/null +++ b/cloudstack/data_source_cloudstack_cluster_test.go @@ -0,0 +1,76 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccClusterDataSource_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testClusterDataSourceConfig_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.cloudstack_cluster.test", "name", "terraform-test-cluster"), + ), + }, + }, + }) +} + +const testClusterDataSourceConfig_basic = ` +data "cloudstack_zone" "zone" { + filter { + name = "name" + value = "Sandbox-simulator" + } +} + +data "cloudstack_pod" "pod" { + filter { + name = "name" + value = "POD0" + } +} + +# Create a cluster first +resource "cloudstack_cluster" "test_cluster" { + name = "terraform-test-cluster" + cluster_type = "CloudManaged" + hypervisor = "KVM" + pod_id = data.cloudstack_pod.pod.id + zone_id = data.cloudstack_zone.zone.id + arch = "x86_64" +} + +# Then query it with the data source +data "cloudstack_cluster" "test" { + filter { + name = "name" + value = "terraform-test-cluster" + } + depends_on = [cloudstack_cluster.test_cluster] +} +` diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 17e0c226..c59a0a8d 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -93,6 +93,7 @@ func Provider() *schema.Provider { "cloudstack_domain": dataSourceCloudstackDomain(), "cloudstack_physicalnetwork": dataSourceCloudStackPhysicalNetwork(), "cloudstack_role": dataSourceCloudstackRole(), + "cloudstack_cluster": dataSourceCloudstackCluster(), }, ResourcesMap: map[string]*schema.Resource{ @@ -113,6 +114,8 @@ func Provider() *schema.Provider { "cloudstack_network_acl": resourceCloudStackNetworkACL(), "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), "cloudstack_nic": resourceCloudStackNIC(), + "cloudstack_pod": resourceCloudStackPod(), + "cloudstack_cluster": resourceCloudStackCluster(), "cloudstack_port_forward": resourceCloudStackPortForward(), "cloudstack_private_gateway": resourceCloudStackPrivateGateway(), "cloudstack_secondary_ipaddress": resourceCloudStackSecondaryIPAddress(), diff --git a/cloudstack/resource_cloudstack_cluster.go b/cloudstack/resource_cloudstack_cluster.go new file mode 100644 index 00000000..21eb6d21 --- /dev/null +++ b/cloudstack/resource_cloudstack_cluster.go @@ -0,0 +1,341 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + "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" +) + +func resourceCloudStackCluster() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackClusterCreate, + Read: resourceCloudStackClusterRead, + Update: resourceCloudStackClusterUpdate, + Delete: resourceCloudStackClusterDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "cluster_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "CloudManaged", + "ExternalManaged", + }, false), + }, + "hypervisor": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "XenServer", + "KVM", + "VMware", + "Hyperv", + "BareMetal", + "Simulator", + "Ovm3", + }, false), + }, + "pod_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "zone_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "allocation_state": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "arch": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "x86_64", + "aarch64", + }, false), + }, + "guest_vswitch_name": { + Type: schema.TypeString, + Optional: true, + }, + "guest_vswitch_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "vmwaresvs", + "vmwaredvs", + }, false), + }, + "ovm3cluster": { + Type: schema.TypeString, + Optional: true, + }, + "ovm3pool": { + Type: schema.TypeString, + Optional: true, + }, + "ovm3vip": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + "public_vswitch_name": { + Type: schema.TypeString, + Optional: true, + }, + "public_vswitch_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "vmwaresvs", + "vmwaredvs", + }, false), + }, + "url": { + Type: schema.TypeString, + Optional: true, + }, + "username": { + Type: schema.TypeString, + Optional: true, + }, + "vsm_ip_address": { + Type: schema.TypeString, + Optional: true, + }, + "vsm_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + "vsm_username": { + Type: schema.TypeString, + Optional: true, + }, + "pod_name": { + Type: schema.TypeString, + Computed: true, + }, + "zone_name": { + Type: schema.TypeString, + Computed: true, + }, + "managed_state": { + Type: schema.TypeString, + Computed: true, + }, + "cpu_overcommit_ratio": { + Type: schema.TypeString, + Computed: true, + }, + "memory_overcommit_ratio": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackClusterCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + clusterType := d.Get("cluster_type").(string) + hypervisor := d.Get("hypervisor").(string) + podID := d.Get("pod_id").(string) + zoneID := d.Get("zone_id").(string) + + // Create a new parameter struct + p := cs.Cluster.NewAddClusterParams(name, clusterType, hypervisor, podID, zoneID) + + // Set optional parameters + if allocationState, ok := d.GetOk("allocation_state"); ok { + p.SetAllocationstate(allocationState.(string)) + } + + if arch, ok := d.GetOk("arch"); ok { + p.SetArch(arch.(string)) + } + + if guestVSwitchName, ok := d.GetOk("guest_vswitch_name"); ok { + p.SetGuestvswitchname(guestVSwitchName.(string)) + } + + if guestVSwitchType, ok := d.GetOk("guest_vswitch_type"); ok { + p.SetGuestvswitchtype(guestVSwitchType.(string)) + } + + if ovm3cluster, ok := d.GetOk("ovm3cluster"); ok { + p.SetOvm3cluster(ovm3cluster.(string)) + } + + if ovm3pool, ok := d.GetOk("ovm3pool"); ok { + p.SetOvm3pool(ovm3pool.(string)) + } + + if ovm3vip, ok := d.GetOk("ovm3vip"); ok { + p.SetOvm3vip(ovm3vip.(string)) + } + + if password, ok := d.GetOk("password"); ok { + p.SetPassword(password.(string)) + } + + if publicVSwitchName, ok := d.GetOk("public_vswitch_name"); ok { + p.SetPublicvswitchname(publicVSwitchName.(string)) + } + + if publicVSwitchType, ok := d.GetOk("public_vswitch_type"); ok { + p.SetPublicvswitchtype(publicVSwitchType.(string)) + } + + if url, ok := d.GetOk("url"); ok { + p.SetUrl(url.(string)) + } + + if username, ok := d.GetOk("username"); ok { + p.SetUsername(username.(string)) + } + + if vsmIPAddress, ok := d.GetOk("vsm_ip_address"); ok { + p.SetVsmipaddress(vsmIPAddress.(string)) + } + + if vsmPassword, ok := d.GetOk("vsm_password"); ok { + p.SetVsmpassword(vsmPassword.(string)) + } + + if vsmUsername, ok := d.GetOk("vsm_username"); ok { + p.SetVsmusername(vsmUsername.(string)) + } + + log.Printf("[DEBUG] Creating Cluster %s", name) + r, err := cs.Cluster.AddCluster(p) + if err != nil { + return fmt.Errorf("Error creating Cluster %s: %s", name, err) + } + + // The response directly contains the cluster information + d.SetId(r.Id) + + return resourceCloudStackClusterRead(d, meta) +} + +func resourceCloudStackClusterRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the Cluster details + c, count, err := cs.Cluster.GetClusterByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Cluster %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + return err + } + + d.Set("name", c.Name) + d.Set("cluster_type", c.Clustertype) + d.Set("hypervisor", c.Hypervisortype) + d.Set("pod_id", c.Podid) + d.Set("pod_name", c.Podname) + d.Set("zone_id", c.Zoneid) + d.Set("zone_name", c.Zonename) + d.Set("allocation_state", c.Allocationstate) + d.Set("managed_state", c.Managedstate) + d.Set("cpu_overcommit_ratio", c.Cpuovercommitratio) + d.Set("memory_overcommit_ratio", c.Memoryovercommitratio) + d.Set("arch", c.Arch) + d.Set("ovm3vip", c.Ovm3vip) + + return nil +} + +func resourceCloudStackClusterUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Cluster.NewUpdateClusterParams(d.Id()) + + if d.HasChange("name") { + p.SetClustername(d.Get("name").(string)) + } + + if d.HasChange("allocation_state") { + p.SetAllocationstate(d.Get("allocation_state").(string)) + } + + // Note: managed_state is a computed field and cannot be set directly + + _, err := cs.Cluster.UpdateCluster(p) + if err != nil { + return fmt.Errorf("Error updating Cluster %s: %s", d.Get("name").(string), err) + } + + return resourceCloudStackClusterRead(d, meta) +} + +func resourceCloudStackClusterDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Cluster.NewDeleteClusterParams(d.Id()) + + log.Printf("[DEBUG] Deleting Cluster %s", d.Get("name").(string)) + _, err := cs.Cluster.DeleteCluster(p) + + if err != nil { + // This is a very poor way to be told the ID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting Cluster %s: %s", d.Get("name").(string), err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_cluster_test.go b/cloudstack/resource_cloudstack_cluster_test.go new file mode 100644 index 00000000..bc77faeb --- /dev/null +++ b/cloudstack/resource_cloudstack_cluster_test.go @@ -0,0 +1,172 @@ +// +// 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/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackCluster_basic(t *testing.T) { + var cluster cloudstack.Cluster + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackCluster_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackClusterExists( + "cloudstack_cluster.foo", &cluster), + testAccCheckCloudStackClusterAttributes(&cluster), + resource.TestCheckResourceAttr( + "cloudstack_cluster.foo", "name", "terraform-cluster"), + ), + }, + }, + }) +} + +func TestAccCloudStackCluster_import(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackCluster_basic, + }, + + { + ResourceName: "cloudstack_cluster.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "password", "vsm_password", + }, + }, + }, + }) +} + +func testAccCheckCloudStackClusterExists( + n string, cluster *cloudstack.Cluster) 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 Cluster ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + c, count, err := cs.Cluster.GetClusterByID(rs.Primary.ID) + if err != nil { + return err + } + + if count == 0 { + return fmt.Errorf("Cluster not found") + } + + *cluster = *c + + return nil + } +} + +func testAccCheckCloudStackClusterAttributes( + cluster *cloudstack.Cluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if cluster.Name != "terraform-cluster" { + return fmt.Errorf("Bad name: %s", cluster.Name) + } + + if cluster.Clustertype != "CloudManaged" { + return fmt.Errorf("Bad cluster type: %s", cluster.Clustertype) + } + + if cluster.Hypervisortype != "KVM" { + return fmt.Errorf("Bad hypervisor: %s", cluster.Hypervisortype) + } + + return nil + } +} + +func testAccCheckCloudStackClusterDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_cluster" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Cluster ID is set") + } + + _, count, err := cs.Cluster.GetClusterByID(rs.Primary.ID) + if err != nil { + return nil + } + + if count > 0 { + return fmt.Errorf("Cluster %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccCloudStackCluster_basic = ` +data "cloudstack_zone" "zone" { + filter { + name = "name" + value = "Sandbox-simulator" + } +} + +resource "cloudstack_pod" "foopod" { + name = "terraform-pod" + zone_id = data.cloudstack_zone.zone.id + gateway = "192.168.56.1" + netmask = "255.255.255.0" + start_ip = "192.168.56.2" + end_ip = "192.168.56.254" +} + +resource "cloudstack_cluster" "foo" { + name = "terraform-cluster" + cluster_type = "CloudManaged" + hypervisor = "KVM" + pod_id = cloudstack_pod.foopod.id + zone_id = data.cloudstack_zone.zone.id + arch = "x86_64" +}` diff --git a/cloudstack/resource_cloudstack_pod.go b/cloudstack/resource_cloudstack_pod.go new file mode 100644 index 00000000..e73e31ac --- /dev/null +++ b/cloudstack/resource_cloudstack_pod.go @@ -0,0 +1,217 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackPod() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackPodCreate, + Read: resourceCloudStackPodRead, + Update: resourceCloudStackPodUpdate, + Delete: resourceCloudStackPodDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "zone_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "gateway": { + Type: schema.TypeString, + Required: true, + }, + "netmask": { + Type: schema.TypeString, + Required: true, + }, + "start_ip": { + Type: schema.TypeString, + Required: true, + }, + "end_ip": { + Type: schema.TypeString, + Required: true, + }, + "allocation_state": { + Type: schema.TypeString, + Computed: true, + }, + "zone_name": { + Type: schema.TypeString, + Computed: true, + }, + // VLAN ID is not directly settable in the CreatePodParams + // It's returned in the response but can't be set during creation + "vlan_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackPodCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + zoneID := d.Get("zone_id").(string) + gateway := d.Get("gateway").(string) + netmask := d.Get("netmask").(string) + startIP := d.Get("start_ip").(string) + + // Create a new parameter struct + p := cs.Pod.NewCreatePodParams(name, zoneID) + + // Set required parameters + p.SetGateway(gateway) + p.SetNetmask(netmask) + p.SetStartip(startIP) + + // Set optional parameters + if endIP, ok := d.GetOk("end_ip"); ok { + p.SetEndip(endIP.(string)) + } + + // Note: VLAN ID is not directly settable in the CreatePodParams + + log.Printf("[DEBUG] Creating Pod %s", name) + pod, err := cs.Pod.CreatePod(p) + if err != nil { + return fmt.Errorf("Error creating Pod %s: %s", name, err) + } + + d.SetId(pod.Id) + + return resourceCloudStackPodRead(d, meta) +} + +func resourceCloudStackPodRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the Pod details + p := cs.Pod.NewListPodsParams() + p.SetId(d.Id()) + + pods, err := cs.Pod.ListPods(p) + if err != nil { + return fmt.Errorf("Error getting Pod %s: %s", d.Id(), err) + } + + if pods.Count == 0 { + log.Printf("[DEBUG] Pod %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + pod := pods.Pods[0] + + d.Set("name", pod.Name) + d.Set("zone_id", pod.Zoneid) + d.Set("zone_name", pod.Zonename) + d.Set("gateway", pod.Gateway) + d.Set("netmask", pod.Netmask) + d.Set("allocation_state", pod.Allocationstate) + + if len(pod.Startip) > 0 { + d.Set("start_ip", pod.Startip[0]) + } + + if len(pod.Endip) > 0 { + d.Set("end_ip", pod.Endip[0]) + } + + if len(pod.Vlanid) > 0 { + d.Set("vlan_id", pod.Vlanid[0]) + } + + return nil +} + +func resourceCloudStackPodUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Pod.NewUpdatePodParams(d.Id()) + + if d.HasChange("name") { + p.SetName(d.Get("name").(string)) + } + + if d.HasChange("gateway") { + p.SetGateway(d.Get("gateway").(string)) + } + + if d.HasChange("netmask") { + p.SetNetmask(d.Get("netmask").(string)) + } + + if d.HasChange("start_ip") { + p.SetStartip(d.Get("start_ip").(string)) + } + + if d.HasChange("end_ip") { + p.SetEndip(d.Get("end_ip").(string)) + } + + _, err := cs.Pod.UpdatePod(p) + if err != nil { + return fmt.Errorf("Error updating Pod %s: %s", d.Get("name").(string), err) + } + + return resourceCloudStackPodRead(d, meta) +} + +func resourceCloudStackPodDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Pod.NewDeletePodParams(d.Id()) + + log.Printf("[DEBUG] Deleting Pod %s", d.Get("name").(string)) + _, err := cs.Pod.DeletePod(p) + + if err != nil { + // This is a very poor way to be told the ID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting Pod %s: %s", d.Get("name").(string), err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_pod_test.go b/cloudstack/resource_cloudstack_pod_test.go new file mode 100644 index 00000000..7b7c8ab0 --- /dev/null +++ b/cloudstack/resource_cloudstack_pod_test.go @@ -0,0 +1,167 @@ +// +// 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/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackPod_basic(t *testing.T) { + var pod cloudstack.Pod + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackPodDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackPod_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackPodExists( + "cloudstack_pod.foo", &pod), + testAccCheckCloudStackPodAttributes(&pod), + resource.TestCheckResourceAttr( + "cloudstack_pod.foo", "name", "terraform-pod"), + ), + }, + }, + }) +} + +func TestAccCloudStackPod_import(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackPodDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackPod_basic, + }, + + { + ResourceName: "cloudstack_pod.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckCloudStackPodExists( + n string, pod *cloudstack.Pod) 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 Pod ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + p := cs.Pod.NewListPodsParams() + p.SetId(rs.Primary.ID) + + list, err := cs.Pod.ListPods(p) + if err != nil { + return err + } + + if list.Count != 1 || list.Pods[0].Id != rs.Primary.ID { + return fmt.Errorf("Pod not found") + } + + *pod = *list.Pods[0] + + return nil + } +} + +func testAccCheckCloudStackPodAttributes( + pod *cloudstack.Pod) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if pod.Name != "terraform-pod" { + return fmt.Errorf("Bad name: %s", pod.Name) + } + + if pod.Gateway != "192.168.56.1" { + return fmt.Errorf("Bad gateway: %s", pod.Gateway) + } + + if pod.Netmask != "255.255.255.0" { + return fmt.Errorf("Bad netmask: %s", pod.Netmask) + } + + return nil + } +} + +func testAccCheckCloudStackPodDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_pod" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Pod ID is set") + } + + p := cs.Pod.NewListPodsParams() + p.SetId(rs.Primary.ID) + + list, err := cs.Pod.ListPods(p) + if err != nil { + return nil + } + + if list.Count > 0 { + return fmt.Errorf("Pod %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccCloudStackPod_basic = ` +data "cloudstack_zone" "zone" { + filter { + name = "name" + value = "Sandbox-simulator" + } +} + +# Create a pod in the zone +resource "cloudstack_pod" "foo" { + name = "terraform-pod" + zone_id = data.cloudstack_zone.zone.id + gateway = "192.168.56.1" + netmask = "255.255.255.0" + start_ip = "192.168.56.2" + end_ip = "192.168.56.254" +}` diff --git a/website/docs/d/cluster.html.markdown b/website/docs/d/cluster.html.markdown new file mode 100644 index 00000000..95a70685 --- /dev/null +++ b/website/docs/d/cluster.html.markdown @@ -0,0 +1,74 @@ +--- +subcategory: "Cluster" +layout: "cloudstack" +page_title: "CloudStack: cloudstack_cluster" +description: |- + Gets information about a cluster. +--- + +# cloudstack_cluster + +Use this data source to get information about a cluster for use in other resources. + +## Example Usage + +```hcl +data "cloudstack_cluster" "cluster" { + filter { + name = "name" + value = "cluster-1" + } +} + +output "cluster_id" { + value = data.cloudstack_cluster.cluster.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `filter` - (Required) One or more name/value pairs to filter off of. See detailed documentation below. + +### Filter Arguments + +* `name` - (Required) The name of the field to filter on. This can be any of the fields returned by the CloudStack API. +* `value` - (Required) The value to filter on. This should be a regular expression. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the cluster. +* `name` - The name of the cluster. +* `cluster_type` - Type of the cluster: CloudManaged, ExternalManaged. +* `hypervisor` - Hypervisor type of the cluster: XenServer, KVM, VMware, Hyperv, BareMetal, Simulator, Ovm3. +* `pod_id` - The Pod ID for the cluster. +* `pod_name` - The name of the pod where the cluster is created. +* `zone_id` - The Zone ID for the cluster. +* `zone_name` - The name of the zone where the cluster is created. +* `allocation_state` - The allocation state of the cluster. +* `managed_state` - The managed state of the cluster. +* `cpu_overcommit_ratio` - The CPU overcommit ratio of the cluster. +* `memory_overcommit_ratio` - The memory overcommit ratio of the cluster. +* `arch` - The CPU arch of the cluster. +* `ovm3vip` - Ovm3 vip used for pool (and cluster). +* `capacity` - The capacity information of the cluster. See Capacity below for more details. + +### Capacity + +The `capacity` attribute supports the following: + +* `capacity_allocated` - The capacity allocated. +* `capacity_total` - The total capacity. +* `capacity_used` - The capacity used. +* `cluster_id` - The ID of the cluster. +* `cluster_name` - The name of the cluster. +* `name` - The name of the capacity. +* `percent_used` - The percentage of capacity used. +* `pod_id` - The ID of the pod. +* `pod_name` - The name of the pod. +* `type` - The type of the capacity. +* `zone_id` - The ID of the zone. +* `zone_name` - The name of the zone. \ No newline at end of file diff --git a/website/docs/r/cluster.html.markdown b/website/docs/r/cluster.html.markdown new file mode 100644 index 00000000..87aa2ac2 --- /dev/null +++ b/website/docs/r/cluster.html.markdown @@ -0,0 +1,66 @@ +--- +subcategory: "Cluster" +layout: "cloudstack" +page_title: "CloudStack: cloudstack_cluster" +description: |- + Creates a cluster. +--- + +# cloudstack_cluster + +Creates a cluster. + +## Example Usage + +```hcl +resource "cloudstack_cluster" "default" { + name = "cluster-1" + cluster_type = "CloudManaged" + hypervisor = "KVM" + pod_id = "1" + zone_id = "1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the cluster. +* `cluster_type` - (Required) Type of the cluster: CloudManaged, ExternalManaged. +* `hypervisor` - (Required) Hypervisor type of the cluster: XenServer, KVM, VMware, Hyperv, BareMetal, Simulator, Ovm3. +* `pod_id` - (Required) The Pod ID for the cluster. +* `zone_id` - (Required) The Zone ID for the cluster. +* `allocation_state` - (Optional) Allocation state of this cluster for allocation of new resources. +* `arch` - (Optional) The CPU arch of the cluster. Valid options are: x86_64, aarch64. +* `guest_vswitch_name` - (Optional) Name of virtual switch used for guest traffic in the cluster. This would override zone wide traffic label setting. +* `guest_vswitch_type` - (Optional) Type of virtual switch used for guest traffic in the cluster. Allowed values are: vmwaresvs (for VMware standard vSwitch) and vmwaredvs (for VMware distributed vSwitch). +* `ovm3cluster` - (Optional) Ovm3 native OCFS2 clustering enabled for cluster. +* `ovm3pool` - (Optional) Ovm3 native pooling enabled for cluster. +* `ovm3vip` - (Optional) Ovm3 vip to use for pool (and cluster). +* `password` - (Optional) The password for the host. +* `public_vswitch_name` - (Optional) Name of virtual switch used for public traffic in the cluster. This would override zone wide traffic label setting. +* `public_vswitch_type` - (Optional) Type of virtual switch used for public traffic in the cluster. Allowed values are: vmwaresvs (for VMware standard vSwitch) and vmwaredvs (for VMware distributed vSwitch). +* `url` - (Optional) The URL for the cluster. +* `username` - (Optional) The username for the cluster. +* `vsm_ip_address` - (Optional) The IP address of the VSM associated with this cluster. +* `vsm_password` - (Optional) The password for the VSM associated with this cluster. +* `vsm_username` - (Optional) The username for the VSM associated with this cluster. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the cluster. +* `pod_name` - The name of the pod where the cluster is created. +* `zone_name` - The name of the zone where the cluster is created. +* `managed_state` - The managed state of the cluster. +* `cpu_overcommit_ratio` - The CPU overcommit ratio of the cluster. +* `memory_overcommit_ratio` - The memory overcommit ratio of the cluster. + +## Import + +Clusters can be imported; use `` as the import ID. For example: + +```shell +terraform import cloudstack_cluster.default 5fb02d7f-9513-4f96-9fbe-b5d167f4e90b \ No newline at end of file diff --git a/website/docs/r/pod.html.markdown b/website/docs/r/pod.html.markdown new file mode 100644 index 00000000..b01e90cd --- /dev/null +++ b/website/docs/r/pod.html.markdown @@ -0,0 +1,52 @@ +--- +subcategory: "Pod" +layout: "cloudstack" +page_title: "CloudStack: cloudstack_pod" +description: |- + Creates a pod. +--- + +# cloudstack_pod + +Creates a pod. + +## Example Usage + +```hcl +resource "cloudstack_pod" "default" { + name = "pod-1" + zone_id = "1" + gateway = "10.1.1.1" + netmask = "255.255.255.0" + start_ip = "10.1.1.100" + end_ip = "10.1.1.200" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the pod. +* `zone_id` - (Required) The Zone ID in which the pod will be created. +* `gateway` - (Required) The gateway for the pod. +* `netmask` - (Required) The netmask for the pod. +* `start_ip` - (Required) The starting IP address for the pod. +* `end_ip` - (Required) The ending IP address for the pod. +* `allocation_state` - (Optional) Allocation state of this pod for allocation of new resources. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the pod. +* `allocation_state` - The allocation state of the pod. +* `zone_name` - The name of the zone where the pod is created. +* `vlan_id` - The VLAN ID associated with the pod. + +## Import + +Pods can be imported; use `` as the import ID. For example: + +```shell +terraform import cloudstack_pod.default 5fb02d7f-9513-4f96-9fbe-b5d167f4e90b \ No newline at end of file