Merge pull request #372 from jrperritt/get-object-cdn-url

Get object CDN URL; Closes #371
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go b/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
new file mode 100644
index 0000000..80246b6
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/firewall_test.go
@@ -0,0 +1,116 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+	"time"
+
+	"github.com/rackspace/gophercloud"
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func firewallSetup(t *testing.T) string {
+	base.Setup(t)
+	return createPolicy(t, &policies.CreateOpts{})
+}
+
+func firewallTeardown(t *testing.T, policyID string) {
+	defer base.Teardown()
+	deletePolicy(t, policyID)
+}
+
+func TestFirewall(t *testing.T) {
+	policyID := firewallSetup(t)
+	defer firewallTeardown(t, policyID)
+
+	firewallID := createFirewall(t, &firewalls.CreateOpts{
+		Name:        "gophercloud test",
+		Description: "acceptance test",
+		PolicyID:    policyID,
+	})
+
+	waitForFirewallToBeActive(t, firewallID)
+
+	listFirewalls(t)
+
+	updateFirewall(t, firewallID, &firewalls.UpdateOpts{
+		Description: "acceptance test updated",
+	})
+
+	waitForFirewallToBeActive(t, firewallID)
+
+	deleteFirewall(t, firewallID)
+
+	waitForFirewallToBeDeleted(t, firewallID)
+}
+
+func createFirewall(t *testing.T, opts *firewalls.CreateOpts) string {
+	f, err := firewalls.Create(base.Client, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created firewall: %#v", opts)
+	return f.ID
+}
+
+func listFirewalls(t *testing.T) {
+	err := firewalls.List(base.Client, firewalls.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		firewallList, err := firewalls.ExtractFirewalls(page)
+		if err != nil {
+			t.Errorf("Failed to extract firewalls: %v", err)
+			return false, err
+		}
+
+		for _, r := range firewallList {
+			t.Logf("Listing firewalls: ID [%s]", r.ID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func updateFirewall(t *testing.T, firewallID string, opts *firewalls.UpdateOpts) {
+	f, err := firewalls.Update(base.Client, firewallID, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated firewall ID [%s]", f.ID)
+}
+
+func getFirewall(t *testing.T, firewallID string) *firewalls.Firewall {
+	f, err := firewalls.Get(base.Client, firewallID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting firewall ID [%s]", f.ID)
+	return f
+}
+
+func deleteFirewall(t *testing.T, firewallID string) {
+	res := firewalls.Delete(base.Client, firewallID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted firewall %s", firewallID)
+}
+
+func waitForFirewallToBeActive(t *testing.T, firewallID string) {
+	for i := 0; i < 10; i++ {
+		fw := getFirewall(t, firewallID)
+		if fw.Status == "ACTIVE" {
+			break
+		}
+		time.Sleep(time.Second)
+	}
+}
+
+func waitForFirewallToBeDeleted(t *testing.T, firewallID string) {
+	for i := 0; i < 10; i++ {
+		err := firewalls.Get(base.Client, firewallID).Err
+		if err != nil {
+			httpStatus := err.(*gophercloud.UnexpectedResponseCodeError)
+			if httpStatus.Actual == 404 {
+				return
+			}
+		}
+		time.Sleep(time.Second)
+	}
+}
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go b/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go
new file mode 100644
index 0000000..206bf33
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/pkg.go
@@ -0,0 +1 @@
+package fwaas
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go b/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go
new file mode 100644
index 0000000..fdca22e
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/policy_test.go
@@ -0,0 +1,107 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func firewallPolicySetup(t *testing.T) string {
+	base.Setup(t)
+	return createRule(t, &rules.CreateOpts{
+		Protocol: "tcp",
+		Action:   "allow",
+	})
+}
+
+func firewallPolicyTeardown(t *testing.T, ruleID string) {
+	defer base.Teardown()
+	deleteRule(t, ruleID)
+}
+
+func TestFirewallPolicy(t *testing.T) {
+	ruleID := firewallPolicySetup(t)
+	defer firewallPolicyTeardown(t, ruleID)
+
+	policyID := createPolicy(t, &policies.CreateOpts{
+		Name:        "gophercloud test",
+		Description: "acceptance test",
+		Rules: []string{
+			ruleID,
+		},
+	})
+
+	listPolicies(t)
+
+	updatePolicy(t, policyID, &policies.UpdateOpts{
+		Description: "acceptance test updated",
+	})
+
+	getPolicy(t, policyID)
+
+	removeRuleFromPolicy(t, policyID, ruleID)
+
+	addRuleToPolicy(t, policyID, ruleID)
+
+	deletePolicy(t, policyID)
+}
+
+func createPolicy(t *testing.T, opts *policies.CreateOpts) string {
+	p, err := policies.Create(base.Client, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created policy: %#v", opts)
+	return p.ID
+}
+
+func listPolicies(t *testing.T) {
+	err := policies.List(base.Client, policies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		policyList, err := policies.ExtractPolicies(page)
+		if err != nil {
+			t.Errorf("Failed to extract policies: %v", err)
+			return false, err
+		}
+
+		for _, p := range policyList {
+			t.Logf("Listing policies: ID [%s]", p.ID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func updatePolicy(t *testing.T, policyID string, opts *policies.UpdateOpts) {
+	p, err := policies.Update(base.Client, policyID, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated policy ID [%s]", p.ID)
+}
+
+func removeRuleFromPolicy(t *testing.T, policyID string, ruleID string) {
+	err := policies.RemoveRule(base.Client, policyID, ruleID)
+	th.AssertNoErr(t, err)
+	t.Logf("Removed rule [%s] from policy ID [%s]", ruleID, policyID)
+}
+
+func addRuleToPolicy(t *testing.T, policyID string, ruleID string) {
+	err := policies.InsertRule(base.Client, policyID, ruleID, "", "")
+	th.AssertNoErr(t, err)
+	t.Logf("Inserted rule [%s] into policy ID [%s]", ruleID, policyID)
+}
+
+func getPolicy(t *testing.T, policyID string) {
+	p, err := policies.Get(base.Client, policyID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting policy ID [%s]", p.ID)
+}
+
+func deletePolicy(t *testing.T, policyID string) {
+	res := policies.Delete(base.Client, policyID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted policy %s", policyID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go b/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go
new file mode 100644
index 0000000..144aa09
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/fwaas/rule_test.go
@@ -0,0 +1,84 @@
+// +build acceptance networking fwaas
+
+package fwaas
+
+import (
+	"testing"
+
+	base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+	"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestFirewallRules(t *testing.T) {
+	base.Setup(t)
+	defer base.Teardown()
+
+	ruleID := createRule(t, &rules.CreateOpts{
+		Name:                 "gophercloud_test",
+		Description:          "acceptance test",
+		Protocol:             "tcp",
+		Action:               "allow",
+		DestinationIPAddress: "192.168.0.0/24",
+		DestinationPort:      "22",
+	})
+
+	listRules(t)
+
+	destinationIPAddress := "192.168.1.0/24"
+	destinationPort := ""
+	sourcePort := "1234"
+
+	updateRule(t, ruleID, &rules.UpdateOpts{
+		DestinationIPAddress: &destinationIPAddress,
+		DestinationPort:      &destinationPort,
+		SourcePort:           &sourcePort,
+	})
+
+	getRule(t, ruleID)
+
+	deleteRule(t, ruleID)
+}
+
+func createRule(t *testing.T, opts *rules.CreateOpts) string {
+	r, err := rules.Create(base.Client, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Created rule: %#v", opts)
+	return r.ID
+}
+
+func listRules(t *testing.T) {
+	err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		ruleList, err := rules.ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract rules: %v", err)
+			return false, err
+		}
+
+		for _, r := range ruleList {
+			t.Logf("Listing rules: ID [%s]", r.ID)
+		}
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+}
+
+func updateRule(t *testing.T, ruleID string, opts *rules.UpdateOpts) {
+	r, err := rules.Update(base.Client, ruleID, *opts).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Updated rule ID [%s]", r.ID)
+}
+
+func getRule(t *testing.T, ruleID string) {
+	r, err := rules.Get(base.Client, ruleID).Extract()
+	th.AssertNoErr(t, err)
+	t.Logf("Getting rule ID [%s]", r.ID)
+}
+
+func deleteRule(t *testing.T, ruleID string) {
+	res := rules.Delete(base.Client, ruleID)
+	th.AssertNoErr(t, res.Err)
+	t.Logf("Deleted rule %s", ruleID)
+}
diff --git a/auth_options.go b/auth_options.go
index 19ce5d4..9819e45 100644
--- a/auth_options.go
+++ b/auth_options.go
@@ -42,7 +42,5 @@
 	// re-authenticate automatically if/when your token expires.  If you set it to
 	// false, it will not cache these settings, but re-authentication will not be
 	// possible.  This setting defaults to false.
-	//
-	// This setting is speculative and is currently not respected!
 	AllowReauth bool
 }
diff --git a/openstack/client.go b/openstack/client.go
index 63e07b8..6818d9d 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -107,6 +107,11 @@
 		return err
 	}
 
+	if options.AllowReauth {
+		client.ReauthFunc = func() error {
+			return AuthenticateV2(client, options)
+		}
+	}
 	client.TokenID = token.ID
 	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
 		return V2EndpointURL(catalog, opts)
@@ -133,6 +138,11 @@
 	}
 	client.TokenID = token.ID
 
+	if options.AllowReauth {
+		client.ReauthFunc = func() error {
+			return AuthenticateV3(client, options)
+		}
+	}
 	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
 		return V3EndpointURL(v3Client, opts)
 	}
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
index 4292894..8f0a7a0 100644
--- a/openstack/compute/v2/extensions/secgroups/requests.go
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -216,7 +216,7 @@
 		rule["cidr"] = opts.CIDR
 	}
 	if opts.FromGroupID != "" {
-		rule["from_group_id"] = opts.FromGroupID
+		rule["group_id"] = opts.FromGroupID
 	}
 
 	return map[string]interface{}{"security_group_rule": rule}, nil
diff --git a/openstack/networking/v2/extensions/fwaas/doc.go b/openstack/networking/v2/extensions/fwaas/doc.go
new file mode 100644
index 0000000..3ec450a
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/doc.go
@@ -0,0 +1,3 @@
+// Package fwaas provides information and interaction with the Firewall
+// as a Service extension for the OpenStack Networking service.
+package fwaas
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/errors.go b/openstack/networking/v2/extensions/fwaas/firewalls/errors.go
new file mode 100644
index 0000000..dd92bb2
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/errors.go
@@ -0,0 +1,11 @@
+package firewalls
+
+import "fmt"
+
+func err(str string) error {
+	return fmt.Errorf("%s", str)
+}
+
+var (
+	errPolicyRequired = err("A policy ID is required")
+)
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/requests.go b/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
new file mode 100644
index 0000000..69f3dca
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
@@ -0,0 +1,227 @@
+package firewalls
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// AdminState gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Up` and `Down` enums.
+type AdminState *bool
+
+// Shared gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Yes` and `No` enums.
+type Shared *bool
+
+// Convenience vars for AdminStateUp and Shared values.
+var (
+	iTrue             = true
+	iFalse            = false
+	Up     AdminState = &iTrue
+	Down   AdminState = &iFalse
+	Yes    Shared     = &iTrue
+	No     Shared     = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToFirewallListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the firewall attributes you want to see returned. SortKey allows you to sort
+// by a particular firewall attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	TenantID     string `q:"tenant_id"`
+	Name         string `q:"name"`
+	Description  string `q:"description"`
+	AdminStateUp bool   `q:"admin_state_up"`
+	Shared       bool   `q:"shared"`
+	PolicyID     string `q:"firewall_policy_id"`
+	ID           string `q:"id"`
+	Limit        int    `q:"limit"`
+	Marker       string `q:"marker"`
+	SortKey      string `q:"sort_key"`
+	SortDir      string `q:"sort_dir"`
+}
+
+// ToFirewallListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToFirewallListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// firewalls. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those firewalls that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+
+	if opts != nil {
+		query, err := opts.ToFirewallListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return FirewallPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToFirewallCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new firewall.
+type CreateOpts struct {
+	// Only required if the caller has an admin role and wants to create a firewall
+	// for another tenant.
+	TenantID     string
+	Name         string
+	Description  string
+	AdminStateUp *bool
+	Shared       *bool
+	PolicyID     string
+}
+
+// ToFirewallCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToFirewallCreateMap() (map[string]interface{}, error) {
+	if opts.PolicyID == "" {
+		return nil, errPolicyRequired
+	}
+
+	f := make(map[string]interface{})
+
+	if opts.TenantID != "" {
+		f["tenant_id"] = opts.TenantID
+	}
+	if opts.Name != "" {
+		f["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		f["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		f["shared"] = *opts.Shared
+	}
+	if opts.AdminStateUp != nil {
+		f["admin_state_up"] = *opts.AdminStateUp
+	}
+	if opts.PolicyID != "" {
+		f["firewall_policy_id"] = opts.PolicyID
+	}
+
+	return map[string]interface{}{"firewall": f}, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToFirewallCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Request("POST", rootURL(c), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{201},
+	})
+	return res
+}
+
+// Get retrieves a particular firewall based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Request("GET", resourceURL(c, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+	ToFirewallUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a firewall.
+type UpdateOpts struct {
+	// Name of the firewall.
+	Name         string
+	Description  string
+	AdminStateUp *bool
+	Shared       *bool
+	PolicyID     string
+}
+
+// ToFirewallUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToFirewallUpdateMap() (map[string]interface{}, error) {
+	f := make(map[string]interface{})
+
+	if opts.Name != "" {
+		f["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		f["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		f["shared"] = *opts.Shared
+	}
+	if opts.AdminStateUp != nil {
+		f["admin_state_up"] = *opts.AdminStateUp
+	}
+	if opts.PolicyID != "" {
+		f["firewall_policy_id"] = opts.PolicyID
+	}
+
+	return map[string]interface{}{"firewall": f}, nil
+}
+
+// Update allows firewalls to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToFirewallUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = c.Request("PUT", resourceURL(c, id), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular firewall based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Request("DELETE", resourceURL(c, id), gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go b/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go
new file mode 100644
index 0000000..f24e283
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/requests_test.go
@@ -0,0 +1,246 @@
+package firewalls
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/fw/firewalls", rootURL(fake.ServiceClient()))
+}
+
+func TestList(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, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+   "firewalls":[
+        {
+           "status": "ACTIVE",
+           "name": "fw1",
+           "admin_state_up": false,
+           "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+           "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a",
+           "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61",
+           "description": "OpenStack firewall 1"
+        },
+        {
+           "status": "PENDING_UPDATE",
+           "name": "fw2",
+           "admin_state_up": true,
+           "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+           "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e299",
+           "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f99",
+           "description": "OpenStack firewall 2"
+        }
+   ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractFirewalls(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []Firewall{
+			Firewall{
+				Status:       "ACTIVE",
+				Name:         "fw1",
+				AdminStateUp: false,
+				TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+				PolicyID:     "34be8c83-4d42-4dca-a74e-b77fffb8e28a",
+				ID:           "fb5b5315-64f6-4ea3-8e58-981cc37c6f61",
+				Description:  "OpenStack firewall 1",
+			},
+			Firewall{
+				Status:       "PENDING_UPDATE",
+				Name:         "fw2",
+				AdminStateUp: true,
+				TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+				PolicyID:     "34be8c83-4d42-4dca-a74e-b77fffb8e299",
+				ID:           "fb5b5315-64f6-4ea3-8e58-981cc37c6f99",
+				Description:  "OpenStack firewall 2",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+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"
+    }
+}
+      `)
+
+		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"
+    }
+}
+    `)
+	})
+
+	options := CreateOpts{
+		TenantID:     "b4eedccc6fb74fa8a7ad6b08382b852b",
+		Name:         "fw",
+		Description:  "OpenStack firewall",
+		AdminStateUp: Up,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+	_, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewalls/fb5b5315-64f6-4ea3-8e58-981cc37c6f61", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall": {
+        "status": "ACTIVE",
+        "name": "fw",
+        "admin_state_up": true,
+        "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+        "firewall_policy_id": "34be8c83-4d42-4dca-a74e-b77fffb8e28a",
+        "id": "fb5b5315-64f6-4ea3-8e58-981cc37c6f61",
+        "description": "OpenStack firewall"
+    }
+}
+        `)
+	})
+
+	fw, err := Get(fake.ServiceClient(), "fb5b5315-64f6-4ea3-8e58-981cc37c6f61").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "ACTIVE", fw.Status)
+	th.AssertEquals(t, "fw", fw.Name)
+	th.AssertEquals(t, "OpenStack firewall", fw.Description)
+	th.AssertEquals(t, true, fw.AdminStateUp)
+	th.AssertEquals(t, "34be8c83-4d42-4dca-a74e-b77fffb8e28a", fw.PolicyID)
+	th.AssertEquals(t, "fb5b5315-64f6-4ea3-8e58-981cc37c6f61", fw.ID)
+	th.AssertEquals(t, "b4eedccc6fb74fa8a7ad6b08382b852b", fw.TenantID)
+}
+
+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"
+    }
+}
+      `)
+
+		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",
+    }
+}
+    `)
+	})
+
+	options := UpdateOpts{
+		Name:         "fw",
+		Description:  "updated fw",
+		AdminStateUp: Down,
+		PolicyID:     "19ab8c87-4a32-4e6a-a74e-b77fffb89a0c",
+	}
+
+	_, err := Update(fake.ServiceClient(), "ea5b5315-64f6-4ea3-8e58-981cc37c6576", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewalls/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/results.go b/openstack/networking/v2/extensions/fwaas/firewalls/results.go
new file mode 100644
index 0000000..a8c76ee
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/results.go
@@ -0,0 +1,101 @@
+package firewalls
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type Firewall struct {
+	ID           string `json:"id" mapstructure:"id"`
+	Name         string `json:"name" mapstructure:"name"`
+	Description  string `json:"description" mapstructure:"description"`
+	AdminStateUp bool   `json:"admin_state_up" mapstructure:"admin_state_up"`
+	Status       string `json:"status" mapstructure:"status"`
+	PolicyID     string `json:"firewall_policy_id" mapstructure:"firewall_policy_id"`
+	TenantID     string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a firewall.
+func (r commonResult) Extract() (*Firewall, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Firewall *Firewall `json:"firewall"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Firewall, err
+}
+
+// FirewallPage is the page returned by a pager when traversing over a
+// collection of firewalls.
+type FirewallPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of firewalls has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (p FirewallPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"firewalls_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a FirewallPage struct is empty.
+func (p FirewallPage) IsEmpty() (bool, error) {
+	is, err := ExtractFirewalls(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractFirewalls accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractFirewalls(page pagination.Page) ([]Firewall, error) {
+	var resp struct {
+		Firewalls []Firewall `mapstructure:"firewalls" json:"firewalls"`
+	}
+
+	err := mapstructure.Decode(page.(FirewallPage).Body, &resp)
+
+	return resp.Firewalls, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/urls.go b/openstack/networking/v2/extensions/fwaas/firewalls/urls.go
new file mode 100644
index 0000000..4dde530
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/urls.go
@@ -0,0 +1,16 @@
+package firewalls
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "fw"
+	resourcePath = "firewalls"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/requests.go b/openstack/networking/v2/extensions/fwaas/policies/requests.go
new file mode 100644
index 0000000..95081df
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/requests.go
@@ -0,0 +1,258 @@
+package policies
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Binary gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Yes` and `No` enums
+type Binary *bool
+
+// Convenience vars for Audited and Shared values.
+var (
+	iTrue         = true
+	iFalse        = false
+	Yes    Binary = &iTrue
+	No     Binary = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToPolicyListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the firewall policy attributes you want to see returned. SortKey allows you
+// to sort by a particular firewall policy attribute. SortDir sets the direction,
+// and is either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	TenantID    string `q:"tenant_id"`
+	Name        string `q:"name"`
+	Description string `q:"description"`
+	Shared      bool   `q:"shared"`
+	Audited     bool   `q:"audited"`
+	ID          string `q:"id"`
+	Limit       int    `q:"limit"`
+	Marker      string `q:"marker"`
+	SortKey     string `q:"sort_key"`
+	SortDir     string `q:"sort_dir"`
+}
+
+// ToPolicyListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPolicyListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// firewall policies. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those firewall policies that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+
+	if opts != nil {
+		query, err := opts.ToPolicyListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return PolicyPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToPolicyCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new firewall policy.
+type CreateOpts struct {
+	// Only required if the caller has an admin role and wants to create a firewall policy
+	// for another tenant.
+	TenantID    string
+	Name        string
+	Description string
+	Shared      *bool
+	Audited     *bool
+	Rules       []string
+}
+
+// ToPolicyCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPolicyCreateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.TenantID != "" {
+		p["tenant_id"] = opts.TenantID
+	}
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		p["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		p["shared"] = *opts.Shared
+	}
+	if opts.Audited != nil {
+		p["audited"] = *opts.Audited
+	}
+	if opts.Rules != nil {
+		p["firewall_rules"] = opts.Rules
+	}
+
+	return map[string]interface{}{"firewall_policy": p}, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall policy
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToPolicyCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Request("POST", rootURL(c), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{201},
+	})
+	return res
+}
+
+// Get retrieves a particular firewall policy based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Request("GET", resourceURL(c, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+	ToPolicyUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a firewall policy.
+type UpdateOpts struct {
+	// Name of the firewall policy.
+	Name        string
+	Description string
+	Shared      *bool
+	Audited     *bool
+	Rules       []string
+}
+
+// ToPolicyUpdateMap casts a CreateOpts struct to a map.
+func (opts UpdateOpts) ToPolicyUpdateMap() (map[string]interface{}, error) {
+	p := make(map[string]interface{})
+
+	if opts.Name != "" {
+		p["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		p["description"] = opts.Description
+	}
+	if opts.Shared != nil {
+		p["shared"] = *opts.Shared
+	}
+	if opts.Audited != nil {
+		p["audited"] = *opts.Audited
+	}
+	if opts.Rules != nil {
+		p["firewall_rules"] = opts.Rules
+	}
+
+	return map[string]interface{}{"firewall_policy": p}, nil
+}
+
+// Update allows firewall policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToPolicyUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = c.Request("PUT", resourceURL(c, id), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res
+}
+
+// Delete will permanently delete a particular firewall policy based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Request("DELETE", resourceURL(c, id), gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	return res
+}
+
+func InsertRule(c *gophercloud.ServiceClient, policyID, ruleID, beforeID, afterID string) error {
+	type request struct {
+		RuleId string `json:"firewall_rule_id"`
+		Before string `json:"insert_before,omitempty"`
+		After  string `json:"insert_after,omitempty"`
+	}
+
+	reqBody := request{
+		RuleId: ruleID,
+		Before: beforeID,
+		After:  afterID,
+	}
+
+	// Send request to API
+	var res commonResult
+	_, res.Err = c.Request("PUT", insertURL(c, policyID), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res.Err
+}
+
+func RemoveRule(c *gophercloud.ServiceClient, policyID, ruleID string) error {
+	type request struct {
+		RuleId string `json:"firewall_rule_id"`
+	}
+
+	reqBody := request{
+		RuleId: ruleID,
+	}
+
+	// Send request to API
+	var res commonResult
+	_, res.Err = c.Request("PUT", removeURL(c, policyID), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res.Err
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/requests_test.go b/openstack/networking/v2/extensions/fwaas/policies/requests_test.go
new file mode 100644
index 0000000..b9d7865
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/requests_test.go
@@ -0,0 +1,279 @@
+package policies
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/fw/firewall_policies", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall_policies": [
+        {
+            "name": "policy1",
+            "firewall_rules": [
+                "75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+                "c9e77ca0-1bc8-497d-904d-948107873dc6"
+            ],
+            "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+            "audited": true,
+			"shared": false,
+            "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+            "description": "Firewall policy 1"
+        },
+        {
+            "name": "policy2",
+            "firewall_rules": [
+                "03d2a6ad-633f-431a-8463-4370d06a22c8"
+            ],
+            "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+            "audited": false,
+			"shared": true,
+            "id": "c854fab5-bdaf-4a86-9359-78de93e5df01",
+            "description": "Firewall policy 2"
+        }
+    ]
+}
+        `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPolicies(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []Policy{
+			Policy{
+				Name: "policy1",
+				Rules: []string{
+					"75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+					"c9e77ca0-1bc8-497d-904d-948107873dc6",
+				},
+				TenantID:    "9145d91459d248b1b02fdaca97c6a75d",
+				Audited:     true,
+				Shared:      false,
+				ID:          "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+				Description: "Firewall policy 1",
+			},
+			Policy{
+				Name: "policy2",
+				Rules: []string{
+					"03d2a6ad-633f-431a-8463-4370d06a22c8",
+				},
+				TenantID:    "9145d91459d248b1b02fdaca97c6a75d",
+				Audited:     false,
+				Shared:      true,
+				ID:          "c854fab5-bdaf-4a86-9359-78de93e5df01",
+				Description: "Firewall policy 2",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies", 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_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "98a58c87-76be-ae7c-a74e-b77fffb88d95",
+            "11a58c87-76be-ae7c-a74e-b77fffb88a32"
+        ],
+        "description": "Firewall policy",
+		"tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+		"audited": true,
+		"shared": false
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+    "firewall_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "98a58c87-76be-ae7c-a74e-b77fffb88d95",
+            "11a58c87-76be-ae7c-a74e-b77fffb88a32"
+        ],
+        "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+        "audited": false,
+        "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+        "description": "Firewall policy"
+    }
+}
+        `)
+	})
+
+	options := CreateOpts{
+		TenantID:    "9145d91459d248b1b02fdaca97c6a75d",
+		Name:        "policy",
+		Description: "Firewall policy",
+		Shared:      No,
+		Audited:     Yes,
+		Rules: []string{
+			"98a58c87-76be-ae7c-a74e-b77fffb88d95",
+			"11a58c87-76be-ae7c-a74e-b77fffb88a32",
+		},
+	}
+
+	_, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies/bcab5315-64f6-4ea3-8e58-981cc37c6f61", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall_policy":{
+        "name": "www",
+        "firewall_rules": [
+            "75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+            "c9e77ca0-1bc8-497d-904d-948107873dc6",
+            "03d2a6ad-633f-431a-8463-4370d06a22c8"
+        ],
+        "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+        "audited": false,
+        "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+        "description": "Firewall policy web"
+    }
+}
+        `)
+	})
+
+	policy, err := Get(fake.ServiceClient(), "bcab5315-64f6-4ea3-8e58-981cc37c6f61").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "www", policy.Name)
+	th.AssertEquals(t, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", policy.ID)
+	th.AssertEquals(t, "Firewall policy web", policy.Description)
+	th.AssertEquals(t, 3, len(policy.Rules))
+	th.AssertEquals(t, "75452b36-268e-4e75-aaf4-f0e7ed50bc97", policy.Rules[0])
+	th.AssertEquals(t, "c9e77ca0-1bc8-497d-904d-948107873dc6", policy.Rules[1])
+	th.AssertEquals(t, "03d2a6ad-633f-431a-8463-4370d06a22c8", policy.Rules[2])
+	th.AssertEquals(t, "9145d91459d248b1b02fdaca97c6a75d", policy.TenantID)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies/f2b08c1e-aa81-4668-8ae1-1401bcb0576c", 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_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "98a58c87-76be-ae7c-a74e-b77fffb88d95",
+            "11a58c87-76be-ae7c-a74e-b77fffb88a32"
+        ],
+        "description": "Firewall policy"
+    }
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall_policy":{
+        "name": "policy",
+        "firewall_rules": [
+            "75452b36-268e-4e75-aaf4-f0e7ed50bc97",
+            "c9e77ca0-1bc8-497d-904d-948107873dc6",
+            "03d2a6ad-633f-431a-8463-4370d06a22c8"
+        ],
+        "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+        "audited": false,
+        "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+        "description": "Firewall policy"
+    }
+}
+    `)
+	})
+
+	options := UpdateOpts{
+		Name:        "policy",
+		Description: "Firewall policy",
+		Rules: []string{
+			"98a58c87-76be-ae7c-a74e-b77fffb88d95",
+			"11a58c87-76be-ae7c-a74e-b77fffb88a32",
+		},
+	}
+
+	_, err := Update(fake.ServiceClient(), "f2b08c1e-aa81-4668-8ae1-1401bcb0576c", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_policies/4ec89077-d057-4a2b-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "4ec89077-d057-4a2b-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/results.go b/openstack/networking/v2/extensions/fwaas/policies/results.go
new file mode 100644
index 0000000..a9a0c35
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/results.go
@@ -0,0 +1,101 @@
+package policies
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type Policy struct {
+	ID          string   `json:"id" mapstructure:"id"`
+	Name        string   `json:"name" mapstructure:"name"`
+	Description string   `json:"description" mapstructure:"description"`
+	TenantID    string   `json:"tenant_id" mapstructure:"tenant_id"`
+	Audited     bool     `json:"audited" mapstructure:"audited"`
+	Shared      bool     `json:"shared" mapstructure:"shared"`
+	Rules       []string `json:"firewall_rules,omitempty" mapstructure:"firewall_rules"`
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a firewall policy.
+func (r commonResult) Extract() (*Policy, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Policy *Policy `json:"firewall_policy" mapstructure:"firewall_policy"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Policy, err
+}
+
+// PolicyPage is the page returned by a pager when traversing over a
+// collection of firewall policies.
+type PolicyPage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of firewall policies has
+// reached the end of a page and the pager seeks to traverse over a new one.
+// In order to do this, it needs to construct the next page's URL.
+func (p PolicyPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"firewall_policies_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a PolicyPage struct is empty.
+func (p PolicyPage) IsEmpty() (bool, error) {
+	is, err := ExtractPolicies(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractPolicies accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPolicies(page pagination.Page) ([]Policy, error) {
+	var resp struct {
+		Policies []Policy `mapstructure:"firewall_policies" json:"firewall_policies"`
+	}
+
+	err := mapstructure.Decode(page.(PolicyPage).Body, &resp)
+
+	return resp.Policies, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/fwaas/policies/urls.go b/openstack/networking/v2/extensions/fwaas/policies/urls.go
new file mode 100644
index 0000000..27ea9ae
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/policies/urls.go
@@ -0,0 +1,26 @@
+package policies
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "fw"
+	resourcePath = "firewall_policies"
+	insertPath   = "insert_rule"
+	removePath   = "remove_rule"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id)
+}
+
+func insertURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id, insertPath)
+}
+
+func removeURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id, removePath)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/errors.go b/openstack/networking/v2/extensions/fwaas/rules/errors.go
new file mode 100644
index 0000000..0b29d39
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/errors.go
@@ -0,0 +1,12 @@
+package rules
+
+import "fmt"
+
+func err(str string) error {
+	return fmt.Errorf("%s", str)
+}
+
+var (
+	errProtocolRequired = err("A protocol is required (tcp, udp, icmp or any)")
+	errActionRequired   = err("An action is required (allow or deny)")
+)
diff --git a/openstack/networking/v2/extensions/fwaas/rules/requests.go b/openstack/networking/v2/extensions/fwaas/rules/requests.go
new file mode 100644
index 0000000..3780106
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/requests.go
@@ -0,0 +1,296 @@
+package rules
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Binary gives users a solid type to work with for create and update
+// operations. It is recommended that users use the `Yes` and `No` enums
+type Binary *bool
+
+// Convenience vars for Enabled and Shared values.
+var (
+	iTrue         = true
+	iFalse        = false
+	Yes    Binary = &iTrue
+	No     Binary = &iFalse
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToRuleListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the Firewall rule attributes you want to see returned. SortKey allows you to
+// sort by a particular firewall rule attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+	TenantID             string `q:"tenant_id"`
+	Name                 string `q:"name"`
+	Description          string `q:"description"`
+	Protocol             string `q:"protocol"`
+	Action               string `q:"action"`
+	IPVersion            int    `q:"ip_version"`
+	SourceIPAddress      string `q:"source_ip_address"`
+	DestinationIPAddress string `q:"destination_ip_address"`
+	SourcePort           string `q:"source_port"`
+	DestinationPort      string `q:"destination_port"`
+	Enabled              bool   `q:"enabled"`
+	ID                   string `q:"id"`
+	Limit                int    `q:"limit"`
+	Marker               string `q:"marker"`
+	SortKey              string `q:"sort_key"`
+	SortDir              string `q:"sort_dir"`
+}
+
+// ToRuleListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToRuleListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// firewall rules. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those firewall rules that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := rootURL(c)
+
+	if opts != nil {
+		query, err := opts.ToRuleListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+		return RulePage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+	ToRuleCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new firewall rule.
+type CreateOpts struct {
+	// Mandatory for create
+	Protocol string
+	Action   string
+	// Optional
+	TenantID             string
+	Name                 string
+	Description          string
+	IPVersion            int
+	SourceIPAddress      string
+	DestinationIPAddress string
+	SourcePort           string
+	DestinationPort      string
+	Shared               *bool
+	Enabled              *bool
+}
+
+// ToRuleCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+	if opts.Protocol == "" {
+		return nil, errProtocolRequired
+	}
+
+	if opts.Action == "" {
+		return nil, errActionRequired
+	}
+
+	r := make(map[string]interface{})
+
+	r["protocol"] = opts.Protocol
+	r["action"] = opts.Action
+
+	if opts.TenantID != "" {
+		r["tenant_id"] = opts.TenantID
+	}
+	if opts.Name != "" {
+		r["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		r["description"] = opts.Description
+	}
+	if opts.IPVersion != 0 {
+		r["ip_version"] = opts.IPVersion
+	}
+	if opts.SourceIPAddress != "" {
+		r["source_ip_address"] = opts.SourceIPAddress
+	}
+	if opts.DestinationIPAddress != "" {
+		r["destination_ip_address"] = opts.DestinationIPAddress
+	}
+	if opts.SourcePort != "" {
+		r["source_port"] = opts.SourcePort
+	}
+	if opts.DestinationPort != "" {
+		r["destination_port"] = opts.DestinationPort
+	}
+	if opts.Shared != nil {
+		r["shared"] = *opts.Shared
+	}
+	if opts.Enabled != nil {
+		r["enabled"] = *opts.Enabled
+	}
+
+	return map[string]interface{}{"firewall_rule": r}, nil
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new firewall rule
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+	var res CreateResult
+
+	reqBody, err := opts.ToRuleCreateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	_, res.Err = c.Request("POST", rootURL(c), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{201},
+	})
+	return res
+}
+
+// Get retrieves a particular firewall rule based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = c.Request("GET", resourceURL(c, id), gophercloud.RequestOpts{
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+	return res
+}
+
+// UpdateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Update operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type UpdateOptsBuilder interface {
+	ToRuleUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a firewall rule.
+// Optional
+type UpdateOpts struct {
+	Protocol             string
+	Action               string
+	Name                 string
+	Description          string
+	IPVersion            int
+	SourceIPAddress      *string
+	DestinationIPAddress *string
+	SourcePort           *string
+	DestinationPort      *string
+	Shared               *bool
+	Enabled              *bool
+}
+
+// ToRuleUpdateMap casts a UpdateOpts struct to a map.
+func (opts UpdateOpts) ToRuleUpdateMap() (map[string]interface{}, error) {
+	r := make(map[string]interface{})
+
+	if opts.Protocol != "" {
+		r["protocol"] = opts.Protocol
+	}
+	if opts.Action != "" {
+		r["action"] = opts.Action
+	}
+	if opts.Name != "" {
+		r["name"] = opts.Name
+	}
+	if opts.Description != "" {
+		r["description"] = opts.Description
+	}
+	if opts.IPVersion != 0 {
+		r["ip_version"] = opts.IPVersion
+	}
+	if opts.SourceIPAddress != nil {
+		s := *opts.SourceIPAddress
+		if s == "" {
+			r["source_ip_address"] = nil
+		} else {
+			r["source_ip_address"] = s
+		}
+	}
+	if opts.DestinationIPAddress != nil {
+		s := *opts.DestinationIPAddress
+		if s == "" {
+			r["destination_ip_address"] = nil
+		} else {
+			r["destination_ip_address"] = s
+		}
+	}
+	if opts.SourcePort != nil {
+		s := *opts.SourcePort
+		if s == "" {
+			r["source_port"] = nil
+		} else {
+			r["source_port"] = s
+		}
+	}
+	if opts.DestinationPort != nil {
+		s := *opts.DestinationPort
+		if s == "" {
+			r["destination_port"] = nil
+		} else {
+			r["destination_port"] = s
+		}
+	}
+	if opts.Shared != nil {
+		r["shared"] = *opts.Shared
+	}
+	if opts.Enabled != nil {
+		r["enabled"] = *opts.Enabled
+	}
+
+	return map[string]interface{}{"firewall_rule": r}, nil
+}
+
+// Update allows firewall policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
+	var res UpdateResult
+
+	reqBody, err := opts.ToRuleUpdateMap()
+	if err != nil {
+		res.Err = err
+		return res
+	}
+
+	// Send request to API
+	_, res.Err = c.Request("PUT", resourceURL(c, id), gophercloud.RequestOpts{
+		JSONBody:     &reqBody,
+		JSONResponse: &res.Body,
+		OkCodes:      []int{200},
+	})
+
+	return res
+}
+
+// Delete will permanently delete a particular firewall rule based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+	var res DeleteResult
+	_, res.Err = c.Request("DELETE", resourceURL(c, id), gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	return res
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/requests_test.go b/openstack/networking/v2/extensions/fwaas/rules/requests_test.go
new file mode 100644
index 0000000..36f89fa
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/requests_test.go
@@ -0,0 +1,328 @@
+package rules
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	fake "github.com/rackspace/gophercloud/openstack/networking/v2/common"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestURLs(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.AssertEquals(t, th.Endpoint()+"v2.0/fw/firewall_rules", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "firewall_rules": [
+        {
+            "protocol": "tcp",
+            "description": "ssh rule",
+            "source_port": null,
+            "source_ip_address": null,
+            "destination_ip_address": "192.168.1.0/24",
+            "firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+            "position": 2,
+            "destination_port": "22",
+            "id": "f03bd950-6c56-4f5e-a307-45967078f507",
+            "name": "ssh_form_any",
+            "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+            "enabled": true,
+            "action": "allow",
+            "ip_version": 4,
+            "shared": false
+        },
+        {
+            "protocol": "udp",
+            "description": "udp rule",
+            "source_port": null,
+            "source_ip_address": null,
+            "destination_ip_address": null,
+            "firewall_policy_id": "98d7fb51-698c-4123-87e8-f1eee6b5ab7e",
+            "position": 1,
+            "destination_port": null,
+            "id": "ab7bd950-6c56-4f5e-a307-45967078f890",
+            "name": "deny_all_udp",
+            "tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+            "enabled": true,
+            "action": "deny",
+            "ip_version": 4,
+            "shared": false
+        }
+    ]
+}
+        `)
+	})
+
+	count := 0
+
+	List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRules(page)
+		if err != nil {
+			t.Errorf("Failed to extract members: %v", err)
+			return false, err
+		}
+
+		expected := []Rule{
+			Rule{
+				Protocol:             "tcp",
+				Description:          "ssh rule",
+				SourcePort:           "",
+				SourceIPAddress:      "",
+				DestinationIPAddress: "192.168.1.0/24",
+				PolicyID:             "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+				Position:             2,
+				DestinationPort:      "22",
+				ID:                   "f03bd950-6c56-4f5e-a307-45967078f507",
+				Name:                 "ssh_form_any",
+				TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+				Enabled:              true,
+				Action:               "allow",
+				IPVersion:            4,
+				Shared:               false,
+			},
+			Rule{
+				Protocol:             "udp",
+				Description:          "udp rule",
+				SourcePort:           "",
+				SourceIPAddress:      "",
+				DestinationIPAddress: "",
+				PolicyID:             "98d7fb51-698c-4123-87e8-f1eee6b5ab7e",
+				Position:             1,
+				DestinationPort:      "",
+				ID:                   "ab7bd950-6c56-4f5e-a307-45967078f890",
+				Name:                 "deny_all_udp",
+				TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+				Enabled:              true,
+				Action:               "deny",
+				IPVersion:            4,
+				Shared:               false,
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
+
+func TestCreate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules", 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_rule": {
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"destination_ip_address": "192.168.1.0/24",
+		"destination_port": "22",
+		"name": "ssh_form_any",
+		"action": "allow",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61"
+	}
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprintf(w, `
+{
+	"firewall_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"source_port": null,
+		"source_ip_address": null,
+		"destination_ip_address": "192.168.1.0/24",
+		"firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+		"position": 2,
+		"destination_port": "22",
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "ssh_form_any",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": true,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+        `)
+	})
+
+	options := CreateOpts{
+		TenantID:             "80cf934d6ffb4ef5b244f1c512ad1e61",
+		Protocol:             "tcp",
+		Description:          "ssh rule",
+		DestinationIPAddress: "192.168.1.0/24",
+		DestinationPort:      "22",
+		Name:                 "ssh_form_any",
+		Action:               "allow",
+	}
+
+	_, err := Create(fake.ServiceClient(), options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"firewall_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"source_port": null,
+		"source_ip_address": null,
+		"destination_ip_address": "192.168.1.0/24",
+		"firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+		"position": 2,
+		"destination_port": "22",
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "ssh_form_any",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": true,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+        `)
+	})
+
+	rule, err := Get(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507").Extract()
+	th.AssertNoErr(t, err)
+
+	th.AssertEquals(t, "tcp", rule.Protocol)
+	th.AssertEquals(t, "ssh rule", rule.Description)
+	th.AssertEquals(t, "192.168.1.0/24", rule.DestinationIPAddress)
+	th.AssertEquals(t, "e2a5fb51-698c-4898-87e8-f1eee6b50919", rule.PolicyID)
+	th.AssertEquals(t, 2, rule.Position)
+	th.AssertEquals(t, "22", rule.DestinationPort)
+	th.AssertEquals(t, "f03bd950-6c56-4f5e-a307-45967078f507", rule.ID)
+	th.AssertEquals(t, "ssh_form_any", rule.Name)
+	th.AssertEquals(t, "80cf934d6ffb4ef5b244f1c512ad1e61", rule.TenantID)
+	th.AssertEquals(t, true, rule.Enabled)
+	th.AssertEquals(t, "allow", rule.Action)
+	th.AssertEquals(t, 4, rule.IPVersion)
+	th.AssertEquals(t, false, rule.Shared)
+}
+
+func TestUpdate(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules/f03bd950-6c56-4f5e-a307-45967078f507", 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_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"destination_ip_address": "192.168.1.0/24",
+		"destination_port": "22",
+		"source_ip_address": null,
+		"source_port": null,
+		"name": "ssh_form_any",
+		"action": "allow",
+		"enabled": false
+	}
+}
+	`)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+	"firewall_rule":{
+		"protocol": "tcp",
+		"description": "ssh rule",
+		"source_port": null,
+		"source_ip_address": null,
+		"destination_ip_address": "192.168.1.0/24",
+		"firewall_policy_id": "e2a5fb51-698c-4898-87e8-f1eee6b50919",
+		"position": 2,
+		"destination_port": "22",
+		"id": "f03bd950-6c56-4f5e-a307-45967078f507",
+		"name": "ssh_form_any",
+		"tenant_id": "80cf934d6ffb4ef5b244f1c512ad1e61",
+		"enabled": false,
+		"action": "allow",
+		"ip_version": 4,
+		"shared": false
+	}
+}
+		`)
+	})
+
+	destinationIPAddress := "192.168.1.0/24"
+	destinationPort := "22"
+	empty := ""
+
+	options := UpdateOpts{
+		Protocol:             "tcp",
+		Description:          "ssh rule",
+		DestinationIPAddress: &destinationIPAddress,
+		DestinationPort:      &destinationPort,
+		Name:                 "ssh_form_any",
+		SourceIPAddress:      &empty,
+		SourcePort:           &empty,
+		Action:               "allow",
+		Enabled:              No,
+	}
+
+	_, err := Update(fake.ServiceClient(), "f03bd950-6c56-4f5e-a307-45967078f507", options).Extract()
+	th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/fw/firewall_rules/4ec89077-d057-4a2b-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "DELETE")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	res := Delete(fake.ServiceClient(), "4ec89077-d057-4a2b-911f-60a3b47ee304")
+	th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/results.go b/openstack/networking/v2/extensions/fwaas/rules/results.go
new file mode 100644
index 0000000..d772024
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/results.go
@@ -0,0 +1,110 @@
+package rules
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// Rule represents a firewall rule
+type Rule struct {
+	ID                   string `json:"id" mapstructure:"id"`
+	Name                 string `json:"name,omitempty" mapstructure:"name"`
+	Description          string `json:"description,omitempty" mapstructure:"description"`
+	Protocol             string `json:"protocol" mapstructure:"protocol"`
+	Action               string `json:"action" mapstructure:"action"`
+	IPVersion            int    `json:"ip_version,omitempty" mapstructure:"ip_version"`
+	SourceIPAddress      string `json:"source_ip_address,omitempty" mapstructure:"source_ip_address"`
+	DestinationIPAddress string `json:"destination_ip_address,omitempty" mapstructure:"destination_ip_address"`
+	SourcePort           string `json:"source_port,omitempty" mapstructure:"source_port"`
+	DestinationPort      string `json:"destination_port,omitempty" mapstructure:"destination_port"`
+	Shared               bool   `json:"shared,omitempty" mapstructure:"shared"`
+	Enabled              bool   `json:"enabled,omitempty" mapstructure:"enabled"`
+	PolicyID             string `json:"firewall_policy_id" mapstructure:"firewall_policy_id"`
+	Position             int    `json:"position" mapstructure:"position"`
+	TenantID             string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// RulePage is the page returned by a pager when traversing over a
+// collection of firewall rules.
+type RulePage struct {
+	pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of firewall rules has
+// reached the end of a page and the pager seeks to traverse over a new one.
+// In order to do this, it needs to construct the next page's URL.
+func (p RulePage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []gophercloud.Link `mapstructure:"firewall_rules_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(p.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	return gophercloud.ExtractNextURL(r.Links)
+}
+
+// IsEmpty checks whether a RulePage struct is empty.
+func (p RulePage) IsEmpty() (bool, error) {
+	is, err := ExtractRules(p)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+// ExtractRules accepts a Page struct, specifically a RouterPage struct,
+// and extracts the elements into a slice of Router structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRules(page pagination.Page) ([]Rule, error) {
+	var resp struct {
+		Rules []Rule `mapstructure:"firewall_rules" json:"firewall_rules"`
+	}
+
+	err := mapstructure.Decode(page.(RulePage).Body, &resp)
+
+	return resp.Rules, err
+}
+
+type commonResult struct {
+	gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a firewall rule.
+func (r commonResult) Extract() (*Rule, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Rule *Rule `json:"firewall_rule" mapstructure:"firewall_rule"`
+	}
+
+	err := mapstructure.Decode(r.Body, &res)
+
+	return res.Rule, err
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+	commonResult
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult struct {
+	commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+	commonResult
+}
diff --git a/openstack/networking/v2/extensions/fwaas/rules/urls.go b/openstack/networking/v2/extensions/fwaas/rules/urls.go
new file mode 100644
index 0000000..20b0879
--- /dev/null
+++ b/openstack/networking/v2/extensions/fwaas/rules/urls.go
@@ -0,0 +1,16 @@
+package rules
+
+import "github.com/rackspace/gophercloud"
+
+const (
+	rootPath     = "fw"
+	resourcePath = "firewall_rules"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/provider_client.go b/provider_client.go
index 3c65984..200ee0b 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -63,6 +63,11 @@
 
 	// UserAgent represents the User-Agent header in the HTTP request.
 	UserAgent UserAgent
+
+	// ReauthFunc is the function used to re-authenticate the user if the request
+	// fails with a 401 HTTP response code. This a needed because there may be multiple
+	// authentication functions for different Identity service versions.
+	ReauthFunc func() error
 }
 
 // AuthenticatedHeaders returns a map of HTTP headers that are common for all
@@ -179,6 +184,19 @@
 		return nil, err
 	}
 
+	if resp.StatusCode == http.StatusUnauthorized {
+		if client.ReauthFunc != nil {
+			err = client.ReauthFunc()
+			if err != nil {
+				return nil, fmt.Errorf("Error trying to re-authenticate: %s", err)
+			}
+			resp, err = client.Request(method, url, options)
+			if err != nil {
+				return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err)
+			}
+		}
+	}
+
 	// Validate the response code, if requested to do so.
 	if options.OkCodes != nil {
 		var ok bool
diff --git a/rackspace/client.go b/rackspace/client.go
index 039f446..8f1f34f 100644
--- a/rackspace/client.go
+++ b/rackspace/client.go
@@ -96,6 +96,11 @@
 		return err
 	}
 
+	if options.AllowReauth {
+		client.ReauthFunc = func() error {
+			return AuthenticateV2(client, options)
+		}
+	}
 	client.TokenID = token.ID
 	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
 		return os.V2EndpointURL(catalog, opts)