From df660a26a1c5dd375d9c09c2f7ee7fa306ac64dc Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sun, 24 Jan 2016 20:39:35 +0000 Subject: [PATCH] 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. --- .../resource_openstack_compute_instance_v2.go | 350 +++++++++++++----- ...urce_openstack_compute_instance_v2_test.go | 108 +++++- .../r/compute_instance_v2.html.markdown | 28 +- 3 files changed, 376 insertions(+), 110 deletions(-) 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.