You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by vi...@apache.org on 2024/03/07 09:06:44 UTC

(cloudstack-terraform-provider) branch main updated: Add Hosts resource and Pod data source (#69)

This is an automated email from the ASF dual-hosted git repository.

vishesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git


The following commit(s) were added to refs/heads/main by this push:
     new c7f0776  Add Hosts resource and Pod data source (#69)
c7f0776 is described below

commit c7f077646e3f7a3204d062ccca0c3d6e0803debe
Author: Fábio Matavelli <fa...@gmail.com>
AuthorDate: Thu Mar 7 09:06:39 2024 +0000

    Add Hosts resource and Pod data source (#69)
---
 cloudstack/data_source_cloudstack_pod.go      | 281 +++++++++++++++++++++++
 cloudstack/data_source_cloudstack_pod_test.go |  50 ++++
 cloudstack/provider.go                        |   2 +
 cloudstack/resource_cloudstack_host.go        | 316 ++++++++++++++++++++++++++
 cloudstack/resource_cloudstack_host_test.go   | 143 ++++++++++++
 5 files changed, 792 insertions(+)

diff --git a/cloudstack/data_source_cloudstack_pod.go b/cloudstack/data_source_cloudstack_pod.go
new file mode 100644
index 0000000..d6d211e
--- /dev/null
+++ b/cloudstack/data_source_cloudstack_pod.go
@@ -0,0 +1,281 @@
+//
+// 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/helper/schema"
+)
+
+func dataSourceCloudstackPod() *schema.Resource {
+	return &schema.Resource{
+		Read: datasourceCloudStackPodRead,
+		Schema: map[string]*schema.Schema{
+			"filter": dataSourceFiltersSchema(),
+
+			//Computed values
+			"pod_id": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"name": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"zone_id": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"end_ip": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"gateway": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"netmask": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"start_ip": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"allocation_state": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"zone_name": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"vlan_id": {
+				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,
+						},
+					},
+				},
+			},
+			"ip_ranges": {
+				Type:     schema.TypeList,
+				Computed: true,
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"end_ip": {
+							Type:     schema.TypeString,
+							Computed: true,
+						},
+						"gateway": {
+							Type:     schema.TypeString,
+							Computed: true,
+						},
+						"for_system_vms": {
+							Type:     schema.TypeString,
+							Computed: true,
+						},
+						"start_ip": {
+							Type:     schema.TypeString,
+							Computed: true,
+						},
+						"vlan_id": {
+							Type:     schema.TypeString,
+							Computed: true,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func dsFlattenPodCapacity(capacity []cloudstack.PodCapacity) []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 dsFlattenPodIpRanges(ip_ranges []cloudstack.PodIpranges) []map[string]interface{} {
+	ranges := make([]map[string]interface{}, len(ip_ranges))
+	for i, ip_range := range ip_ranges {
+		ranges[i] = map[string]interface{}{
+			"end_ip":         ip_range.Endip,
+			"for_system_vms": ip_range.Forsystemvms,
+			"start_ip":       ip_range.Startip,
+			"vlan_id":        ip_range.Vlanid,
+		}
+	}
+	return ranges
+}
+
+func datasourceCloudStackPodRead(d *schema.ResourceData, meta interface{}) error {
+	cs := meta.(*cloudstack.CloudStackClient)
+	p := cs.Pod.NewListPodsParams()
+
+	csPods, err := cs.Pod.ListPods(p)
+	if err != nil {
+		return fmt.Errorf("failed to list pods: %s", err)
+	}
+
+	filters := d.Get("filter")
+
+	for _, pod := range csPods.Pods {
+		match, err := applyPodFilters(pod, filters.(*schema.Set))
+		if err != nil {
+			return err
+		}
+		if match {
+			return podDescriptionAttributes(d, pod)
+		}
+	}
+
+	return fmt.Errorf("no pods found")
+}
+
+func podDescriptionAttributes(d *schema.ResourceData, pod *cloudstack.Pod) error {
+	d.SetId(pod.Id)
+	var end_ip string
+	if len(pod.Endip) > 0 {
+		end_ip = pod.Endip[0]
+	}
+
+	fields := map[string]interface{}{
+		"pod_id":           pod.Id,
+		"name":             pod.Name,
+		"allocation_state": pod.Allocationstate,
+		"gateway":          pod.Gateway,
+		"netmask":          pod.Netmask,
+		"start_ip":         pod.Startip[0],
+		"vlan_id":          pod.Vlanid[0],
+		"zone_id":          pod.Zoneid,
+		"zone_name":        pod.Zonename,
+		"end_ip":           end_ip,
+		"ip_ranges":        dsFlattenPodIpRanges(pod.Ipranges),
+		"capacity":         dsFlattenPodCapacity(pod.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 applyPodFilters(pod *cloudstack.Pod, filters *schema.Set) (bool, error) {
+	val := reflect.ValueOf(pod).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), "_", "")
+		podField := val.FieldByNameFunc(func(fieldName string) bool {
+			if strings.EqualFold(fieldName, updatedName) {
+				updatedName = fieldName
+				return true
+			}
+			return false
+		}).String()
+
+		if r.MatchString(podField) {
+			return true, nil
+		}
+	}
+
+	return false, nil
+}
diff --git a/cloudstack/data_source_cloudstack_pod_test.go b/cloudstack/data_source_cloudstack_pod_test.go
new file mode 100644
index 0000000..815e57e
--- /dev/null
+++ b/cloudstack/data_source_cloudstack_pod_test.go
@@ -0,0 +1,50 @@
+//
+// 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/helper/resource"
+)
+
+func TestAccPodDataSource_basic(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:  func() { testAccPreCheck(t) },
+		Providers: testAccProviders,
+		Steps: []resource.TestStep{
+			{
+				Config: testPodDataSourceConfig_basic,
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("data.cloudstack_pod.test", "name", "POD0"),
+				),
+			},
+		},
+	})
+}
+
+const testPodDataSourceConfig_basic = `
+ data "cloudstack_pod" "test" {
+    filter {
+    	name  = "name"
+    	value = "POD0"
+    }
+  }
+  `
diff --git a/cloudstack/provider.go b/cloudstack/provider.go
index 3def7c9..c35a263 100644
--- a/cloudstack/provider.go
+++ b/cloudstack/provider.go
@@ -89,6 +89,7 @@ func Provider() terraform.ResourceProvider {
 			"cloudstack_ipaddress":        dataSourceCloudstackIPAddress(),
 			"cloudstack_user":             dataSourceCloudstackUser(),
 			"cloudstack_vpn_connection":   dataSourceCloudstackVPNConnection(),
+			"cloudstack_pod":              dataSourceCloudstackPod(),
 		},
 
 		ResourcesMap: map[string]*schema.Resource{
@@ -98,6 +99,7 @@ func Provider() terraform.ResourceProvider {
 			"cloudstack_disk":                 resourceCloudStackDisk(),
 			"cloudstack_egress_firewall":      resourceCloudStackEgressFirewall(),
 			"cloudstack_firewall":             resourceCloudStackFirewall(),
+			"cloudstack_host":                 resourceCloudStackHost(),
 			"cloudstack_instance":             resourceCloudStackInstance(),
 			"cloudstack_ipaddress":            resourceCloudStackIPAddress(),
 			"cloudstack_kubernetes_cluster":   resourceCloudStackKubernetesCluster(),
diff --git a/cloudstack/resource_cloudstack_host.go b/cloudstack/resource_cloudstack_host.go
new file mode 100644
index 0000000..46fc8e2
--- /dev/null
+++ b/cloudstack/resource_cloudstack_host.go
@@ -0,0 +1,316 @@
+//
+// 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 (
+	"errors"
+	"fmt"
+	"log"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/apache/cloudstack-go/v2/cloudstack"
+	"github.com/hashicorp/terraform/helper/schema"
+)
+
+func resourceCloudStackHost() *schema.Resource {
+	return &schema.Resource{
+		Read:   resourceCloudStackHostRead,
+		Update: resourceCloudStackHostUpdate,
+		Create: resourceCloudStackHostCreate,
+		Delete: resourceCloudStackHostDelete,
+		Schema: map[string]*schema.Schema{
+			"hypervisor": {
+				Type:     schema.TypeString,
+				Required: true,
+				ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
+					validHypervisors := []string{"xenserver", "kvm", "vmware", "baremetal", "simulator"}
+
+					sort.Strings(validHypervisors)
+
+					if sort.SearchStrings(validHypervisors, v.(string)) >= len(validHypervisors) {
+						errors = append(errors, fmt.Errorf("%q must be one of %v", k, validHypervisors))
+					}
+					return
+				},
+				ForceNew: true,
+			},
+			"pod_id": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"url": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"zone_id": {
+				Type:     schema.TypeString,
+				Required: true,
+				ForceNew: true,
+			},
+			"cluster_id": {
+				Type:     schema.TypeString,
+				Optional: true,
+				ForceNew: true,
+				ConflictsWith: []string{
+					"cluster_name",
+				},
+			},
+			"cluster_name": {
+				Type:     schema.TypeString,
+				Optional: true,
+				ForceNew: true,
+				ConflictsWith: []string{
+					"cluster_id",
+				},
+			},
+			"host_tags": {
+				Type:     schema.TypeList,
+				Optional: true,
+				Elem: &schema.Schema{
+					Type: schema.TypeString,
+				},
+			},
+			"username": {
+				Type:     schema.TypeString,
+				Optional: true,
+			},
+			"password": {
+				Type:      schema.TypeString,
+				Optional:  true,
+				Sensitive: true,
+			},
+			"prevent_destroy": {
+				Type:        schema.TypeBool,
+				Description: "Prevent the host from being destroyed. This is useful when you want to avoid destroy the host in any change.",
+				Optional:    true,
+				Default:     false,
+			},
+			"force_destroy": {
+				Type:        schema.TypeBool,
+				Description: "Force the host to be destroyed.",
+				Optional:    true,
+				Default:     false,
+			},
+			"allocation_state": {
+				Type:     schema.TypeString,
+				Optional: true,
+				Default:  "Enabled",
+			},
+			"state": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"resource_state": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"name": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			// Creating a host can fail if the instance is still being created and Cloudsack
+			// user is created during cloud-init. This timeout is used to wait for the host
+			// to be created and Cloudstack user to be available.
+			"create_timeout": {
+				Type:        schema.TypeInt,
+				Description: "Timeout in seconds to wait for the host to be created.",
+				Optional:    true,
+				Default:     300,
+			},
+			// Destroying a host will put it in Maintenance mode first. If the VMs are still
+			// being migrated, the host will be in state PrepareForMaintenance. This timeout
+			// is used to wait for the host to be in Maintenance state.
+			"destroy_timeout": {
+				Type:        schema.TypeInt,
+				Description: "Timeout in seconds to wait for the host to be destroyed.",
+				Optional:    true,
+				Default:     300,
+			},
+		},
+	}
+}
+
+func resourceCloudStackHostCreate(d *schema.ResourceData, meta interface{}) error {
+	cs := meta.(*cloudstack.CloudStackClient)
+	hypervisor := d.Get("hypervisor").(string)
+	pod_id := d.Get("pod_id").(string)
+	url := d.Get("url").(string)
+	zone_id := d.Get("zone_id").(string)
+
+	p := cs.Host.NewAddHostParams(hypervisor, pod_id, url, zone_id)
+
+	if cluster_id, ok := d.GetOk("cluster_id"); ok {
+		p.SetClusterid(cluster_id.(string))
+	}
+
+	if cluster_name, ok := d.GetOk("cluster_name"); ok {
+		p.SetClustername(cluster_name.(string))
+	}
+
+	if host_tags, ok := d.GetOk("host_tags"); ok {
+		p.SetHosttags(host_tags.([]string))
+	}
+
+	if username, ok := d.GetOk("username"); ok {
+		p.SetUsername(username.(string))
+	}
+
+	if password, ok := d.GetOk("password"); ok {
+		p.SetPassword(password.(string))
+	}
+
+	timeout := time.After(time.Duration(d.Get("create_timeout").(int)) * time.Second)
+	tick := time.NewTicker(5 * time.Second)
+	var err error
+	var host *cloudstack.AddHostResponse
+
+	for {
+		select {
+		case <-timeout:
+			return fmt.Errorf("timeout waiting for Host to be created, with error: %s", err)
+		case <-tick.C:
+			log.Printf("[DEBUG] Trying to create host %s", d.Get("url").(string))
+			host, err = cs.Host.AddHost(p)
+			if err != nil {
+				log.Printf("[ERROR] Error creating host %s: %s. Will try again...", d.Get("url").(string), err)
+				continue
+			}
+
+			if host.Id != "" {
+				log.Printf("[DEBUG] Host %s successfully created", url)
+				d.SetId(host.Id)
+				return resourceCloudStackHostRead(d, meta)
+			}
+		}
+	}
+}
+
+func resourceCloudStackHostRead(d *schema.ResourceData, meta interface{}) error {
+	cs := meta.(*cloudstack.CloudStackClient)
+	log.Printf("[DEBUG] Retrieving Host %s", d.Get("url").(string))
+
+	h, count, err := cs.Host.GetHostByID(d.Id())
+
+	if err != nil {
+		if count == 0 {
+			log.Printf("[WARN] Host %s does no longer exist", d.Get("url").(string))
+			d.SetId("")
+			return nil
+		}
+		return err
+	}
+
+	d.SetId(h.Id)
+
+	fields := map[string]interface{}{
+		"hypervisor":     h.Hypervisor,
+		"pod_id":         h.Podid,
+		"zone_id":        h.Zoneid,
+		"state":          h.State,
+		"resource_state": h.Resourcestate,
+		"name":           h.Name,
+	}
+
+	for k, v := range fields {
+		if err := d.Set(k, v); err != nil {
+			return err
+		}
+	}
+
+	if cluster_id := d.Get("cluster_id"); cluster_id != "" {
+		d.Set("cluster_id", h.Clusterid)
+	} else {
+		d.Set("cluster_name", h.Clustername)
+	}
+
+	if h.Hosttags != "" {
+		d.Set("host_tags", strings.Split(h.Hosttags, ","))
+	}
+
+	return nil
+}
+
+func resourceCloudStackHostUpdate(d *schema.ResourceData, meta interface{}) error {
+	log.Printf("[DEBUG] Updating Host: %s", d.Id())
+
+	cs := meta.(*cloudstack.CloudStackClient)
+
+	p := cs.Host.NewUpdateHostParams(d.Id())
+
+	if d.HasChange("allocation_state") {
+		log.Printf("[DEBUG] Updating Host allocation state: %s", d.Id())
+		p.SetAllocationstate(d.Get("allocation_state").(string))
+	}
+
+	if d.HasChange("host_tags") {
+		log.Printf("[DEBUG] Updating Host tags: %s", d.Id())
+		p.SetHosttags(d.Get("host_tags").([]string))
+	}
+
+	return resourceCloudStackHostRead(d, meta)
+}
+
+func resourceCloudStackHostDelete(d *schema.ResourceData, meta interface{}) error {
+	cs := meta.(*cloudstack.CloudStackClient)
+
+	if d.Get("prevent_destroy").(bool) {
+		log.Printf("[INFO] Skipping Host deletion: %s", d.Id())
+		return fmt.Errorf("host %s is marked to be protected from deletion", d.Id())
+	}
+
+	log.Printf("[INFO] Removing Host: %s", d.Id())
+	mm := cs.Host.NewPrepareHostForMaintenanceParams(d.Id())
+	_, err := cs.Host.PrepareHostForMaintenance(mm)
+
+	if err != nil {
+		return fmt.Errorf("error preparing Host for maintenance: %s", err)
+	}
+
+	timeout := time.After(time.Duration(d.Get("destroy_timeout").(int)) * time.Second)
+	tick := time.NewTicker(3 * time.Second)
+
+	for {
+		select {
+		case <-timeout:
+			return errors.New("timeout waiting for Host to enter Maintenance state")
+		case <-tick.C:
+			log.Printf("[DEBUG] Checking Host state: %s", d.Id())
+			err = resourceCloudStackHostRead(d, meta)
+			if err != nil {
+				return fmt.Errorf("error reading Host: %s", err)
+			}
+
+			if d.Get("resource_state").(string) == "Maintenance" || d.Get("resource_state").(string) == "Disconnected" {
+				log.Printf("[INFO] Deleting Host: %s", d.Id())
+				h := cs.Host.NewDeleteHostParams(d.Id())
+				_, err = cs.Host.DeleteHost(h)
+
+				if err != nil {
+					return fmt.Errorf("error deleting Host: %s", err)
+				}
+				return nil
+			}
+		}
+	}
+}
diff --git a/cloudstack/resource_cloudstack_host_test.go b/cloudstack/resource_cloudstack_host_test.go
new file mode 100644
index 0000000..8da6f58
--- /dev/null
+++ b/cloudstack/resource_cloudstack_host_test.go
@@ -0,0 +1,143 @@
+//
+// 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"
+	"regexp"
+	"testing"
+
+	"github.com/apache/cloudstack-go/v2/cloudstack"
+	"github.com/hashicorp/terraform/helper/resource"
+	"github.com/hashicorp/terraform/terraform"
+)
+
+func TestAccCloudStackHost_basic(t *testing.T) {
+	var h cloudstack.Host
+	resource.Test(t, resource.TestCase{
+		PreCheck:  func() { testAccPreCheck(t) },
+		Providers: testAccProviders,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackHost_basic,
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckCloudStackHostExists("cloudstack_host.test", &h),
+					resource.TestCheckResourceAttr("cloudstack_host.test", "hypervisor", "Simulator"),
+					resource.TestCheckResourceAttr("cloudstack_host.test", "cluster_name", "C1"),
+					resource.TestCheckResourceAttrSet("cloudstack_host.test", "state"),
+					resource.TestCheckResourceAttrSet("cloudstack_host.test", "name"),
+				),
+			},
+		},
+	})
+}
+
+const testAccCloudStackHost_basic = `
+data "cloudstack_zone" "zone" {
+	filter {
+		name  = "name"
+		value = "Sandbox-simulator"
+	}
+}
+
+data "cloudstack_pod" "pod" {
+	filter {
+		name  = "name"
+		value = "POD0"
+	}
+}
+
+resource "cloudstack_host" "test" {
+  hypervisor 	= "Simulator"
+  pod_id     	= data.cloudstack_pod.pod.id
+  url        	= "http://sim/c1/h"
+  zone_id    	= data.cloudstack_zone.zone.id
+  cluster_name 	= "C1"
+  username   	= "root"
+  password   	= "password"
+}
+`
+
+func testAccCheckCloudStackHostExists(n string, h *cloudstack.Host) 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 host ID is set")
+		}
+
+		cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
+		resp, _, err := cs.Host.GetHostByID(rs.Primary.ID)
+		if err != nil {
+			return err
+		}
+
+		if resp.Id != rs.Primary.ID {
+			return fmt.Errorf("Host not found")
+		}
+
+		*h = *resp
+
+		return nil
+	}
+}
+
+func TestAccCloudStackHost_fail(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:  func() { testAccPreCheck(t) },
+		Providers: testAccProviders,
+		Steps: []resource.TestStep{
+			{
+				Config:      testAccCloudStackHost_fail,
+				ExpectError: regexp.MustCompile("timeout waiting for Host to be created, with error: .*Username and Password need to be provided.*"),
+			},
+		},
+	})
+}
+
+const testAccCloudStackHost_fail = `
+data "cloudstack_zone" "zone_fail" {
+	filter {
+		name  = "name"
+		value = "Sandbox-simulator"
+	}
+}
+
+data "cloudstack_pod" "pod_fail" {
+	filter {
+		name  = "name"
+		value = "POD0"
+	}
+}
+
+resource "cloudstack_host" "test_fail" {
+  hypervisor 	 = "Simulator"
+  pod_id     	 = data.cloudstack_pod.pod_fail.id
+  url        	 = "http://sim/c1/h"
+  zone_id    	 = data.cloudstack_zone.zone_fail.id
+  cluster_name 	 = "C1"
+  username   	 = "root"
+  password   	 = ""
+  create_timeout = 10
+}
+`