Merge pull request #236 from jamiehannaford/server-list-opts

Adding Server list opts
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index 047d763..5193e4e 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -20,7 +20,7 @@
 
 	t.Logf("ID\tRegion\tName\tStatus\tIPv4\tIPv6")
 
-	pager := servers.List(client)
+	pager := servers.List(client, servers.ListOpts{})
 	count, pages := 0, 0
 	pager.EachPage(func(page pagination.Page) (bool, error) {
 		pages++
diff --git a/docs/identity/v2/index.md b/docs/identity/v2/index.md
new file mode 100644
index 0000000..c0e82bd
--- /dev/null
+++ b/docs/identity/v2/index.md
@@ -0,0 +1,60 @@
+---
+layout: page
+title: Getting Started with Identity v2
+---
+
+## Tokens
+
+A token is an arbitrary bit of text that is used to access resources. Each
+token has a scope that describes which resources are accessible with it. A
+token may be revoked at anytime and is valid for a finite duration.
+
+### Generate a token
+
+The nature of required and optional auth options will depend on your provider,
+but generally the `Username` and `IdentityEndpoint` fields are always
+required. Some providers will insist on a `Password` instead of an `APIKey`,
+others will prefer `TenantID` over `TenantName` - so it is always worth
+checking before writing your implementation in Go.
+
+{% highlight go %}
+import "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
+
+opts := tokens.AuthOptions{
+  IdentityEndpoint: "{identityURL}",
+  Username:         "{username}",
+  APIKey:           "{apiKey}",
+}
+
+token, err := tokens.Create(client, opts).Extract()
+{% endhighlight %}
+
+## Tenants
+
+A tenant is a container used to group or isolate API resources. Depending on
+the provider, a tenant can map to a customer, account, organization, or project.
+
+### List tenants
+
+{% highlight go %}
+import (
+  "github.com/rackspace/gophercloud/pagination"
+  "github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
+)
+
+// We have the option of filtering the tenant list. If we want the full
+// collection, leave it as an empty struct
+opts := tenants.ListOpts{Limit: 10}
+
+// Retrieve a pager (i.e. a paginated collection)
+pager := tenants.List(client, opts)
+
+// Define an anonymous function to be executed on each page's iteration
+err := pager.EachPage(func(page pagination.Page) (bool, error) {
+  tenantList, err := tenants.ExtractTenants(page)
+
+  for _, t := range tenantList {
+    // "t" will be a tenants.Tenant
+  }
+})
+{% endhighlight %}
diff --git a/docs/identity/v3/index.md b/docs/identity/v3/index.md
new file mode 100644
index 0000000..ab2da67
--- /dev/null
+++ b/docs/identity/v3/index.md
@@ -0,0 +1,36 @@
+---
+layout: page
+title: Getting Started with Identity v3
+---
+
+## Tokens
+
+### Generate a token
+
+### Get a token
+
+### Validate token
+
+### Revoke token
+
+## Endpoints
+
+### Create an endpoint
+
+### List endpoints
+
+### Update endpoint
+
+### Delete endpoint
+
+## Services
+
+### Create a service
+
+### List services
+
+### Get a service
+
+### Update service
+
+### Delete service
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 386e0f6..df622bf 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -9,13 +9,71 @@
 	"github.com/rackspace/gophercloud/pagination"
 )
 
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+	ToServerListQuery() (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 server attributes you want to see returned. Marker and Limit are used
+// for pagination.
+type ListOpts struct {
+	// A time/date stamp for when the server last changed status.
+	ChangesSince string `q:"changes-since"`
+
+	// Name of the image in URL format.
+	Image string `q:"image"`
+
+	// Name of the flavor in URL format.
+	Flavor string `q:"flavor"`
+
+	// Name of the server as a string; can be queried with regular expressions.
+	// Realize that ?name=bob returns both bob and bobb. If you need to match bob
+	// only, you can use a regular expression matching the syntax of the
+	// underlying database server implemented for Compute.
+	Name string `q:"name"`
+
+	// Value of the status of the server so that you can filter on "ACTIVE" for example.
+	Status string `q:"status"`
+
+	// Name of the host as a string.
+	Host string `q:"host"`
+
+	// UUID of the server at which you want to set a marker.
+	Marker string `q:"marker"`
+
+	// Integer value for the limit of values to return.
+	Limit int `q:"limit"`
+}
+
+// ToServerListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToServerListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // List makes a request against the API to list servers accessible to you.
-func List(client *gophercloud.ServiceClient) pagination.Pager {
-	createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listDetailURL(client)
+
+	if opts != nil {
+		query, err := opts.ToServerListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	createPageFn := func(r pagination.LastHTTPResponse) pagination.Page {
 		return ServerPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
 	}
 
-	return pagination.NewPager(client, listDetailURL(client), createPage)
+	return pagination.NewPager(client, url, createPageFn)
 }
 
 // CreateOptsBuilder describes struct types that can be accepted by the Create call.
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 806efd3..d7ce2a0 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -5,27 +5,18 @@
 	"net/http"
 	"testing"
 
-	"github.com/rackspace/gophercloud"
 	"github.com/rackspace/gophercloud/pagination"
 	"github.com/rackspace/gophercloud/testhelper"
+	fake "github.com/rackspace/gophercloud/testhelper/client"
 )
 
-const tokenID = "bzbzbzbzbz"
-
-func serviceClient() *gophercloud.ServiceClient {
-	return &gophercloud.ServiceClient{
-		Provider: &gophercloud.ProviderClient{TokenID: tokenID},
-		Endpoint: testhelper.Endpoint(),
-	}
-}
-
 func TestListServers(t *testing.T) {
 	testhelper.SetupHTTP()
 	defer testhelper.TeardownHTTP()
 
 	testhelper.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
 		w.Header().Add("Content-Type", "application/json")
 		r.ParseForm()
@@ -40,9 +31,8 @@
 		}
 	})
 
