From 909fb66f5bfa56edabe21eda4ea017c72b69cbd7 Mon Sep 17 00:00:00 2001 From: Benjamin Vickers Date: Wed, 18 Mar 2015 10:11:56 +0000 Subject: [PATCH] template resource for cloudstack --- builtin/providers/cloudstack/provider.go | 1 + builtin/providers/cloudstack/provider_test.go | 5 + .../resource_cloudstack_template.go | 302 ++++++++++++++++++ .../resource_cloudstack_template_test.go | 183 +++++++++++ builtin/providers/cloudstack/resources.go | 13 + 5 files changed, 504 insertions(+) create mode 100644 builtin/providers/cloudstack/resource_cloudstack_template.go create mode 100755 builtin/providers/cloudstack/resource_cloudstack_template_test.go diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go index 8bea67ef5..8cdafd1ab 100644 --- a/builtin/providers/cloudstack/provider.go +++ b/builtin/providers/cloudstack/provider.go @@ -45,6 +45,7 @@ func Provider() terraform.ResourceProvider { "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), "cloudstack_nic": resourceCloudStackNIC(), "cloudstack_port_forward": resourceCloudStackPortForward(), + "cloudstack_template": resourceCloudStackTemplate(), "cloudstack_vpc": resourceCloudStackVPC(), "cloudstack_vpn_connection": resourceCloudStackVPNConnection(), "cloudstack_vpn_customer_gateway": resourceCloudStackVPNCustomerGateway(), diff --git a/builtin/providers/cloudstack/provider_test.go b/builtin/providers/cloudstack/provider_test.go index 537a2664f..9636acf55 100644 --- a/builtin/providers/cloudstack/provider_test.go +++ b/builtin/providers/cloudstack/provider_test.go @@ -59,3 +59,8 @@ var CLOUDSTACK_VPC_NETWORK_OFFERING = "" var CLOUDSTACK_PUBLIC_IPADDRESS = "" var CLOUDSTACK_TEMPLATE = "" var CLOUDSTACK_ZONE = "" +var CLOUDSTACK_TEMPLATE_URL = "" +var CLOUDSTACK_HYPERVISOR = "" +var CLOUDSTACK_TEMPLATE_OS_TYPE = "" +var CLOUDSTACK_TEMPLATE_FORMAT = "" +var CLOUDSTACK_TEMPLATE_CHECKSUM = "" diff --git a/builtin/providers/cloudstack/resource_cloudstack_template.go b/builtin/providers/cloudstack/resource_cloudstack_template.go new file mode 100644 index 000000000..ea5a2a23d --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_template.go @@ -0,0 +1,302 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackTemplate() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackTemplateCreate, + Read: resourceCloudStackTemplateRead, + Update: resourceCloudStackTemplateUpdate, + Delete: resourceCloudStackTemplateDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "display_text": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "hypervisor": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "os_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "format": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "requires_hvm": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "is_featured": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "password_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "template_tag": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "ssh_key_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "is_routing": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "is_public": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "is_extractable": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "bits": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "is_dynamically_scalable": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "checksum": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "is_ready": &schema.Schema{ + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func resourceCloudStackTemplateCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + //Retrieving required parameters + format := d.Get("format").(string) + + hypervisor := d.Get("hypervisor").(string) + + name := d.Get("name").(string) + + // Retrieve the os_type UUID + ostypeid, e := retrieveUUID(cs, "os_type", d.Get("os_type").(string)) + if e != nil { + return e.Error() + } + + url := d.Get("url").(string) + + //Retrieve the zone UUID + zoneid, e := retrieveUUID(cs, "zone", d.Get("zone").(string)) + if e != nil { + return e.Error() + } + + // Compute/set the display text + displaytext, ok := d.GetOk("display_text") + if !ok { + displaytext = name + } + + // Create a new parameter struct + p := cs.Template.NewRegisterTemplateParams(displaytext.(string), format, hypervisor, name, ostypeid, url, zoneid) + //Set optional parameters + p.SetPasswordenabled(d.Get("password_enabled").(bool)) + p.SetSshkeyenabled(d.Get("ssh_key_enabled").(bool)) + p.SetIsdynamicallyscalable(d.Get("is_dynamically_scalable").(bool)) + p.SetRequireshvm(d.Get("requires_hvm").(bool)) + p.SetIsfeatured(d.Get("is_featured").(bool)) + ttag := d.Get("template_tag").(string) + if ttag != "" { + //error if we give this a value as non-root + p.SetTemplatetag(ttag) + } + ir := d.Get("is_routing").(bool) + if ir == true { + p.SetIsrouting(ir) + } + p.SetIspublic(d.Get("is_public").(bool)) + p.SetIsextractable(d.Get("is_extractable").(bool)) + p.SetBits(d.Get("bits").(int)) + p.SetChecksum(d.Get("checksum").(string)) + //TODO: set project ref / details + + // Create the new template + r, err := cs.Template.RegisterTemplate(p) + if err != nil { + return fmt.Errorf("Error creating template %s: %s", name, err) + } + log.Printf("[DEBUG] Register template response: %+v\n", r) + d.SetId(r.RegisterTemplate[0].Id) + + //dont return until the template is ready to use + result := resourceCloudStackTemplateRead(d, meta) + + for !d.Get("is_ready").(bool) { + time.Sleep(5 * time.Second) + result = resourceCloudStackTemplateRead(d, meta) + } + return result +} + +func resourceCloudStackTemplateRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + log.Printf("[DEBUG] looking for template %s", d.Id()) + // Get the template details + t, count, err := cs.Template.GetTemplateByID(d.Id(), "all") + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] Template %s no longer exists", d.Get("name").(string)) + d.SetId("") + return nil + } + + return err + } + d.Set("name", t.Name) + d.Set("display_text", t.Displaytext) + d.Set("zone", t.Zonename) + d.Set("hypervisor", t.Hypervisor) + d.Set("os_type", t.Ostypename) + d.Set("format", t.Format) + d.Set("zone", t.Zonename) + d.Set("is_featured", t.Isfeatured) + d.Set("password_enabled", t.Passwordenabled) + d.Set("template_tag", t.Templatetag) + d.Set("ssh_key_enabled", t.Sshkeyenabled) + d.Set("is_public", t.Ispublic) + d.Set("is_extractable", t.Isextractable) + d.Set("is_dynamically_scalable", t.Isdynamicallyscalable) + d.Set("checksum", t.Checksum) + d.Set("is_ready", t.Isready) + log.Printf("[DEBUG] Read template values: %+v\n", d) + return nil +} + +func resourceCloudStackTemplateUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + d.Partial(true) + name := d.Get("name").(string) + + sim_attrs := []string{"name", "display_text", "bootable", "is_dynamically_scalable", + "is_routing"} + for _, attr := range sim_attrs { + if d.HasChange(attr) { + log.Printf("[DEBUG] %s changed for %s, starting update", attr, name) + p := cs.Template.NewUpdateTemplateParams(d.Id()) + switch attr { + case "name": + p.SetName(name) + case "display_text": + p.SetDisplaytext(d.Get("display_name").(string)) + case "bootable": + p.SetBootable(d.Get("bootable").(bool)) + case "is_dynamically_scalable": + p.SetIsdynamicallyscalable(d.Get("is_dynamically_scalable").(bool)) + case "is_routing": + p.SetIsrouting(d.Get("is_routing").(bool)) + default: + return fmt.Errorf("Unhandleable updateable attribute was declared, fix the code here.") + } + _, err := cs.Template.UpdateTemplate(p) + if err != nil { + return fmt.Errorf("Error updating the %s for instance %s: %s", attr, name, err) + } + d.SetPartial(attr) + } + } + if d.HasChange("os_type") { + log.Printf("[DEBUG] OS type changed for %s, starting update", name) + p := cs.Template.NewUpdateTemplateParams(d.Id()) + ostypeid, e := retrieveUUID(cs, "os_type", d.Get("os_type").(string)) + if e != nil { + return e.Error() + } + p.SetOstypeid(ostypeid) + _, err := cs.Template.UpdateTemplate(p) + if err != nil { + return fmt.Errorf("Error updating the OS type for instance %s: %s", name, err) + } + d.SetPartial("os_type") + + } + d.Partial(false) + return resourceCloudStackTemplateRead(d, meta) +} + +func resourceCloudStackTemplateDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Template.NewDeleteTemplateParams(d.Id()) + + // Delete the template + log.Printf("[INFO] Destroying instance: %s", d.Get("name").(string)) + _, err := cs.Template.DeleteTemplate(p) + if err != nil { + // This is a very poor way to be told the UUID 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 template %s: %s", d.Get("name").(string), err) + } + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_template_test.go b/builtin/providers/cloudstack/resource_cloudstack_template_test.go new file mode 100755 index 000000000..cb1b729ca --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_template_test.go @@ -0,0 +1,183 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackTemplate_full(t *testing.T) { + var template cloudstack.Template + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackTemplate_options, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackTemplateExists("cloudstack_template.foo", &template), + testAccCheckCloudStackTemplateBasicAttributes(&template), + testAccCheckCloudStackTemplateOptionalAttributes(&template), + ), + }, + }, + }) +} + +func testAccCheckCloudStackTemplateExists(n string, template *cloudstack.Template) 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 template ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + tmpl, _, err := cs.Template.GetTemplateByID(rs.Primary.ID, "all") + + if err != nil { + return err + } + + if tmpl.Id != rs.Primary.ID { + return fmt.Errorf("Template not found") + } + + *template = *tmpl + + return nil + } +} + +func testAccCheckCloudStackTemplateDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_template" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No template ID is set") + } + + p := cs.Template.NewDeleteTemplateParams(rs.Primary.ID) + _, err := cs.Template.DeleteTemplate(p) + + if err != nil { + return fmt.Errorf( + "Error deleting template (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackTemplate_basic = fmt.Sprintf(` +resource "cloudstack_template" "foo" { + name = "terraform-acc-test" + url = "%s" + hypervisor = "%s" + os_type = "%s" + format = "%s" + zone = "%s" +} +`, + CLOUDSTACK_TEMPLATE_URL, + CLOUDSTACK_HYPERVISOR, + CLOUDSTACK_TEMPLATE_OS_TYPE, + CLOUDSTACK_TEMPLATE_FORMAT, + CLOUDSTACK_ZONE) + +func testAccCheckCloudStackTemplateBasicAttributes(template *cloudstack.Template) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if template.Name != "terraform-acc-test" { + return fmt.Errorf("Bad name: %s", template.Name) + } + + //todo: could add size to schema and check that, would assure we downloaded/initialize the image properly + + if template.Hypervisor != CLOUDSTACK_HYPERVISOR { + return fmt.Errorf("Bad hypervisor: %s", template.Hypervisor) + } + + if template.Ostypename != CLOUDSTACK_TEMPLATE_OS_TYPE { + return fmt.Errorf("Bad os type: %s", template.Ostypename) + } + + if template.Format != CLOUDSTACK_TEMPLATE_FORMAT { + return fmt.Errorf("Bad format: %s", template.Format) + } + + if template.Zonename != CLOUDSTACK_ZONE { + return fmt.Errorf("Bad zone: %s", template.Zonename) + } + + return nil + } +} + +//may prove difficult to test isrouting, isfeatured, ispublic, bits so not set here +var testAccCloudStackTemplate_options = fmt.Sprintf(` +resource "cloudstack_template" "foo" { + name = "terraform-acc-test" + url = "%s" + hypervisor = "%s" + os_type = "%s" + format = "%s" + zone = "%s" + password_enabled = true + template_tag = "acctest" + ssh_key_enabled = true + is_extractable = true + is_dynamically_scalable = true + checksum = "%s" +} +`, + CLOUDSTACK_TEMPLATE_URL, + CLOUDSTACK_HYPERVISOR, + CLOUDSTACK_TEMPLATE_OS_TYPE, + CLOUDSTACK_TEMPLATE_FORMAT, + CLOUDSTACK_ZONE, + CLOUDSTACK_TEMPLATE_CHECKSUM) + +func testAccCheckCloudStackTemplateOptionalAttributes(template *cloudstack.Template) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if !template.Passwordenabled { + return fmt.Errorf("Bad password_enabled: %s", template.Passwordenabled) + } + + if template.Templatetag != "acctest" { + return fmt.Errorf("Bad template_tag: %s", template.Templatetag) + } + + if !template.Sshkeyenabled { + return fmt.Errorf("Bad ssh_key_enabled: %s", template.Sshkeyenabled) + } + + if !template.Isextractable { + return fmt.Errorf("Bad is_extractable: %s", template.Isextractable) + } + + if !template.Isdynamicallyscalable { + return fmt.Errorf("Bad is_dynamically_scalable: %s", template.Isdynamicallyscalable) + } + + if template.Checksum != CLOUDSTACK_TEMPLATE_CHECKSUM { + return fmt.Errorf("Bad checksum: %s", template.Checksum) + } + + return nil + } +} diff --git a/builtin/providers/cloudstack/resources.go b/builtin/providers/cloudstack/resources.go index 76d38eb7c..3859930e2 100644 --- a/builtin/providers/cloudstack/resources.go +++ b/builtin/providers/cloudstack/resources.go @@ -36,6 +36,19 @@ func retrieveUUID(cs *cloudstack.CloudStackClient, name, value string) (uuid str uuid, err = cs.ServiceOffering.GetServiceOfferingID(value) case "network_offering": uuid, err = cs.NetworkOffering.GetNetworkOfferingID(value) + case "os_type": + p := cs.GuestOS.NewListOsTypesParams() + p.SetDescription(value) + o, e := cs.GuestOS.ListOsTypes(p) + if e != nil { + err = e + break + } + if o.Count == 1 { + uuid = o.OsTypes[0].Id + break + } + err = fmt.Errorf("Could not find UUID of OS Type: %s", value) case "vpc_offering": uuid, err = cs.VPC.GetVPCOfferingID(value) case "vpc":