Feature/filestorage securityservices list (#134)

* sfs: Add support for security services List

* sfs: Add acceptance tests for security service List

* sfs: Remove extra parameters for security service list

After taking a closer look at the code, some parameters
seem to be filtered out during the list request. They
have now been filtered out.

* sfs: Fix unit tests

* sfs: Use SecurityServiceType for ListOpts Type
diff --git a/openstack/sharedfilesystems/v2/securityservices/requests.go b/openstack/sharedfilesystems/v2/securityservices/requests.go
index 1253d33..9de58b9 100644
--- a/openstack/sharedfilesystems/v2/securityservices/requests.go
+++ b/openstack/sharedfilesystems/v2/securityservices/requests.go
@@ -1,6 +1,9 @@
 package securityservices
 
-import "github.com/gophercloud/gophercloud"
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
 
 type SecurityServiceType string
 
@@ -65,3 +68,54 @@
 	_, r.Err = client.Delete(deleteURL(client, id), nil)
 	return
 }
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToSecurityServiceListQuery() (string, error)
+}
+
+// ListOpts holds options for listing SecurityServices. It is passed to the
+// securityservices.List function.
+type ListOpts struct {
+	// admin-only option. Set it to true to see all tenant security services.
+	AllTenants bool `q:"all_tenants"`
+	// The security service ID
+	ID string `q:"id"`
+	// The security service domain
+	Domain string `q:"domain"`
+	// The security service type. A valid value is ldap, kerberos, or active_directory
+	Type SecurityServiceType `q:"type"`
+	// The security service name
+	Name string `q:"name"`
+	// The DNS IP address that is used inside the tenant network
+	DNSIP string `q:"dns_ip"`
+	// The security service user or group name that is used by the tenant
+	User string `q:"user"`
+	// The security service host name or IP address
+	Server string `q:"server"`
+	// The ID of the share network using security services
+	ShareNetworkID string `q:"share_network_id"`
+}
+
+// ToSecurityServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSecurityServiceListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List returns SecurityServices optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToSecurityServiceListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return SecurityServicePage{pagination.SinglePageBase(r)}
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/results.go b/openstack/sharedfilesystems/v2/securityservices/results.go
index 6874208..ab9da7d 100644
--- a/openstack/sharedfilesystems/v2/securityservices/results.go
+++ b/openstack/sharedfilesystems/v2/securityservices/results.go
@@ -5,6 +5,7 @@
 	"time"
 
 	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
 )
 
 // SecurityService contains all the information associated with an OpenStack
@@ -61,6 +62,27 @@
 	gophercloud.Result
 }
 
+// SecurityServicePage is a pagination.pager that is returned from a call to the List function.
+type SecurityServicePage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no SecurityServices.
+func (r SecurityServicePage) IsEmpty() (bool, error) {
+	securityServices, err := ExtractSecurityServices(r)
+	return len(securityServices) == 0, err
+}
+
+// ExtractSecurityServices extracts and returns SecurityServices. It is used while
+// iterating over a securityservices.List call.
+func ExtractSecurityServices(r pagination.Page) ([]SecurityService, error) {
+	var s struct {
+		SecurityServices []SecurityService `json:"security_services"`
+	}
+	err := (r.(SecurityServicePage)).ExtractInto(&s)
+	return s.SecurityServices, err
+}
+
 // Extract will get the SecurityService object out of the commonResult object.
 func (r commonResult) Extract() (*SecurityService, error) {
 	var s struct {
diff --git a/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go b/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go
index e6b101a..c6b5f70 100644
--- a/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go
+++ b/openstack/sharedfilesystems/v2/securityservices/testing/fixtures.go
@@ -58,3 +58,80 @@
 		w.WriteHeader(http.StatusAccepted)
 	})
 }
