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)