FWaaS Router Insertion (#220)

* Implement fwaasrouterinsertion FWaaS extension

This commit adds the FWaaS router insertion extension which enables
the firewall to be associated with one or more routers.

* FWaaS Router Insertion Acceptance Tests
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go b/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
index 976eb79..0b021d3 100644
--- a/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
@@ -6,7 +6,9 @@
 	"testing"
 
 	"github.com/gophercloud/gophercloud/acceptance/clients"
+	layer3 "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion"
 )
 
 func TestFirewallList(t *testing.T) {
@@ -36,6 +38,12 @@
 		t.Fatalf("Unable to create a network client: %v", err)
 	}
 
+	router, err := layer3.CreateExternalRouter(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create router: %v", err)
+	}
+	defer layer3.DeleteRouter(t, client, router.ID)
+
 	rule, err := CreateRule(t, client)
 	if err != nil {
 		t.Fatalf("Unable to create rule: %v", err)
@@ -52,7 +60,7 @@
 
 	PrintPolicy(t, policy)
 
-	firewall, err := CreateFirewall(t, client, policy.ID)
+	firewall, err := CreateFirewallOnRouter(t, client, policy.ID, router.ID)
 	if err != nil {
 		t.Fatalf("Unable to create firewall: %v", err)
 	}
@@ -60,11 +68,22 @@
 
 	PrintFirewall(t, firewall)
 
-	updateOpts := firewalls.UpdateOpts{
+	router2, err := layer3.CreateExternalRouter(t, client)
+	if err != nil {
+		t.Fatalf("Unable to create router: %v", err)
+	}
+	defer layer3.DeleteRouter(t, client, router2.ID)
+
+	firewallUpdateOpts := firewalls.UpdateOpts{
 		PolicyID:    policy.ID,
 		Description: "Some firewall description",
 	}
 
+	updateOpts := routerinsertion.UpdateOptsExt{
+		firewallUpdateOpts,
+		[]string{router2.ID},
+	}
+
 	_, err = firewalls.Update(client, firewall.ID, updateOpts).Extract()
 	if err != nil {
 		t.Fatalf("Unable to update firewall: %v", err)
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go b/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
index 6533a51..dd361f9 100644
--- a/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/fwaas.go
@@ -9,6 +9,7 @@
 	"github.com/gophercloud/gophercloud/acceptance/tools"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion"
 	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
 )
 
@@ -39,6 +40,39 @@
 	return firewall, nil
 }
 
+// CreateFirewallOnRouter will create a Firewall with a random name and a
+// specified policy ID attached to a specified Router. An error will be
+// returned if the firewall could not be created.
+func CreateFirewallOnRouter(t *testing.T, client *gophercloud.ServiceClient, policyID string, routerID string) (*firewalls.Firewall, error) {
+	firewallName := tools.RandomString("TESTACC-", 8)
+
+	t.Logf("Attempting to create firewall %s", firewallName)
+
+	firewallCreateOpts := firewalls.CreateOpts{
+		Name:     firewallName,
+		PolicyID: policyID,
+	}
+
+	createOpts := routerinsertion.CreateOptsExt{
+		firewallCreateOpts,
+		[]string{routerID},
+	}
+
+	firewall, err := firewalls.Create(client, createOpts).Extract()
+	if err != nil {
+		return firewall, err
+	}
+
+	t.Logf("Waiting for firewall to become active.")
+	if err := WaitForFirewallState(client, firewall.ID, "ACTIVE", 60); err != nil {
+		return firewall, err
+	}
+
+	t.Logf("Successfully created firewall %s", firewallName)
+
+	return firewall, nil
+}
+
 // CreatePolicy will create a Firewall Policy with a random name and given
 // rule. An error will be returned if the rule could not be created.
 func CreatePolicy(t *testing.T, client *gophercloud.ServiceClient, ruleID string) (*policies.Policy, error) {
diff --git a/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go
new file mode 100644
index 0000000..9b847e2
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/doc.go
@@ -0,0 +1,2 @@
+// Package routerinsertion implements the fwaasrouterinsertion FWaaS extension.
+package routerinsertion
diff --git a/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go
new file mode 100644
index 0000000..3a5942e
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/requests.go
@@ -0,0 +1,51 @@
+package routerinsertion
+
+import (
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+)
+
+// CreateOptsExt adds a RouterIDs option to the base CreateOpts.
+type CreateOptsExt struct {
+	firewalls.CreateOptsBuilder
+	RouterIDs []string `json:"router_ids"`
+}
+
+// ToFirewallCreateMap adds router_ids to the base firewall creation options.
+func (opts CreateOptsExt) ToFirewallCreateMap() (map[string]interface{}, error) {
+	base, err := opts.CreateOptsBuilder.ToFirewallCreateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(opts.RouterIDs) == 0 {
+		return base, nil
+	}
+
+	firewallMap := base["firewall"].(map[string]interface{})
+	firewallMap["router_ids"] = opts.RouterIDs
+
+	return base, nil
+}
+
+// UpdateOptsExt updates a RouterIDs option to the base UpdateOpts.
+type UpdateOptsExt struct {
+	firewalls.UpdateOptsBuilder
+	RouterIDs []string `json:"router_ids"`
+}
+
+// ToFirewallUpdateMap adds router_ids to the base firewall update options.
+func (opts UpdateOptsExt) ToFirewallUpdateMap() (map[string]interface{}, error) {
+	base, err := opts.UpdateOptsBuilder.ToFirewallUpdateMap()
+	if err != nil {
+		return nil, err
+	}
+
+	if len(opts.RouterIDs) == 0 {
+		return base, nil
+	}
+
+	firewallMap := base["firewall"].(map[string]interface{})
+	firewallMap["router_ids"] = opts.RouterIDs
+
+	return base, nil
+}
diff --git a/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go
new file mode 100644
index 0000000..36a6c1c
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/doc.go
@@ -0,0 +1,2 @@
+// networking_extensions_fwaas_extensions_routerinsertion_v2
+package testing
diff --git a/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go
new file mode 100644
index 0000000..a4890ee
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/routerinsertion/testing/requests_test.go
@@ -0,0 +1,126 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/gophercloud/gophercloud"
+	fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas/routerinsertion"
+	th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewalls", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "firewall":{
+        "name": "fw",
+        "description": "OpenStack firewall",
+        "admin_state_up": true,
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+        "router_ids": [
+          "8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"
+        ]
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "firewall":{
+        "status": "PENDING_CREATE",
+        "name": "fw",
+        "description": "OpenStack firewall",
+        "admin_state_up": true,
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c"
+    }
+}
+    `)
+	})
+
+	firewallCreateOpts := firewalls.CreateOpts{
+		TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+		Name:         "fw",
+		Description:  "OpenStack firewall",
+		AdminStateUp: gophercloud.Enabled,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+	createOpts := routerinsertion.CreateOptsExt{
+		firewallCreateOpts,
+		[]string{"8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"},
+	}
+
+	_, err := firewalls.Create(fake.ServiceClient(), createOpts).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewalls/ea5b5315-64f6-4ea3-8e58-981cc37c6576", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestHeader(t, r, "Content-Type", "application/json")
+		th.TestHeader(t, r, "Accept", "application/json")
+		th.TestJSONRequest(t, r, `
+{
+    "firewall":{
+        "name": "fw",
+        "description": "updated fw",
+        "admin_state_up":false,
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+        "router_ids": [
+          "8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"
+        ]
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall": {
+        "status": "ACTIVE",
+        "name": "fw",
+        "admin_state_up": false,
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+        "firewall_policy_id": "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+        "id": "ea5b5315-64f6-4ea3-8e58-981cc37c6576",
+        "description": "OpenStack firewall"
+    }
+}
+    `)
+	})
+
+	firewallUpdateOpts := firewalls.UpdateOpts{
+		Name:         "fw",
+		Description:  "updated fw",
+		AdminStateUp: gophercloud.Disabled,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+	updateOpts := routerinsertion.UpdateOptsExt{
+		firewallUpdateOpts,
+		[]string{"8a3a0d6a-34b5-4a92-b65d-6375a4c1e9e8"},
+	}
+
+	_, err := firewalls.Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", updateOpts).Extract()
+	th.AssertNoErr(t, err)
+}