terraform/vendor/github.com/terraform-providers/terraform-provider-openstack/openstack/compute_instance_v2_network...

531 lines
16 KiB
Go

// This set of code handles all functions required to configure networking
// on an openstack_compute_instance_v2 resource.
//
// This is a complicated task because it's not possible to obtain all
// information in a single API call. In fact, it even traverses multiple
// OpenStack services.
//
// The end result, from the user's point of view, is a structured set of
// understandable network information within the instance resource.
package openstack
import (
"fmt"
"log"
"os"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
"github.com/hashicorp/terraform/helper/schema"
)
// InstanceNIC is a structured representation of a Gophercloud servers.Server
// virtual NIC.
type InstanceNIC struct {
FixedIPv4 string
FixedIPv6 string
MAC string
}
// InstanceAddresses is a collection of InstanceNICs, grouped by the
// network name. An instance/server could have multiple NICs on the same
// network.
type InstanceAddresses struct {
NetworkName string
InstanceNICs []InstanceNIC
}
// InstanceNetwork represents a collection of network information that a
// Terraform instance needs to satisfy all network information requirements.
type InstanceNetwork struct {
UUID string
Name string
Port string
FixedIP string
AccessNetwork bool
}
// getAllInstanceNetworks loops through the networks defined in the Terraform
// configuration and structures that information into something standard that
// can be consumed by both OpenStack and Terraform.
//
// This would be simple, except we have ensure both the network name and
// network ID have been determined. This isn't just for the convenience of a
// user specifying a human-readable network name, but the network information
// returned by an OpenStack instance only has the network name set! So if a
// user specified a network ID, there's no way to correlate it to the instance
// unless we know both the name and ID.
//
// Not only that, but we have to account for two OpenStack network services
// running: nova-network (legacy) and Neutron (current).
//
// In addition, if a port was specified, not all of the port information
// will be displayed, such as multiple fixed and floating IPs. This resource
// isn't currently configured for that type of flexibility. It's better to
// reference the actual port resource itself.
//
// So, let's begin the journey.
func getAllInstanceNetworks(d *schema.ResourceData, meta interface{}) ([]InstanceNetwork, error) {
var instanceNetworks []InstanceNetwork
networks := d.Get("network").([]interface{})
for _, v := range networks {
network := v.(map[string]interface{})
networkID := network["uuid"].(string)
networkName := network["name"].(string)
portID := network["port"].(string)
if networkID == "" && networkName == "" && portID == "" {
return nil, fmt.Errorf(
"At least one of network.uuid, network.name, or network.port must be set.")
}
// If a user specified both an ID and name, that makes things easy
// since both name and ID are already satisfied. No need to query
// further.
if networkID != "" && networkName != "" {
v := InstanceNetwork{
UUID: networkID,
Name: networkName,
Port: portID,
FixedIP: network["fixed_ip_v4"].(string),
AccessNetwork: network["access_network"].(bool),
}
instanceNetworks = append(instanceNetworks, v)
continue
}
// But if at least one of name or ID was missing, we have to query
// for that other piece.
//
// Priority is given to a port since a network ID or name usually isn't
// specified when using a port.
//
// Next priority is given to the network ID since it's guaranteed to be
// an exact match.
queryType := "name"
queryTerm := networkName
if networkID != "" {
queryType = "id"
queryTerm = networkID
}
if portID != "" {
queryType = "port"
queryTerm = portID
}
networkInfo, err := getInstanceNetworkInfo(d, meta, queryType, queryTerm)
if err != nil {
return nil, err
}
v := InstanceNetwork{
Port: portID,
FixedIP: network["fixed_ip_v4"].(string),
AccessNetwork: network["access_network"].(bool),
}
if networkInfo["uuid"] != nil {
v.UUID = networkInfo["uuid"].(string)
}
if networkInfo["name"] != nil {
v.Name = networkInfo["name"].(string)
}
instanceNetworks = append(instanceNetworks, v)
}
log.Printf("[DEBUG] getAllInstanceNetworks: %#v", instanceNetworks)
return instanceNetworks, nil
}
// getInstanceNetworkInfo will query for network information in order to make
// an accurate determination of a network's name and a network's ID.
//
// We will try to first query the Neutron network service and fall back to the
// legacy nova-network service if that fails.
//
// If OS_NOVA_NETWORK is set, query nova-network even if Neutron is available.
// This is to be able to explicitly test the nova-network API.
func getInstanceNetworkInfo(
d *schema.ResourceData, meta interface{}, queryType, queryTerm string) (map[string]interface{}, error) {
config := meta.(*Config)
if _, ok := os.LookupEnv("OS_NOVA_NETWORK"); !ok {
networkClient, err := config.networkingV2Client(GetRegion(d, config))
if err == nil {
networkInfo, err := getInstanceNetworkInfoNeutron(networkClient, queryType, queryTerm)
if err != nil {
return nil, fmt.Errorf("Error trying to get network information from the Network API: %s", err)
}
return networkInfo, nil
}
}
log.Printf("[DEBUG] Unable to obtain a network client")
computeClient, err := config.computeV2Client(GetRegion(d, config))
if err != nil {
return nil, fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
networkInfo, err := getInstanceNetworkInfoNovaNet(computeClient, queryType, queryTerm)
if err != nil {
return nil, fmt.Errorf("Error trying to get network information from the Nova API: %s", err)
}
return networkInfo, nil
}
// getInstanceNetworkInfoNovaNet will query the os-tenant-networks API for
// the network information.
func getInstanceNetworkInfoNovaNet(
client *gophercloud.ServiceClient, queryType, queryTerm string) (map[string]interface{}, error) {
// If somehow a port ended up here, we should just error out.
if queryType == "port" {
return nil, fmt.Errorf(
"Unable to query a port (%s) using the Nova API", queryTerm)
}
// test to see if the tenantnetworks api is available
log.Printf("[DEBUG] testing for os-tenant-networks")
tenantNetworksAvailable := true
allPages, err := tenantnetworks.List(client).AllPages()
if err != nil {
switch err.(type) {
case gophercloud.ErrDefault404:
tenantNetworksAvailable = false
case gophercloud.ErrDefault403:
tenantNetworksAvailable = false
case gophercloud.ErrUnexpectedResponseCode:
tenantNetworksAvailable = false
default:
return nil, fmt.Errorf(
"An error occurred while querying the Nova API for network information: %s", err)
}
}
if !tenantNetworksAvailable {
// we can't query the APIs for more information, but in some cases
// the information provided is enough
log.Printf("[DEBUG] os-tenant-networks disabled.")
return map[string]interface{}{queryType: queryTerm}, nil
}
networkList, err := tenantnetworks.ExtractNetworks(allPages)
if err != nil {
return nil, fmt.Errorf(
"An error occurred while querying the Nova API for network information: %s", err)
}
var networkFound bool
var network tenantnetworks.Network
for _, v := range networkList {
if queryType == "id" && v.ID == queryTerm {
networkFound = true
network = v
break
}
if queryType == "name" && v.Name == queryTerm {
networkFound = true
network = v
break
}
}
if networkFound {
v := map[string]interface{}{
"uuid": network.ID,
"name": network.Name,
}
log.Printf("[DEBUG] getInstanceNetworkInfoNovaNet: %#v", v)
return v, nil
}
return nil, fmt.Errorf("Could not find any matching network for %s %s", queryType, queryTerm)
}
// getInstanceNetworkInfoNeutron will query the neutron API for the network
// information.
func getInstanceNetworkInfoNeutron(
client *gophercloud.ServiceClient, queryType, queryTerm string) (map[string]interface{}, error) {
// If a port was specified, use it to look up the network ID
// and then query the network as if a network ID was originally used.
if queryType == "port" {
listOpts := ports.ListOpts{
ID: queryTerm,
}
allPages, err := ports.List(client, listOpts).AllPages()
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
allPorts, err := ports.ExtractPorts(allPages)
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
var port ports.Port
switch len(allPorts) {
case 0:
return nil, fmt.Errorf("Could not find any matching port for %s %s", queryType, queryTerm)
case 1:
port = allPorts[0]
default:
return nil, fmt.Errorf("More than one port found for %s %s", queryType, queryTerm)
}
queryType = "id"
queryTerm = port.NetworkID
}
listOpts := networks.ListOpts{
Status: "ACTIVE",
}
switch queryType {
case "name":
listOpts.Name = queryTerm
default:
listOpts.ID = queryTerm
}
allPages, err := networks.List(client, listOpts).AllPages()
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
allNetworks, err := networks.ExtractNetworks(allPages)
if err != nil {
return nil, fmt.Errorf("Unable to retrieve networks from the Network API: %s", err)
}
var network networks.Network
switch len(allNetworks) {
case 0:
return nil, fmt.Errorf("Could not find any matching network for %s %s", queryType, queryTerm)
case 1:
network = allNetworks[0]
default:
return nil, fmt.Errorf("More than one network found for %s %s", queryType, queryTerm)
}
v := map[string]interface{}{
"uuid": network.ID,
"name": network.Name,
}
log.Printf("[DEBUG] getInstanceNetworkInfoNeutron: %#v", v)
return v, nil
}
// getInstanceAddresses parses a Gophercloud server.Server's Address field into
// a structured InstanceAddresses struct.
func getInstanceAddresses(addresses map[string]interface{}) []InstanceAddresses {
var allInstanceAddresses []InstanceAddresses
for networkName, v := range addresses {
instanceAddresses := InstanceAddresses{
NetworkName: networkName,
}
for _, v := range v.([]interface{}) {
instanceNIC := InstanceNIC{}
var exists bool
v := v.(map[string]interface{})
if v, ok := v["OS-EXT-IPS-MAC:mac_addr"].(string); ok {
instanceNIC.MAC = v
}
if v["OS-EXT-IPS:type"] == "fixed" || v["OS-EXT-IPS:type"] == nil {
switch v["version"].(float64) {
case 6:
instanceNIC.FixedIPv6 = fmt.Sprintf("[%s]", v["addr"].(string))
default:
instanceNIC.FixedIPv4 = v["addr"].(string)
}
}
// To associate IPv4 and IPv6 on the right NIC,
// key on the mac address and fill in the blanks.
for i, v := range instanceAddresses.InstanceNICs {
if v.MAC == instanceNIC.MAC {
exists = true
if instanceNIC.FixedIPv6 != "" {
instanceAddresses.InstanceNICs[i].FixedIPv6 = instanceNIC.FixedIPv6
}
if instanceNIC.FixedIPv4 != "" {
instanceAddresses.InstanceNICs[i].FixedIPv4 = instanceNIC.FixedIPv4
}
}
}
if !exists {
instanceAddresses.InstanceNICs = append(instanceAddresses.InstanceNICs, instanceNIC)
}
}
allInstanceAddresses = append(allInstanceAddresses, instanceAddresses)
}
log.Printf("[DEBUG] Addresses: %#v", addresses)
log.Printf("[DEBUG] allInstanceAddresses: %#v", allInstanceAddresses)
return allInstanceAddresses
}
// expandInstanceNetworks takes network information found in []InstanceNetwork
// and builds a Gophercloud []servers.Network for use in creating an Instance.
func expandInstanceNetworks(allInstanceNetworks []InstanceNetwork) []servers.Network {
var networks []servers.Network
for _, v := range allInstanceNetworks {
n := servers.Network{
UUID: v.UUID,
Port: v.Port,
FixedIP: v.FixedIP,
}
networks = append(networks, n)
}
return networks
}
// flattenInstanceNetworks collects instance network information from different
// sources and aggregates it all together into a map array.
func flattenInstanceNetworks(
d *schema.ResourceData, meta interface{}) ([]map[string]interface{}, error) {
config := meta.(*Config)
computeClient, err := config.computeV2Client(GetRegion(d, config))
if err != nil {
return nil, fmt.Errorf("Error creating OpenStack compute client: %s", err)
}
server, err := servers.Get(computeClient, d.Id()).Extract()
if err != nil {
return nil, CheckDeleted(d, err, "server")
}
allInstanceAddresses := getInstanceAddresses(server.Addresses)
allInstanceNetworks, err := getAllInstanceNetworks(d, meta)
if err != nil {
return nil, err
}
networks := []map[string]interface{}{}
// If there were no instance networks returned, this means that there
// was not a network specified in the Terraform configuration. When this
// happens, the instance will be launched on a "default" network, if one
// is available. If there isn't, the instance will fail to launch, so
// this is a safe assumption at this point.
if len(allInstanceNetworks) == 0 {
for _, instanceAddresses := range allInstanceAddresses {
for _, instanceNIC := range instanceAddresses.InstanceNICs {
v := map[string]interface{}{
"name": instanceAddresses.NetworkName,
"fixed_ip_v4": instanceNIC.FixedIPv4,
"fixed_ip_v6": instanceNIC.FixedIPv6,
"mac": instanceNIC.MAC,
}
// Use the same method as getAllInstanceNetworks to get the network uuid
networkInfo, err := getInstanceNetworkInfo(d, meta, "name", instanceAddresses.NetworkName)
if err != nil {
log.Printf("[WARN] Error getting default network uuid: %s", err)
} else {
if v["uuid"] != nil {
v["uuid"] = networkInfo["uuid"].(string)
} else {
log.Printf("[WARN] Could not get default network uuid")
}
}
networks = append(networks, v)
}
}
log.Printf("[DEBUG] flattenInstanceNetworks: %#v", networks)
return networks, nil
}
// Loop through all networks and addresses, merge relevant address details.
for _, instanceNetwork := range allInstanceNetworks {
for _, instanceAddresses := range allInstanceAddresses {
// Skip if instanceAddresses has no NICs
if len(instanceAddresses.InstanceNICs) == 0 {
continue
}
if instanceNetwork.Name == instanceAddresses.NetworkName {
// Only use one NIC since it's possible the user defined another NIC
// on this same network in another Terraform network block.
instanceNIC := instanceAddresses.InstanceNICs[0]
copy(instanceAddresses.InstanceNICs, instanceAddresses.InstanceNICs[1:])
v := map[string]interface{}{
"name": instanceAddresses.NetworkName,
"fixed_ip_v4": instanceNIC.FixedIPv4,
"fixed_ip_v6": instanceNIC.FixedIPv6,
"mac": instanceNIC.MAC,
"uuid": instanceNetwork.UUID,
"port": instanceNetwork.Port,
"access_network": instanceNetwork.AccessNetwork,
}
networks = append(networks, v)
}
}
}
log.Printf("[DEBUG] flattenInstanceNetworks: %#v", networks)
return networks, nil
}
// getInstanceAccessAddresses determines the best IP address to communicate
// with the instance. It does this by looping through all networks and looking
// for a valid IP address. Priority is given to a network that was flagged as
// an access_network.
func getInstanceAccessAddresses(
d *schema.ResourceData, networks []map[string]interface{}) (string, string) {
var hostv4, hostv6 string
// Loop through all networks
// If the network has a valid fixed v4 or fixed v6 address
// and hostv4 or hostv6 is not set, set hostv4/hostv6.
// If the network is an "access_network" overwrite hostv4/hostv6.
for _, n := range networks {
var accessNetwork bool
if an, ok := n["access_network"].(bool); ok && an {
accessNetwork = true
}
if fixedIPv4, ok := n["fixed_ip_v4"].(string); ok && fixedIPv4 != "" {
if hostv4 == "" || accessNetwork {
hostv4 = fixedIPv4
}
}
if fixedIPv6, ok := n["fixed_ip_v6"].(string); ok && fixedIPv6 != "" {
if hostv6 == "" || accessNetwork {
hostv6 = fixedIPv6
}
}
}
log.Printf("[DEBUG] OpenStack Instance Network Access Addresses: %s, %s", hostv4, hostv6)
return hostv4, hostv6
}