Merge pull request #4812 from jtopjian/openstack-per-network-floating-ip

provider/openstack: Per-network Floating IPs
This commit is contained in:
Joe Topjian 2016-01-31 15:24:59 -07:00
commit 1ccd0491ff
3 changed files with 376 additions and 110 deletions

View File

@ -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)
@ -903,6 +938,7 @@ func resourceInstanceNetworks(computeClient *gophercloud.ServiceClient, d *schem
"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{}) {

View File

@ -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

View File

@ -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.