Bulk delete network items from access list
diff --git a/rackspace/lb/v1/acl/fixtures.go b/rackspace/lb/v1/acl/fixtures.go
index 953130d..a1b30c9 100644
--- a/rackspace/lb/v1/acl/fixtures.go
+++ b/rackspace/lb/v1/acl/fixtures.go
@@ -27,43 +27,23 @@
"accessList": [
{
"address": "206.160.163.21",
+ "id": 21,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.22",
+ "id": 22,
+ "type": "DENY"
+ },
+ {
+ "address": "206.160.163.23",
"id": 23,
"type": "DENY"
},
{
- "address": "206.160.165.11",
+ "address": "206.160.163.24",
"id": 24,
"type": "DENY"
- },
- {
- "address": "206.160.163.21",
- "id": 25,
- "type": "DENY"
- },
- {
- "address": "206.160.165.11",
- "id": 26,
- "type": "DENY"
- },
- {
- "address": "206.160.123.11",
- "id": 27,
- "type": "DENY"
- },
- {
- "address": "206.160.122.21",
- "id": 28,
- "type": "DENY"
- },
- {
- "address": "206.140.123.11",
- "id": 29,
- "type": "DENY"
- },
- {
- "address": "206.140.122.21",
- "id": 30,
- "type": "DENY"
}
]
}
@@ -71,8 +51,8 @@
})
}
-func mockCreateResponse(t *testing.T) {
- th.Mux.HandleFunc("/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+func mockCreateResponse(t *testing.T, lbID int) {
+ th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
diff --git a/rackspace/lb/v1/acl/requests.go b/rackspace/lb/v1/acl/requests.go
index e564307..206ec48 100644
--- a/rackspace/lb/v1/acl/requests.go
+++ b/rackspace/lb/v1/acl/requests.go
@@ -1 +1,101 @@
package acl
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/rackspace/lb/v1"
+)
+
+// List is the operation responsible for returning a paginated collection of
+// network items that define a load balancer's access list.
+func List(client *gophercloud.ServiceClient, lbID int) pagination.Pager {
+ url := rootURL(client, lbID)
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return AccessListPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// CreateOptsBuilder is the interface responsible for generating the JSON
+// for a Create operation.
+type CreateOptsBuilder interface {
+ ToAccessListCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is a slice of CreateOpt structs, that allow the user to create
+// multiple nodes in a single operation (one node per CreateOpt).
+type CreateOpts []CreateOpt
+
+// CreateOpt represents the options to create a single node.
+type CreateOpt struct {
+ // Required - the IP address or CIDR for item to add to access list.
+ Address string
+
+ // Required - the type of the node. Either ALLOW or DENY.
+ Type Type
+}
+
+// ToAccessListCreateMap converts a slice of options into a map that can be
+// used for the JSON.
+func (opts CreateOpts) ToAccessListCreateMap() (map[string]interface{}, error) {
+ type itemMap map[string]interface{}
+ items := []itemMap{}
+
+ for k, v := range opts {
+ if v.Address == "" {
+ return itemMap{}, fmt.Errorf("Address is a required attribute, none provided for %d CreateOpt element", k)
+ }
+ if v.Type != ALLOW && v.Type != DENY {
+ return itemMap{}, fmt.Errorf("Type must be ALLOW or DENY")
+ }
+
+ item := make(itemMap)
+ item["address"] = v.Address
+ item["type"] = v.Type
+
+ items = append(items, item)
+ }
+
+ return itemMap{"accessList": items}, nil
+}
+
+func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
+
+ reqBody, err := opts.ToAccessListCreateMap()
+ if err != nil {
+ res.Err = err
+ return res
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(client, loadBalancerID), perigee.Options{
+ MoreHeaders: client.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ OkCodes: []int{202},
+ })
+
+ return res
+}
+
+func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, itemIDs []int) DeleteResult {
+ var res DeleteResult
+
+ if len(itemIDs) > 10 || len(itemIDs) == 0 {
+ res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 item IDs")
+ return res
+ }
+
+ url := rootURL(c, loadBalancerID)
+ url += v1.IDSliceToQueryString("id", itemIDs)
+
+ _, res.Err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: c.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+
+ return res
+}
diff --git a/rackspace/lb/v1/acl/requests_test.go b/rackspace/lb/v1/acl/requests_test.go
index e564307..67fb2f1 100644
--- a/rackspace/lb/v1/acl/requests_test.go
+++ b/rackspace/lb/v1/acl/requests_test.go
@@ -1 +1,71 @@
package acl
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+const (
+ lbID = 12345
+ niID1 = 67890
+ niID2 = 67891
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockListResponse(t, lbID)
+
+ count := 0
+
+ err := List(client.ServiceClient(), lbID).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAccessList(page)
+ th.AssertNoErr(t, err)
+
+ expected := AccessList{
+ NetworkItem{Address: "206.160.163.21", ID: 21, Type: DENY},
+ NetworkItem{Address: "206.160.163.22", ID: 22, Type: DENY},
+ NetworkItem{Address: "206.160.163.23", ID: 23, Type: DENY},
+ NetworkItem{Address: "206.160.163.24", ID: 24, Type: DENY},
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, 1, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockCreateResponse(t, lbID)
+
+ opts := CreateOpts{
+ CreateOpt{Address: "206.160.163.21", Type: DENY},
+ CreateOpt{Address: "206.160.165.11", Type: DENY},
+ }
+
+ err := Create(client.ServiceClient(), lbID, opts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestBulkDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ ids := []int{niID1, niID2}
+
+ mockBatchDeleteResponse(t, lbID, ids)
+
+ err := BulkDelete(client.ServiceClient(), lbID, ids).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/rackspace/lb/v1/acl/results.go b/rackspace/lb/v1/acl/results.go
index e564307..ed87be7 100644
--- a/rackspace/lb/v1/acl/results.go
+++ b/rackspace/lb/v1/acl/results.go
@@ -1 +1,59 @@
package acl
+
+import (
+ "github.com/mitchellh/mapstructure"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type AccessList []NetworkItem
+
+type NetworkItem struct {
+ Address string
+ ID int
+ Type Type
+}
+
+type Type string
+
+const (
+ ALLOW Type = "ALLOW"
+ DENY Type = "DENY"
+)
+
+// AccessListPage is the page returned by a pager when traversing over a collection of
+// network items in an access list.
+type AccessListPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an AccessListPage struct is empty.
+func (p AccessListPage) IsEmpty() (bool, error) {
+ is, err := ExtractAccessList(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAccessList accepts a Page struct, specifically an AccessListPage
+// struct, and extracts the elements into a slice of NetworkItem structs. In
+// other words, a generic collection is mapped into a relevant slice.
+func ExtractAccessList(page pagination.Page) (AccessList, error) {
+ var resp struct {
+ List AccessList `mapstructure:"accessList" json:"accessList"`
+ }
+
+ err := mapstructure.Decode(page.(AccessListPage).Body, &resp)
+
+ return resp.List, err
+}
+
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/rackspace/lb/v1/acl/urls.go b/rackspace/lb/v1/acl/urls.go
index e564307..e373fa1 100644
--- a/rackspace/lb/v1/acl/urls.go
+++ b/rackspace/lb/v1/acl/urls.go
@@ -1 +1,20 @@
package acl
+
+import (
+ "strconv"
+
+ "github.com/rackspace/gophercloud"
+)
+
+const (
+ path = "loadbalancers"
+ aclPath = "accesslist"
+)
+
+func resourceURL(c *gophercloud.ServiceClient, lbID, networkID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), aclPath, strconv.Itoa(networkID))
+}
+
+func rootURL(c *gophercloud.ServiceClient, lbID int) string {
+ return c.ServiceURL(path, strconv.Itoa(lbID), aclPath)
+}