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)
+}