+
+func MockListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/security-services/detail", 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, `
+        {
+            "security_services": [
+                {
+                    "status": "new",
+                    "domain": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "name": "SecServ1",
+                    "created_at": "2015-09-07T12:19:10.000000",
+                    "description": "Creating my first Security Service",
+                    "updated_at": null,
+                    "server": null,
+                    "dns_ip": "10.0.0.0/24",
+                    "user": "demo",
+                    "password": "supersecret",
+                    "type": "kerberos",
+                    "id": "3c829734-0679-4c17-9637-801da48c0d5f"
+                },
+                {
+                    "status": "new",
+                    "domain": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "name": "SecServ2",
+                    "created_at": "2015-09-07T12:25:03.000000",
+                    "description": "Creating my second Security Service",
+                    "updated_at": null,
+                    "server": null,
+                    "dns_ip": "10.0.0.0/24",
+                    "user": null,
+                    "password": null,
+                    "type": "ldap",
+                    "id": "5a1d3a12-34a7-4087-8983-50e9ed03509a"
+                }
+            ]
+        }`)
+	})
+}
+
+func MockFilteredListResponse(t *testing.T) {
+	th.Mux.HandleFunc("/security-services/detail", 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, `
+        {
+            "security_services": [
+                {
+                    "status": "new",
+                    "domain": null,
+                    "project_id": "16e1ab15c35a457e9c2b2aa189f544e1",
+                    "name": "SecServ1",
+                    "created_at": "2015-09-07T12:19:10.000000",
+                    "description": "Creating my first Security Service",
+                    "updated_at": null,
+                    "server": null,
+                    "dns_ip": "10.0.0.0/24",
+                    "user": "demo",
+                    "password": "supersecret",
+                    "type": "kerberos",
+                    "id": "3c829734-0679-4c17-9637-801da48c0d5f"
+                }
+            ]
+        }`)
+	})
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go b/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go
index 8ef88ee..9feff53 100644
--- a/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go
+++ b/openstack/sharedfilesystems/v2/securityservices/testing/requests_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"testing"
+	"time"
 
 	"github.com/gophercloud/gophercloud"
 	"github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/securityservices"
@@ -62,3 +63,88 @@
 	res := securityservices.Delete(client.ServiceClient(), "securityServiceID")
 	th.AssertNoErr(t, res.Err)
 }
+
+// Verifies that security services can be listed correctly
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockListResponse(t)
+
+	allPages, err := securityservices.List(client.ServiceClient(), &securityservices.ListOpts{}).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := securityservices.ExtractSecurityServices(allPages)
+	th.AssertNoErr(t, err)
+	var nilTime time.Time
+	expected := []securityservices.SecurityService{
+		{
+			Status:      "new",
+			Domain:      "",
+			ProjectID:   "16e1ab15c35a457e9c2b2aa189f544e1",
+			Name:        "SecServ1",
+			CreatedAt:   time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC),
+			Description: "Creating my first Security Service",
+			UpdatedAt:   nilTime,
+			Server:      "",
+			DNSIP:       "10.0.0.0/24",
+			User:        "demo",
+			Password:    "supersecret",
+			Type:        "kerberos",
+			ID:          "3c829734-0679-4c17-9637-801da48c0d5f",
+		},
+		{
+			Status:      "new",
+			Domain:      "",
+			ProjectID:   "16e1ab15c35a457e9c2b2aa189f544e1",
+			Name:        "SecServ2",
+			CreatedAt:   time.Date(2015, 9, 7, 12, 25, 03, 0, time.UTC),
+			Description: "Creating my second Security Service",
+			UpdatedAt:   nilTime,
+			Server:      "",
+			DNSIP:       "10.0.0.0/24",
+			User:        "",
+			Password:    "",
+			Type:        "ldap",
+			ID:          "5a1d3a12-34a7-4087-8983-50e9ed03509a",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+// Verifies that security services list can be called with query parameters
+func TestFilteredList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	MockFilteredListResponse(t)
+
+	options := &securityservices.ListOpts{
+		Type: "kerberos",
+	}
+
+	allPages, err := securityservices.List(client.ServiceClient(), options).AllPages()
+	th.AssertNoErr(t, err)
+	actual, err := securityservices.ExtractSecurityServices(allPages)
+	th.AssertNoErr(t, err)
+	var nilTime time.Time
+	expected := []securityservices.SecurityService{
+		{
+			Status:      "new",
+			Domain:      "",
+			ProjectID:   "16e1ab15c35a457e9c2b2aa189f544e1",
+			Name:        "SecServ1",
+			CreatedAt:   time.Date(2015, 9, 7, 12, 19, 10, 0, time.UTC),
+			Description: "Creating my first Security Service",
+			UpdatedAt:   nilTime,
+			Server:      "",
+			DNSIP:       "10.0.0.0/24",
+			User:        "demo",
+			Password:    "supersecret",
+			Type:        "kerberos",
+			ID:          "3c829734-0679-4c17-9637-801da48c0d5f",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
diff --git a/openstack/sharedfilesystems/v2/securityservices/urls.go b/openstack/sharedfilesystems/v2/securityservices/urls.go
index 5cbdd17..e4deb5f 100644
--- a/openstack/sharedfilesystems/v2/securityservices/urls.go
+++ b/openstack/sharedfilesystems/v2/securityservices/urls.go
@@ -9,3 +9,7 @@
 func deleteURL(c *gophercloud.ServiceClient, id string) string {
 	return c.ServiceURL("security-services", id)
 }
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("security-services", "detail")
+}