Adds Fixed IP support to os-floating-ips

This commit enables the ability to specify a fixed IP when associating a
floating IP to an instance. If a fixed IP is not specified, Nova will
attempt to associate the floating IP to the first detected fixed IP, as it
did prior to this patch.
diff --git a/openstack/compute/v2/extensions/floatingip/fixtures.go b/openstack/compute/v2/extensions/floatingip/fixtures.go
index 26f3299..e47fa4c 100644
--- a/openstack/compute/v2/extensions/floatingip/fixtures.go
+++ b/openstack/compute/v2/extensions/floatingip/fixtures.go
@@ -155,6 +155,25 @@
 	})
 }
 
+// HandleFixedAssociateSucessfully configures the test server to respond to a Post request
+// to associate an allocated floating IP with a specific fixed IP address
+func HandleAssociateFixedSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/4d8c3732-a248-40ed-bebc-539a6ffd25c0/action", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `
+{
+	"addFloatingIp": {
+		"address": "10.10.10.2",
+		"fixed_address": "166.78.185.201"
+	}
+}
+`)
+
+		w.WriteHeader(http.StatusAccepted)
+	})
+}
+
 // HandleDisassociateSuccessfully configures the test server to respond to a Post request
 // to disassociate an allocated floating IP
 func HandleDisassociateSuccessfully(t *testing.T) {
diff --git a/openstack/compute/v2/extensions/floatingip/requests.go b/openstack/compute/v2/extensions/floatingip/requests.go
index 8abb72d..941dc3b 100644
--- a/openstack/compute/v2/extensions/floatingip/requests.go
+++ b/openstack/compute/v2/extensions/floatingip/requests.go
@@ -26,6 +26,18 @@
 	Pool string
 }
 
+// AssociateOpts specifies the required information to associate or disassociate a floating IP to an instance
+type AssociateOpts struct {
+	// ServerID is the UUID of the server
+	ServerID string
+
+	// FixedIP is an optional fixed IP address of the server
+	FixedIP string
+
+	// FloatingIP is the floating IP to associate with an instance
+	FloatingIP string
+}
+
 // ToFloatingIPCreateMap constructs a request body from CreateOpts.
 func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
 	if opts.Pool == "" {
@@ -35,6 +47,26 @@
 	return map[string]interface{}{"pool": opts.Pool}, nil
 }
 
+// ToAssociateMap constructs a request body from AssociateOpts.
+func (opts AssociateOpts) ToAssociateMap() (map[string]interface{}, error) {
+	if opts.ServerID == "" {
+		return nil, errors.New("Required field missing for floating IP association: ServerID")
+	}
+
+	if opts.FloatingIP == "" {
+		return nil, errors.New("Required field missing for floating IP association: FloatingIP")
+	}
+
+	associateInfo := map[string]interface{}{
+		"serverId":   opts.ServerID,
+		"floatingIp": opts.FloatingIP,
+		"fixedIp":    opts.FixedIP,
+	}
+
+	return associateInfo, nil
+
+}
+
 // Create requests the creation of a new floating IP
 func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
 	var res CreateResult
@@ -68,6 +100,7 @@
 // association / disassociation
 
 // Associate pairs an allocated floating IP with an instance
+// Deprecated. Use AssociateFloatingIP.
 func Associate(client *gophercloud.ServiceClient, serverId, fip string) AssociateResult {
 	var res AssociateResult
 
@@ -79,7 +112,33 @@
 	return res
 }
 
+// AssociateFloatingIP pairs an allocated floating IP with an instance.
+func AssociateFloatingIP(client *gophercloud.ServiceClient, opts AssociateOpts) AssociateResult {
+	var res AssociateResult
+
+	associateInfo, err := opts.ToAssociateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	addFloatingIp := make(map[string]interface{})
+	addFloatingIp["address"] = associateInfo["floatingIp"].(string)
+
+	// fixedIp is not required
+	if associateInfo["fixedIp"] != "" {
+		addFloatingIp["fixed_address"] = associateInfo["fixedIp"].(string)
+	}
+
+	serverId := associateInfo["serverId"].(string)
+
+	reqBody := map[string]interface{}{"addFloatingIp": addFloatingIp}
+	_, res.Err = client.Post(associateURL(client, serverId), reqBody, nil, nil)
+	return res
+}
+
 // Disassociate decouples an allocated floating IP from an instance
+// Deprecated. Use DisassociateFloatingIP.
 func Disassociate(client *gophercloud.ServiceClient, serverId, fip string) DisassociateResult {
 	var res DisassociateResult
 
@@ -90,3 +149,23 @@
 	_, res.Err = client.Post(disassociateURL(client, serverId), reqBody, nil, nil)
 	return res
 }
+
+// DisassociateFloatingIP decouples an allocated floating IP from an instance
+func DisassociateFloatingIP(client *gophercloud.ServiceClient, opts AssociateOpts) DisassociateResult {
+	var res DisassociateResult
+
+	associateInfo, err := opts.ToAssociateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	removeFloatingIp := make(map[string]interface{})
+	removeFloatingIp["address"] = associateInfo["floatingIp"].(string)
+	reqBody := map[string]interface{}{"removeFloatingIp": removeFloatingIp}
+
+	serverId := associateInfo["serverId"].(string)
+
+	_, res.Err = client.Post(disassociateURL(client, serverId), reqBody, nil, nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/floatingip/requests_test.go b/openstack/compute/v2/extensions/floatingip/requests_test.go
index ed2460e..b92496b 100644
--- a/openstack/compute/v2/extensions/floatingip/requests_test.go
+++ b/openstack/compute/v2/extensions/floatingip/requests_test.go
@@ -57,7 +57,7 @@
 	th.AssertNoErr(t, err)
 }
 
-func TestAssociate(t *testing.T) {
+func TestAssociateDeprecated(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 	HandleAssociateSuccessfully(t)
@@ -68,7 +68,36 @@
 	th.AssertNoErr(t, err)
 }
 
-func TestDisassociate(t *testing.T) {
+func TestAssociate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAssociateSuccessfully(t)
+
+	associateOpts := AssociateOpts{
+		ServerID:   "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+		FloatingIP: "10.10.10.2",
+	}
+
+	err := AssociateFloatingIP(client.ServiceClient(), associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAssociateFixed(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAssociateFixedSuccessfully(t)
+
+	associateOpts := AssociateOpts{
+		ServerID:   "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+		FloatingIP: "10.10.10.2",
+		FixedIP:    "166.78.185.201",
+	}
+
+	err := AssociateFloatingIP(client.ServiceClient(), associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDisassociateDeprecated(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
 	HandleDisassociateSuccessfully(t)
@@ -78,3 +107,17 @@
 	err := Disassociate(client.ServiceClient(), serverId, fip).ExtractErr()
 	th.AssertNoErr(t, err)
 }
+
+func TestDisassociateFloatingIP(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDisassociateSuccessfully(t)
+
+	associateOpts := AssociateOpts{
+		ServerID:   "4d8c3732-a248-40ed-bebc-539a6ffd25c0",
+		FloatingIP: "10.10.10.2",
+	}
+
+	err := DisassociateFloatingIP(client.ServiceClient(), associateOpts).ExtractErr()
+	th.AssertNoErr(t, err)
+}