diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go index 44f959088fa4..18d688b86c8b 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -136,10 +136,20 @@ func resourceComputeInstanceV2() *schema.Resource { Optional: true, Computed: true, }, + "floating_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, "mac": &schema.Schema{ Type: schema.TypeString, Computed: true, }, + "access_network": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, }, }, }, @@ -320,11 +330,6 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e return err } - networkDetails, err := resourceInstanceNetworks(computeClient, d) - if err != nil { - return err - } - // determine if volume/block_device configuration is correct // this includes ensuring volume_ids are set // and if only one block_device was specified. @@ -332,6 +337,18 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e return err } + // check if floating IP configuration is correct + if err := checkInstanceFloatingIPs(d); err != nil { + return err + } + + // Build a list of networks with the information given upon creation. + // Error out if an invalid network configuration was used. + networkDetails, err := getInstanceNetworks(computeClient, d) + if err != nil { + return err + } + networks := make([]servers.Network, len(networkDetails)) for i, net := range networkDetails { networks[i] = servers.Network{ @@ -424,11 +441,15 @@ func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) e "Error waiting for instance (%s) to become ready: %s", server.ID, err) } - floatingIP := d.Get("floating_ip").(string) - if floatingIP != "" { - if err := floatingip.Associate(computeClient, server.ID, floatingIP).ExtractErr(); err != nil { - return fmt.Errorf("Error associating floating IP: %s", err) - } + + // Now that the instance has been created, we need to do an early read on the + // networks in order to associate floating IPs + _, err = getInstanceNetworksAndAddresses(computeClient, d) + + // If floating IPs were specified, associate them after the instance has launched. + err = associateFloatingIPsToInstance(computeClient, d) + if err != nil { + return err } // if volumes were specified, attach them after the instance has launched. @@ -462,99 +483,35 @@ func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) err d.Set("name", server.Name) - // begin reading the network configuration - d.Set("access_ip_v4", server.AccessIPv4) - d.Set("access_ip_v6", server.AccessIPv6) - hostv4 := server.AccessIPv4 - hostv6 := server.AccessIPv6 - - networkDetails, err := resourceInstanceNetworks(computeClient, d) - addresses := resourceInstanceAddresses(server.Addresses) + // Get the instance network and address information + networks, err := getInstanceNetworksAndAddresses(computeClient, d) if err != nil { return err } - // if there are no networkDetails, make networks at least a length of 1 - networkLength := 1 - if len(networkDetails) > 0 { - networkLength = len(networkDetails) - } - networks := make([]map[string]interface{}, networkLength) - - // Loop through all networks and addresses, - // merge relevant address details. - if len(networkDetails) == 0 { - for netName, n := range addresses { - if floatingIP, ok := n["floating_ip"]; ok { - hostv4 = floatingIP.(string) - } else { - if hostv4 == "" && n["fixed_ip_v4"] != nil { - hostv4 = n["fixed_ip_v4"].(string) - } - } - - if hostv6 == "" && n["fixed_ip_v6"] != nil { - hostv6 = n["fixed_ip_v6"].(string) - } - - networks[0] = map[string]interface{}{ - "name": netName, - "fixed_ip_v4": n["fixed_ip_v4"], - "fixed_ip_v6": n["fixed_ip_v6"], - "mac": n["mac"], - } - } - } else { - for i, net := range networkDetails { - n := addresses[net["name"].(string)] - - if floatingIP, ok := n["floating_ip"]; ok { - hostv4 = floatingIP.(string) - } else { - if hostv4 == "" && n["fixed_ip_v4"] != nil { - hostv4 = n["fixed_ip_v4"].(string) - } - } - - if hostv6 == "" && n["fixed_ip_v6"] != nil { - hostv6 = n["fixed_ip_v6"].(string) - } - - networks[i] = map[string]interface{}{ - "uuid": networkDetails[i]["uuid"], - "name": networkDetails[i]["name"], - "port": networkDetails[i]["port"], - "fixed_ip_v4": n["fixed_ip_v4"], - "fixed_ip_v6": n["fixed_ip_v6"], - "mac": n["mac"], - } - } - } - - log.Printf("[DEBUG] new networks: %+v", networks) + // Determine the best IPv4 and IPv6 addresses to access the instance with + hostv4, hostv6 := getInstanceAccessAddresses(d, networks) d.Set("network", networks) d.Set("access_ip_v4", hostv4) d.Set("access_ip_v6", hostv6) - log.Printf("hostv4: %s", hostv4) - log.Printf("hostv6: %s", hostv6) - // prefer the v6 address if no v4 address exists. - preferredv := "" + // Determine the best IP address to use for SSH connectivity. + // Prefer IPv4 over IPv6. + preferredSSHAddress := "" if hostv4 != "" { - preferredv = hostv4 + preferredSSHAddress = hostv4 } else if hostv6 != "" { - preferredv = hostv6 + preferredSSHAddress = hostv6 } - if preferredv != "" { + if preferredSSHAddress != "" { // Initialize the connection info d.SetConnInfo(map[string]string{ "type": "ssh", - "host": preferredv, + "host": preferredSSHAddress, }) } - // end network configuration d.Set("metadata", server.Metadata) @@ -600,12 +557,6 @@ func resourceComputeInstanceV2Update(d *schema.ResourceData, meta interface{}) e if d.HasChange("name") { updateOpts.Name = d.Get("name").(string) } - if d.HasChange("access_ip_v4") { - updateOpts.AccessIPv4 = d.Get("access_ip_v4").(string) - } - if d.HasChange("access_ip_v6") { - updateOpts.AccessIPv4 = d.Get("access_ip_v6").(string) - } if updateOpts != (servers.UpdateOpts{}) { _, err := servers.Update(computeClient, d.Id(), updateOpts).Extract() @@ -679,20 +630,48 @@ func resourceComputeInstanceV2Update(d *schema.ResourceData, meta interface{}) e log.Printf("[DEBUG] Old Floating IP: %v", oldFIP) log.Printf("[DEBUG] New Floating IP: %v", newFIP) if oldFIP.(string) != "" { - log.Printf("[DEBUG] Attemping to disassociate %s from %s", oldFIP, d.Id()) - if err := floatingip.Disassociate(computeClient, d.Id(), oldFIP.(string)).ExtractErr(); err != nil { + log.Printf("[DEBUG] Attempting to disassociate %s from %s", oldFIP, d.Id()) + if err := disassociateFloatingIPFromInstance(computeClient, oldFIP.(string), d.Id(), ""); err != nil { return fmt.Errorf("Error disassociating Floating IP during update: %s", err) } } if newFIP.(string) != "" { - log.Printf("[DEBUG] Attemping to associate %s to %s", newFIP, d.Id()) - if err := floatingip.Associate(computeClient, d.Id(), newFIP.(string)).ExtractErr(); err != nil { + log.Printf("[DEBUG] Attempting to associate %s to %s", newFIP, d.Id()) + if err := associateFloatingIPToInstance(computeClient, newFIP.(string), d.Id(), ""); err != nil { return fmt.Errorf("Error associating Floating IP during update: %s", err) } } } + if d.HasChange("network") { + oldNetworks, newNetworks := d.GetChange("network") + oldNetworkList := oldNetworks.([]interface{}) + newNetworkList := newNetworks.([]interface{}) + for i, oldNet := range oldNetworkList { + oldNetRaw := oldNet.(map[string]interface{}) + oldFIP := oldNetRaw["floating_ip"].(string) + oldFixedIP := oldNetRaw["fixed_ip_v4"].(string) + + newNetRaw := newNetworkList[i].(map[string]interface{}) + newFIP := newNetRaw["floating_ip"].(string) + newFixedIP := newNetRaw["fixed_ip_v4"].(string) + + // Only changes to the floating IP are supported + if oldFIP != newFIP { + log.Printf("[DEBUG] Attempting to disassociate %s from %s", oldFIP, d.Id()) + if err := disassociateFloatingIPFromInstance(computeClient, oldFIP, d.Id(), oldFixedIP); err != nil { + return fmt.Errorf("Error disassociating Floating IP during update: %s", err) + } + + log.Printf("[DEBUG] Attempting to associate %s to %s", newFIP, d.Id()) + if err := associateFloatingIPToInstance(computeClient, newFIP, d.Id(), newFixedIP); err != nil { + return fmt.Errorf("Error associating Floating IP during update: %s", err) + } + } + } + } + if d.HasChange("volume") { // ensure the volume configuration is correct if err := checkVolumeConfig(d); err != nil { @@ -845,7 +824,62 @@ func resourceInstanceSecGroupsV2(d *schema.ResourceData) []string { return secgroups } -func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) ([]map[string]interface{}, error) { +// getInstanceNetworks collects instance network information from different sources +// and aggregates it all together. +func getInstanceNetworksAndAddresses(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) ([]map[string]interface{}, error) { + server, err := servers.Get(computeClient, d.Id()).Extract() + if err != nil { + return nil, CheckDeleted(d, err, "server") + } + + networkDetails, err := getInstanceNetworks(computeClient, d) + addresses := getInstanceAddresses(server.Addresses) + if err != nil { + return nil, err + } + + // if there are no networkDetails, make networks at least a length of 1 + networkLength := 1 + if len(networkDetails) > 0 { + networkLength = len(networkDetails) + } + networks := make([]map[string]interface{}, networkLength) + + // Loop through all networks and addresses, + // merge relevant address details. + if len(networkDetails) == 0 { + for netName, n := range addresses { + networks[0] = map[string]interface{}{ + "name": netName, + "fixed_ip_v4": n["fixed_ip_v4"], + "fixed_ip_v6": n["fixed_ip_v6"], + "floating_ip": n["floating_ip"], + "mac": n["mac"], + } + } + } else { + for i, net := range networkDetails { + n := addresses[net["name"].(string)] + + networks[i] = map[string]interface{}{ + "uuid": networkDetails[i]["uuid"], + "name": networkDetails[i]["name"], + "port": networkDetails[i]["port"], + "fixed_ip_v4": n["fixed_ip_v4"], + "fixed_ip_v6": n["fixed_ip_v6"], + "floating_ip": n["floating_ip"], + "mac": n["mac"], + "access_network": networkDetails[i]["access_network"], + } + } + } + + log.Printf("[DEBUG] networks: %+v", networks) + + return networks, nil +} + +func getInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) ([]map[string]interface{}, error) { rawNetworks := d.Get("network").([]interface{}) newNetworks := make([]map[string]interface{}, 0, len(rawNetworks)) var tenantnet tenantnetworks.Network @@ -860,6 +894,7 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem } rawMap := raw.(map[string]interface{}) + allPages, err := tenantnetworks.List(computeClient).AllPages() if err != nil { errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) @@ -899,10 +934,11 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem } newNetworks = append(newNetworks, map[string]interface{}{ - "uuid": networkID, - "name": networkName, - "port": rawMap["port"].(string), - "fixed_ip_v4": rawMap["fixed_ip_v4"].(string), + "uuid": networkID, + "name": networkName, + "port": rawMap["port"].(string), + "fixed_ip_v4": rawMap["fixed_ip_v4"].(string), + "access_network": rawMap["access_network"].(bool), }) } @@ -910,8 +946,7 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem return newNetworks, nil } -func resourceInstanceAddresses(addresses map[string]interface{}) map[string]map[string]interface{} { - +func getInstanceAddresses(addresses map[string]interface{}) map[string]map[string]interface{} { addrs := make(map[string]map[string]interface{}) for n, networkAddresses := range addresses { addrs[n] = make(map[string]interface{}) @@ -937,6 +972,117 @@ func resourceInstanceAddresses(addresses map[string]interface{}) map[string]map[ return addrs } +func getInstanceAccessAddresses(d *schema.ResourceData, networks []map[string]interface{}) (string, string) { + var hostv4, hostv6 string + + // Start with a global floating IP + floatingIP := d.Get("floating_ip").(string) + if floatingIP != "" { + hostv4 = floatingIP + } + + // Loop through all networks and check for the following: + // * If the network is set as an access network. + // * If the network has a floating IP. + // * If the network has a v4/v6 fixed IP. + for _, n := range networks { + if n["floating_ip"] != nil { + hostv4 = n["floating_ip"].(string) + } else { + if hostv4 == "" && n["fixed_ip_v4"] != nil { + hostv4 = n["fixed_ip_v4"].(string) + } + } + + if hostv6 == "" && n["fixed_ip_v6"] != nil { + hostv6 = n["fixed_ip_v6"].(string) + } + + if n["access_network"].(bool) { + break + } + } + + log.Printf("[DEBUG] OpenStack Instance Network Access Addresses: %s, %s", hostv4, hostv6) + + return hostv4, hostv6 +} + +func checkInstanceFloatingIPs(d *schema.ResourceData) error { + rawNetworks := d.Get("network").([]interface{}) + floatingIP := d.Get("floating_ip").(string) + + for _, raw := range rawNetworks { + if raw == nil { + continue + } + + rawMap := raw.(map[string]interface{}) + + // Error if a floating IP was specified both globally and in the network block. + if floatingIP != "" && rawMap["floating_ip"] != "" { + return fmt.Errorf("Cannot specify a floating IP both globally and in a network block.") + } + } + return nil +} + +func associateFloatingIPsToInstance(computeClient *gophercloud.ServiceClient, d *schema.ResourceData) error { + floatingIP := d.Get("floating_ip").(string) + rawNetworks := d.Get("network").([]interface{}) + instanceID := d.Id() + + if floatingIP != "" { + if err := associateFloatingIPToInstance(computeClient, floatingIP, instanceID, ""); err != nil { + return err + } + } else { + for _, raw := range rawNetworks { + if raw == nil { + continue + } + + rawMap := raw.(map[string]interface{}) + if rawMap["floating_ip"].(string) != "" { + floatingIP := rawMap["floating_ip"].(string) + fixedIP := rawMap["fixed_ip_v4"].(string) + if err := associateFloatingIPToInstance(computeClient, floatingIP, instanceID, fixedIP); err != nil { + return err + } + } + } + } + return nil +} + +func associateFloatingIPToInstance(computeClient *gophercloud.ServiceClient, floatingIP string, instanceID string, fixedIP string) error { + associateOpts := floatingip.AssociateOpts{ + ServerID: instanceID, + FloatingIP: floatingIP, + FixedIP: fixedIP, + } + + if err := floatingip.AssociateInstance(computeClient, associateOpts).ExtractErr(); err != nil { + return fmt.Errorf("Error associating floating IP: %s", err) + } + + return nil +} + +func disassociateFloatingIPFromInstance(computeClient *gophercloud.ServiceClient, floatingIP string, instanceID string, fixedIP string) error { + associateOpts := floatingip.AssociateOpts{ + ServerID: instanceID, + FloatingIP: floatingIP, + FixedIP: fixedIP, + } + + if err := floatingip.DisassociateInstance(computeClient, associateOpts).ExtractErr(); err != nil { + return fmt.Errorf("Error disassociating floating IP: %s", err) + } + + return nil +} + func resourceInstanceMetadataV2(d *schema.ResourceData) map[string]string { m := make(map[string]string) for key, val := range d.Get("metadata").(map[string]interface{}) { diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go index 574f3b199386..77f970f853bf 100644 --- a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go @@ -178,10 +178,10 @@ func TestAccComputeV2Instance_volumeDetachPostCreation(t *testing.T) { }) } -func TestAccComputeV2Instance_floatingIPAttach(t *testing.T) { +func TestAccComputeV2Instance_floatingIPAttachGlobally(t *testing.T) { var instance servers.Server var fip floatingip.FloatingIP - var testAccComputeV2Instance_floatingIPAttach = fmt.Sprintf(` + var testAccComputeV2Instance_floatingIPAttachGlobally = fmt.Sprintf(` resource "openstack_compute_floatingip_v2" "myip" { } @@ -202,7 +202,7 @@ func TestAccComputeV2Instance_floatingIPAttach(t *testing.T) { CheckDestroy: testAccCheckComputeV2InstanceDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccComputeV2Instance_floatingIPAttach, + Config: testAccComputeV2Instance_floatingIPAttachGlobally, Check: resource.ComposeTestCheckFunc( testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip", &fip), testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), @@ -213,6 +213,108 @@ func TestAccComputeV2Instance_floatingIPAttach(t *testing.T) { }) } +func TestAccComputeV2Instance_floatingIPAttachToNetwork(t *testing.T) { + var instance servers.Server + var fip floatingip.FloatingIP + var testAccComputeV2Instance_floatingIPAttachToNetwork = fmt.Sprintf(` + resource "openstack_compute_floatingip_v2" "myip" { + } + + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + security_groups = ["default"] + + network { + uuid = "%s" + floating_ip = "${openstack_compute_floatingip_v2.myip.address}" + access_network = true + } + }`, + os.Getenv("OS_NETWORK_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_floatingIPAttachToNetwork, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip", &fip), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckComputeV2InstanceFloatingIPAttach(&instance, &fip), + ), + }, + }, + }) +} + +func TestAccComputeV2Instance_floatingIPAttachAndChange(t *testing.T) { + var instance servers.Server + var fip floatingip.FloatingIP + var testAccComputeV2Instance_floatingIPAttachToNetwork_1 = fmt.Sprintf(` + resource "openstack_compute_floatingip_v2" "myip_1" { + } + + resource "openstack_compute_floatingip_v2" "myip_2" { + } + + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + security_groups = ["default"] + + network { + uuid = "%s" + floating_ip = "${openstack_compute_floatingip_v2.myip_1.address}" + access_network = true + } + }`, + os.Getenv("OS_NETWORK_ID")) + + var testAccComputeV2Instance_floatingIPAttachToNetwork_2 = fmt.Sprintf(` + resource "openstack_compute_floatingip_v2" "myip_1" { + } + + resource "openstack_compute_floatingip_v2" "myip_2" { + } + + resource "openstack_compute_instance_v2" "foo" { + name = "terraform-test" + security_groups = ["default"] + + network { + uuid = "%s" + floating_ip = "${openstack_compute_floatingip_v2.myip_2.address}" + access_network = true + } + }`, + os.Getenv("OS_NETWORK_ID")) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_floatingIPAttachToNetwork_1, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip_1", &fip), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckComputeV2InstanceFloatingIPAttach(&instance, &fip), + ), + }, + resource.TestStep{ + Config: testAccComputeV2Instance_floatingIPAttachToNetwork_2, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.myip_2", &fip), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckComputeV2InstanceFloatingIPAttach(&instance, &fip), + ), + }, + }, + }) +} + func TestAccComputeV2Instance_multi_secgroups(t *testing.T) { var instance servers.Server var secgroup secgroups.SecurityGroup diff --git a/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown index 8dced11dcc20..ef91edfaad49 100644 --- a/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown +++ b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown @@ -50,7 +50,8 @@ The following arguments are supported: desired flavor for the server. Changing this resizes the existing server. * `floating_ip` - (Optional) A *Compute* Floating IP that will be associated - with the Instance. The Floating IP must be provisioned already. + with the Instance. The Floating IP must be provisioned already. See *Notes* + for more information about Floating IPs. * `user_data` - (Optional) The user data to provide when launching the instance. Changing this creates a new server. @@ -106,6 +107,13 @@ The `network` block supports: * `fixed_ip_v4` - (Optional) Specifies a fixed IPv4 address to be used on this network. +* `floating_ip` - (Optional) Specifies a floating IP address to be associated + with this network. Cannot be combined with a top-level floating IP. See + *Notes* for more information about Floating IPs. + +* `access_network` - (Optional) Specifies if this network should be used for + provisioning access. Accepts true or false. Defaults to false. + The `block_device` block supports: * `uuid` - (Required) The UUID of the image, volume, or snapshot. @@ -173,11 +181,21 @@ The following attributes are exported: network. * `network/fixed_ip_v6` - The Fixed IPv6 address of the Instance on that network. +* `network/floating_ip` - The Floating IP address of the Instance on that + network. * `network/mac` - The MAC address of the NIC on that network. ## Notes -If you configure the instance to have multiple networks, be aware that only -the first network can be associated with a Floating IP. So the first network -in the instance resource _must_ be the network that you have configured to -communicate with your floating IP / public network via a Neutron Router. +Floating IPs can be associated in one of two ways: + +* You can specify a Floating IP address by using the top-level `floating_ip` +attribute. This floating IP will be associated with either the network defined +in the first `network` block or the default network if no `network` blocks are +defined. + +* You can specify a Floating IP address by using the `floating_ip` attribute +defined in the `network` block. Each `network` block can have its own floating +IP address. + +Only one of the above methods can be used.