cloudstack/resource_cloudstack_host.go (253 lines of code) (raw):

// // 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-plugin-sdk/v2/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 } } } }