provider/openstack: Per-network Floating IPs

This commit adds the ability to associate a Floating IP to a specific
network. Previously, there only existed a top-level floating IP
attribute which was automatically associated with either the first
defined network or the default network (when no network block was
used).

Now floating IPs can be associated with networks beyond the first
defined network as well as each network being able to have their own
floating IP.

Specifying the floating IP by using the top-level floating_ip
attribute and the per-network floating IP attribute is not possible.

Additionally, an `access_network` attribute has been added in order
to easily specify which network should be used for provisioning.
This commit is contained in:
Joe Topjian 2016-01-24 20:39:35 +00:00
parent 1e99ff6c44
commit df660a26a1
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.