-	client := serviceClient()
 	pages := 0
-	err := List(client).EachPage(func(page pagination.Page) (bool, error) {
+	err := List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
 		pages++
 
 		actual, err := ExtractServers(page)
@@ -59,9 +49,8 @@
 		return true, nil
 	})
 
-	if err != nil {
-		t.Fatalf("Unexpected error from EachPage: %v", err)
-	}
+	testhelper.AssertNoErr(t, err)
+
 	if pages != 1 {
 		t.Errorf("Expected 1 page, saw %d", pages)
 	}
@@ -73,7 +62,7 @@
 
 	testhelper.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestJSONRequest(t, r, `{
 			"server": {
 				"name": "derp",
@@ -87,7 +76,7 @@
 		fmt.Fprintf(w, singleServerBody)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	actual, err := Create(client, CreateOpts{
 		Name:      "derp",
 		ImageRef:  "f90f6034-2570-4974-8351-6b49732ef2eb",
@@ -106,12 +95,12 @@
 
 	testhelper.Mux.HandleFunc("/servers/asdfasdfasdf", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "DELETE")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	err := Delete(client, "asdfasdfasdf")
 	if err != nil {
 		t.Fatalf("Unexpected Delete error: %v", err)
@@ -124,13 +113,13 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "GET")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestHeader(t, r, "Accept", "application/json")
 
 		fmt.Fprintf(w, singleServerBody)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	actual, err := Get(client, "1234asdf").Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Get error: %v", err)
@@ -145,7 +134,7 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "PUT")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestHeader(t, r, "Accept", "application/json")
 		testhelper.TestHeader(t, r, "Content-Type", "application/json")
 		testhelper.TestJSONRequest(t, r, `{ "server": { "name": "new-name" } }`)
@@ -153,7 +142,7 @@
 		fmt.Fprintf(w, singleServerBody)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Update error: %v", err)
@@ -168,13 +157,13 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestJSONRequest(t, r, `{ "changePassword": { "adminPass": "new-password" } }`)
 
 		w.WriteHeader(http.StatusAccepted)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	err := ChangeAdminPassword(client, "1234asdf", "new-password")
 	if err != nil {
 		t.Errorf("Unexpected ChangeAdminPassword error: %v", err)
@@ -187,13 +176,13 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestJSONRequest(t, r, `{ "reboot": { "type": "SOFT" } }`)
 
 		w.WriteHeader(http.StatusAccepted)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	err := Reboot(client, "1234asdf", SoftReboot)
 	if err != nil {
 		t.Errorf("Unexpected Reboot error: %v", err)
@@ -206,7 +195,7 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestJSONRequest(t, r, `
 			{
 				"rebuild": {
@@ -230,7 +219,7 @@
 		AccessIPv4: "1.2.3.4",
 	}
 
-	actual, err := Rebuild(serviceClient(), "1234asdf", opts).Extract()
+	actual, err := Rebuild(fake.ServiceClient(), "1234asdf", opts).Extract()
 	testhelper.AssertNoErr(t, err)
 
 	equalServers(t, serverDerp, *actual)
@@ -242,13 +231,13 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestJSONRequest(t, r, `{ "resize": { "flavorRef": "2" } }`)
 
 		w.WriteHeader(http.StatusAccepted)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	err := Resize(client, "1234asdf", "2")
 	if err != nil {
 		t.Errorf("Unexpected Reboot error: %v", err)
@@ -261,13 +250,13 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestJSONRequest(t, r, `{ "confirmResize": null }`)
 
 		w.WriteHeader(http.StatusNoContent)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	err := ConfirmResize(client, "1234asdf")
 	if err != nil {
 		t.Errorf("Unexpected ConfirmResize error: %v", err)
@@ -280,13 +269,13 @@
 
 	testhelper.Mux.HandleFunc("/servers/1234asdf/action", func(w http.ResponseWriter, r *http.Request) {
 		testhelper.TestMethod(t, r, "POST")
-		testhelper.TestHeader(t, r, "X-Auth-Token", tokenID)
+		testhelper.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
 		testhelper.TestJSONRequest(t, r, `{ "revertResize": null }`)
 
 		w.WriteHeader(http.StatusAccepted)
 	})
 
-	client := serviceClient()
+	client := fake.ServiceClient()
 	err := RevertResize(client, "1234asdf")
 	if err != nil {
 		t.Errorf("Unexpected RevertResize error: %v", err)