diff --git a/builtin/providers/cloudstack/resource_cloudstack_disk.go b/builtin/providers/cloudstack/resource_cloudstack_disk.go index 7bea9c105..f9d472e30 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_disk.go +++ b/builtin/providers/cloudstack/resource_cloudstack_disk.go @@ -28,8 +28,8 @@ func resourceCloudStackDisk() *schema.Resource { Default: false, }, - "device": &schema.Schema{ - Type: schema.TypeString, + "device_id": &schema.Schema{ + Type: schema.TypeInt, Optional: true, Computed: true, }, @@ -59,6 +59,7 @@ func resourceCloudStackDisk() *schema.Resource { "project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, @@ -116,7 +117,7 @@ func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) erro // Set the volume ID and partials d.SetId(r.Id) d.SetPartial("name") - d.SetPartial("device") + d.SetPartial("device_id") d.SetPartial("disk_offering") d.SetPartial("size") d.SetPartial("virtual_machine_id") @@ -163,28 +164,7 @@ func resourceCloudStackDiskRead(d *schema.ResourceData, meta interface{}) error setValueOrID(d, "zone", v.Zonename, v.Zoneid) if v.Attached != "" { - // Get the virtual machine details - vm, _, err := cs.VirtualMachine.GetVirtualMachineByID( - v.Virtualmachineid, - cloudstack.WithProject(d.Get("project").(string)), - ) - if err != nil { - return err - } - - // Get the guest OS type details - os, _, err := cs.GuestOS.GetOsTypeByID(vm.Guestosid) - if err != nil { - return err - } - - // Get the guest OS category details - c, _, err := cs.GuestOS.GetOsCategoryByID(os.Oscategoryid) - if err != nil { - return err - } - - d.Set("device", retrieveDeviceName(v.Deviceid, c.Name)) + d.Set("device_id", int(v.Deviceid)) d.Set("virtual_machine_id", v.Virtualmachineid) } @@ -235,9 +215,9 @@ func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) erro d.SetPartial("size") } - // If the device changed, just detach here so we can re-attach the + // If the device ID changed, just detach here so we can re-attach the // volume at the end of this function - if d.HasChange("device") || d.HasChange("virtual_machine") { + if d.HasChange("device_id") || d.HasChange("virtual_machine") { // Detach the volume if err := resourceCloudStackDiskDetach(d, meta); err != nil { return fmt.Errorf("Error detaching disk %s from virtual machine: %s", name, err) @@ -253,7 +233,7 @@ func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) erro // Set the additional partials d.SetPartial("attach") - d.SetPartial("device") + d.SetPartial("device_id") d.SetPartial("virtual_machine_id") } else { // Detach the volume @@ -304,21 +284,14 @@ func resourceCloudStackDiskAttach(d *schema.ResourceData, meta interface{}) erro // Create a new parameter struct p := cs.Volume.NewAttachVolumeParams(d.Id(), virtualmachineid.(string)) - if device, ok := d.GetOk("device"); ok { - // Retrieve the device ID - deviceid := retrieveDeviceID(device.(string)) - if deviceid == -1 { - return fmt.Errorf("Device %s is not a valid device", device.(string)) - } - - // Set the device ID - p.SetDeviceid(deviceid) + if deviceid, ok := d.GetOk("device_id"); ok { + p.SetDeviceid(int64(deviceid.(int))) } // Attach the new volume - r, err := Retry(4, retryableAttachVolumeFunc(cs, p)) + r, err := Retry(10, retryableAttachVolumeFunc(cs, p)) if err != nil { - return err + return fmt.Errorf("Error attaching volume to VM: %s", err) } d.SetId(r.(*cloudstack.AttachVolumeResponse).Id) @@ -397,115 +370,3 @@ func retryableAttachVolumeFunc( return r, nil } } - -func retrieveDeviceID(device string) int64 { - switch device { - case "/dev/xvdb", "D:": - return 1 - case "/dev/xvdc", "E:": - return 2 - case "/dev/xvde", "F:": - return 4 - case "/dev/xvdf", "G:": - return 5 - case "/dev/xvdg", "H:": - return 6 - case "/dev/xvdh", "I:": - return 7 - case "/dev/xvdi", "J:": - return 8 - case "/dev/xvdj", "K:": - return 9 - case "/dev/xvdk", "L:": - return 10 - case "/dev/xvdl", "M:": - return 11 - case "/dev/xvdm", "N:": - return 12 - case "/dev/xvdn", "O:": - return 13 - case "/dev/xvdo", "P:": - return 14 - case "/dev/xvdp", "Q:": - return 15 - default: - return -1 - } -} - -func retrieveDeviceName(device int64, os string) string { - switch device { - case 1: - if os == "Windows" { - return "D:" - } - return "/dev/xvdb" - case 2: - if os == "Windows" { - return "E:" - } - return "/dev/xvdc" - case 4: - if os == "Windows" { - return "F:" - } - return "/dev/xvde" - case 5: - if os == "Windows" { - return "G:" - } - return "/dev/xvdf" - case 6: - if os == "Windows" { - return "H:" - } - return "/dev/xvdg" - case 7: - if os == "Windows" { - return "I:" - } - return "/dev/xvdh" - case 8: - if os == "Windows" { - return "J:" - } - return "/dev/xvdi" - case 9: - if os == "Windows" { - return "K:" - } - return "/dev/xvdj" - case 10: - if os == "Windows" { - return "L:" - } - return "/dev/xvdk" - case 11: - if os == "Windows" { - return "M:" - } - return "/dev/xvdl" - case 12: - if os == "Windows" { - return "N:" - } - return "/dev/xvdm" - case 13: - if os == "Windows" { - return "O:" - } - return "/dev/xvdn" - case 14: - if os == "Windows" { - return "P:" - } - return "/dev/xvdo" - case 15: - if os == "Windows" { - return "Q:" - } - return "/dev/xvdp" - default: - return "unknown" - } -} diff --git a/builtin/providers/cloudstack/resource_cloudstack_disk_test.go b/builtin/providers/cloudstack/resource_cloudstack_disk_test.go index a4e9bc582..2484534fd 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_disk_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_disk_test.go @@ -29,7 +29,7 @@ func TestAccCloudStackDisk_basic(t *testing.T) { }) } -func TestAccCloudStackDisk_device(t *testing.T) { +func TestAccCloudStackDisk_deviceID(t *testing.T) { var disk cloudstack.Volume resource.Test(t, resource.TestCase{ @@ -38,13 +38,13 @@ func TestAccCloudStackDisk_device(t *testing.T) { CheckDestroy: testAccCheckCloudStackDiskDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccCloudStackDisk_device, + Config: testAccCloudStackDisk_deviceID, Check: resource.ComposeTestCheckFunc( testAccCheckCloudStackDiskExists( "cloudstack_disk.foo", &disk), testAccCheckCloudStackDiskAttributes(&disk), resource.TestCheckResourceAttr( - "cloudstack_disk.foo", "device", "/dev/xvde"), + "cloudstack_disk.foo", "device_id", "4"), ), }, }, @@ -170,7 +170,7 @@ resource "cloudstack_disk" "foo" { CLOUDSTACK_DISK_OFFERING_1, CLOUDSTACK_ZONE) -var testAccCloudStackDisk_device = fmt.Sprintf(` +var testAccCloudStackDisk_deviceID = fmt.Sprintf(` resource "cloudstack_instance" "foobar" { name = "terraform-test" display_name = "terraform" @@ -184,7 +184,7 @@ resource "cloudstack_instance" "foobar" { resource "cloudstack_disk" "foo" { name = "terraform-disk" attach = true - device = "/dev/xvde" + device_id = 4 disk_offering = "%s" virtual_machine_id = "${cloudstack_instance.foobar.id}" zone = "${cloudstack_instance.foobar.zone}" diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance.go b/builtin/providers/cloudstack/resource_cloudstack_instance.go index 550647f4e..6939f05df 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_instance.go +++ b/builtin/providers/cloudstack/resource_cloudstack_instance.go @@ -105,6 +105,7 @@ func resourceCloudStackInstance() *schema.Resource { "project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, @@ -251,9 +252,12 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) p.SetKeypair(keypair.(string)) } - if ud, err := getUserData(d, cs); err != nil { - return err - } else if len(ud) > 0 { + if userData, ok := d.GetOk("user_data"); ok { + ud, err := getUserData(userData.(string), cs.HTTPGETOnly) + if err != nil { + return err + } + p.SetUserdata(ud) } @@ -437,6 +441,7 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) d.SetPartial("service_offering") } + // Check if the affinity group IDs have changed and if so, update the IDs if d.HasChange("affinity_group_ids") { p := cs.AffinityGroup.NewUpdateVMAffinityGroupParams(d.Id()) groups := []string{} @@ -450,6 +455,7 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) p.SetAffinitygroupids(groups) } + // Check if the affinity group names have changed and if so, update the names if d.HasChange("affinity_group_names") { p := cs.AffinityGroup.NewUpdateVMAffinityGroupParams(d.Id()) groups := []string{} @@ -463,6 +469,7 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) p.SetAffinitygroupids(groups) } + // Check if the keypair has changed and if so, update the keypair if d.HasChange("keypair") { log.Printf("[DEBUG] SSH keypair changed for %s, starting update", name) @@ -477,10 +484,11 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) d.SetPartial("keypair") } + // Check if the user data has changed and if so, update the user data if d.HasChange("user_data") { log.Printf("[DEBUG] user_data changed for %s, starting update", name) - ud, err := getUserData(d, cs) + ud, err := getUserData(d.Get("user_data").(string), cs.HTTPGETOnly) if err != nil { return err } @@ -533,28 +541,23 @@ func resourceCloudStackInstanceDelete(d *schema.ResourceData, meta interface{}) return nil } -// getUserData returns user_data as a base64 encoded string. An empty -// string is returned if unset. -func getUserData(d *schema.ResourceData, cs *cloudstack.CloudStackClient) (string, error) { - if userData, ok := d.GetOk("user_data"); ok { - ud := base64.StdEncoding.EncodeToString([]byte(userData.(string))) +// getUserData returns the user data as a base64 encoded string +func getUserData(userData string, httpGetOnly bool) (string, error) { + ud := base64.StdEncoding.EncodeToString([]byte(userData)) - // deployVirtualMachine uses POST by default, so max userdata is 32K - maxUD := 32768 + // deployVirtualMachine uses POST by default, so max userdata is 32K + maxUD := 32768 - if cs.HTTPGETOnly { - // deployVirtualMachine using GET instead, so max userdata is 2K - maxUD = 2048 - } - - if len(ud) > maxUD { - return "", fmt.Errorf( - "The supplied user_data contains %d bytes after encoding, "+ - "this exeeds the limit of %d bytes", len(ud), maxUD) - } - - return ud, nil + if httpGetOnly { + // deployVirtualMachine using GET instead, so max userdata is 2K + maxUD = 2048 } - return "", nil + if len(ud) > maxUD { + return "", fmt.Errorf( + "The supplied user_data contains %d bytes after encoding, "+ + "this exeeds the limit of %d bytes", len(ud), maxUD) + } + + return ud, nil } diff --git a/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go b/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go index 81211cd95..ffee80f4a 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go +++ b/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go @@ -31,6 +31,7 @@ func resourceCloudStackIPAddress() *schema.Resource { "project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, diff --git a/builtin/providers/cloudstack/resource_cloudstack_network.go b/builtin/providers/cloudstack/resource_cloudstack_network.go index 049f127ad..1fccdfd55 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_network.go +++ b/builtin/providers/cloudstack/resource_cloudstack_network.go @@ -97,6 +97,7 @@ func resourceCloudStackNetwork() *schema.Resource { "project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, @@ -133,20 +134,35 @@ func resourceCloudStackNetworkCreate(d *schema.ResourceData, meta interface{}) e if !ok { displaytext = name } + // Create a new parameter struct p := cs.Network.NewCreateNetworkParams(displaytext.(string), name, networkofferingid, zoneid) - m, err := parseCIDR(d) + // Get the network offering to check if it supports specifying IP ranges + no, _, err := cs.NetworkOffering.GetNetworkOfferingByID(networkofferingid) + if err != nil { + return err + } + + m, err := parseCIDR(d, no.Specifyipranges) if err != nil { return err } // Set the needed IP config - p.SetStartip(m["startip"]) p.SetGateway(m["gateway"]) - p.SetEndip(m["endip"]) p.SetNetmask(m["netmask"]) + // Only set the start IP if we have one + if startip, ok := m["startip"]; ok { + p.SetStartip(startip) + } + + // Only set the end IP if we have one + if endip, ok := m["endip"]; ok { + p.SetEndip(endip) + } + if vlan, ok := d.GetOk("vlan"); ok { p.SetVlan(strconv.Itoa(vlan.(int))) } @@ -313,7 +329,7 @@ func resourceCloudStackNetworkDelete(d *schema.ResourceData, meta interface{}) e return nil } -func parseCIDR(d *schema.ResourceData) (map[string]string, error) { +func parseCIDR(d *schema.ResourceData, specifyiprange bool) (map[string]string, error) { m := make(map[string]string, 4) cidr := d.Get("cidr").(string) @@ -335,13 +351,13 @@ func parseCIDR(d *schema.ResourceData) (map[string]string, error) { if startip, ok := d.GetOk("startip"); ok { m["startip"] = startip.(string) - } else { + } else if specifyiprange { m["startip"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+2) } if endip, ok := d.GetOk("endip"); ok { m["endip"] = endip.(string) - } else { + } else if specifyiprange { m["endip"] = fmt.Sprintf("%d.%d.%d.%d", sub[0]+(0xff-msk[0]), sub[1]+(0xff-msk[1]), sub[2]+(0xff-msk[2]), sub[3]+(0xff-msk[3]-1)) } diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl.go index 19302f2d2..cf19a511f 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_network_acl.go +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl.go @@ -29,6 +29,12 @@ func resourceCloudStackNetworkACL() *schema.Resource { ForceNew: true, }, + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "vpc_id": &schema.Schema{ Type: schema.TypeString, Required: true, @@ -70,7 +76,7 @@ func resourceCloudStackNetworkACLRead(d *schema.ResourceData, meta interface{}) // Get the network ACL list details f, count, err := cs.NetworkACL.GetNetworkACLListByID( d.Id(), - cloudstack.WithVPCID(d.Get("vpc_id").(string)), + cloudstack.WithProject(d.Get("project").(string)), ) if err != nil { if count == 0 { diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go index 8c4200873..0bc86d392 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go @@ -2,6 +2,7 @@ package cloudstack import ( "fmt" + "log" "strconv" "strings" "sync" @@ -88,6 +89,12 @@ func resourceCloudStackNetworkACLRule() *schema.Resource { }, }, + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "parallelism": &schema.Schema{ Type: schema.TypeInt, Optional: true, @@ -265,6 +272,22 @@ func createNetworkACLRule(d *schema.ResourceData, meta interface{}, rule map[str func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) + // First check if the ACL itself still exists + _, count, err := cs.NetworkACL.GetNetworkACLListByID( + d.Id(), + cloudstack.WithProject(d.Get("project").(string)), + ) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] Network ACL list %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + return err + } + // Get all the rules from the running environment p := cs.NetworkACL.NewListNetworkACLsParams() p.SetAclid(d.Id()) diff --git a/builtin/providers/cloudstack/resource_cloudstack_nic.go b/builtin/providers/cloudstack/resource_cloudstack_nic.go index 9fc93d75f..063497aee 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_nic.go +++ b/builtin/providers/cloudstack/resource_cloudstack_nic.go @@ -53,13 +53,13 @@ func resourceCloudStackNICCreate(d *schema.ResourceData, meta interface{}) error } // Create and attach the new NIC - r, err := cs.VirtualMachine.AddNicToVirtualMachine(p) + r, err := Retry(10, retryableAddNicFunc(cs, p)) if err != nil { return fmt.Errorf("Error creating the new NIC: %s", err) } found := false - for _, n := range r.Nic { + for _, n := range r.(*cloudstack.AddNicToVirtualMachineResponse).Nic { if n.Networkid == d.Get("network_id").(string) { d.SetId(n.Id) found = true @@ -133,3 +133,13 @@ func resourceCloudStackNICDelete(d *schema.ResourceData, meta interface{}) error return nil } + +func retryableAddNicFunc(cs *cloudstack.CloudStackClient, p *cloudstack.AddNicToVirtualMachineParams) func() (interface{}, error) { + return func() (interface{}, error) { + r, err := cs.VirtualMachine.AddNicToVirtualMachine(p) + if err != nil { + return nil, err + } + return r, nil + } +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_port_forward.go b/builtin/providers/cloudstack/resource_cloudstack_port_forward.go index 616adcf78..96064251d 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_port_forward.go +++ b/builtin/providers/cloudstack/resource_cloudstack_port_forward.go @@ -2,6 +2,7 @@ package cloudstack import ( "fmt" + "log" "sync" "time" @@ -173,6 +174,22 @@ func createPortForward(d *schema.ResourceData, meta interface{}, forward map[str func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) + // First check if the IP address is still associated + _, count, err := cs.Address.GetPublicIpAddressByID( + d.Id(), + cloudstack.WithProject(d.Get("project").(string)), + ) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] IP address with ID %s is no longer associated", d.Id()) + d.SetId("") + return nil + } + + return err + } + // Get all the forwards from the running environment p := cs.Firewall.NewListPortForwardingRulesParams() p.SetIpaddressid(d.Id()) diff --git a/builtin/providers/cloudstack/resource_cloudstack_template.go b/builtin/providers/cloudstack/resource_cloudstack_template.go index b3b3a4518..a7591d558 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_template.go +++ b/builtin/providers/cloudstack/resource_cloudstack_template.go @@ -54,6 +54,7 @@ func resourceCloudStackTemplate() *schema.Resource { "project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpc.go b/builtin/providers/cloudstack/resource_cloudstack_vpc.go index 16cf2ad11..44ce69212 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_vpc.go +++ b/builtin/providers/cloudstack/resource_cloudstack_vpc.go @@ -50,6 +50,7 @@ func resourceCloudStackVPC() *schema.Resource { "project": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, @@ -171,12 +172,10 @@ func resourceCloudStackVPCRead(d *schema.ResourceData, meta interface{}) error { return err } - if l.Count != 1 { - return fmt.Errorf("Unexpected number (%d) of source NAT IPs returned", l.Count) + if l.Count == 1 { + d.Set("source_nat_ip", l.PublicIpAddresses[0].Ipaddress) } - d.Set("source_nat_ip", l.PublicIpAddresses[0].Ipaddress) - return nil } diff --git a/website/source/docs/providers/cloudstack/r/disk.html.markdown b/website/source/docs/providers/cloudstack/r/disk.html.markdown index 8a59a6bcc..3b62e99cf 100644 --- a/website/source/docs/providers/cloudstack/r/disk.html.markdown +++ b/website/source/docs/providers/cloudstack/r/disk.html.markdown @@ -34,7 +34,7 @@ The following arguments are supported: * `attach` - (Optional) Determines whether or not to attach the disk volume to a virtual machine (defaults false). -* `device` - (Optional) The device to map the disk volume to within the guest OS. +* `device_id` - (Optional) The device ID to map the disk volume to within the guest OS. * `disk_offering` - (Required) The name or ID of the disk offering to use for this disk volume. @@ -58,4 +58,4 @@ The following arguments are supported: The following attributes are exported: * `id` - The ID of the disk volume. -* `device` - The device the disk volume is mapped to within the guest OS. +* `device_id` - The device ID the disk volume is mapped to within the guest OS. diff --git a/website/source/docs/providers/cloudstack/r/network_acl.html.markdown b/website/source/docs/providers/cloudstack/r/network_acl.html.markdown index e8a010cfc..3f91170b6 100644 --- a/website/source/docs/providers/cloudstack/r/network_acl.html.markdown +++ b/website/source/docs/providers/cloudstack/r/network_acl.html.markdown @@ -29,6 +29,9 @@ The following arguments are supported: * `description` - (Optional) The description of the ACL. Changing this forces a new resource to be created. +* `project` - (Optional) The name or ID of the project to deploy this + instance to. Changing this forces a new resource to be created. + * `vpc_id` - (Required) The ID of the VPC to create this ACL for. Changing this forces a new resource to be created. diff --git a/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown b/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown index 0a61bc769..23b3158d4 100644 --- a/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown +++ b/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown @@ -40,6 +40,9 @@ The following arguments are supported: * `rule` - (Optional) Can be specified multiple times. Each rule block supports fields documented below. If `managed = false` at least one rule is required! +* `project` - (Optional) The name or ID of the project to deploy this + instance to. Changing this forces a new resource to be created. + * `parallelism` (Optional) Specifies how much rules will be created or deleted concurrently. (defaults 2)