Adding list ports operation
diff --git a/openstack/networking/v2/ports/doc.go b/openstack/networking/v2/ports/doc.go
new file mode 100644
index 0000000..808de88
--- /dev/null
+++ b/openstack/networking/v2/ports/doc.go
@@ -0,0 +1 @@
+package ports
diff --git a/openstack/networking/v2/ports/errors.go b/openstack/networking/v2/ports/errors.go
new file mode 100644
index 0000000..1f9e7dd
--- /dev/null
+++ b/openstack/networking/v2/ports/errors.go
@@ -0,0 +1,9 @@
+package ports
+
+import "fmt"
+
+func err(str string) error {
+	return fmt.Errorf("%s", str)
+}
+
+var ()
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
new file mode 100644
index 0000000..f8e5620
--- /dev/null
+++ b/openstack/networking/v2/ports/requests.go
@@ -0,0 +1,89 @@
+package ports
+
+import (
+	"strconv"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/openstack/utils"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type ListOpts struct {
+	Status          string
+	Name            string
+	AdminStateUp    *bool
+	NetworkID       string
+	TenantID        string
+	DeviceOwner     string
+	MACAddress      string
+	ID              string
+	SecurityGroups  string
+	DeviceID        string
+	BindingHostID   string
+	BindingVIFType  string
+	BindingVNICType string
+	Limit           int
+	Page            string
+	PerPage         string
+}
+
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+	// Build query parameters
+	q := make(map[string]string)
+	if opts.Status != "" {
+		q["status"] = opts.Status
+	}
+	if opts.Name != "" {
+		q["name"] = opts.Name
+	}
+	if opts.AdminStateUp != nil {
+		q["admin_state_up"] = strconv.FormatBool(*opts.AdminStateUp)
+	}
+	if opts.NetworkID != "" {
+		q["network_id"] = opts.NetworkID
+	}
+	if opts.TenantID != "" {
+		q["tenant_id"] = opts.TenantID
+	}
+	if opts.DeviceOwner != "" {
+		q["device_owner"] = opts.DeviceOwner
+	}
+	if opts.MACAddress != "" {
+		q["mac_address"] = opts.MACAddress
+	}
+	if opts.ID != "" {
+		q["id"] = opts.ID
+	}
+	if opts.SecurityGroups != "" {
+		q["security_groups"] = opts.SecurityGroups
+	}
+	if opts.DeviceID != "" {
+		q["device_id"] = opts.DeviceID
+	}
+	if opts.BindingHostID != "" {
+		q["binding:host_id"] = opts.BindingHostID
+	}
+	if opts.BindingVIFType != "" {
+		q["binding:vif_type"] = opts.BindingVIFType
+	}
+	if opts.BindingVNICType != "" {
+		q["binding:vnic_type"] = opts.BindingVNICType
+	}
+	if opts.NetworkID != "" {
+		q["network_id"] = opts.NetworkID
+	}
+	if opts.Limit != 0 {
+		q["limit"] = strconv.Itoa(opts.Limit)
+	}
+	if opts.Page != "" {
+		q["page"] = opts.Page
+	}
+	if opts.PerPage != "" {
+		q["per_page"] = opts.PerPage
+	}
+
+	u := ListURL(c) + utils.BuildQuery(q)
+	return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+		return PortPage{pagination.LinkedPageBase(r)}
+	})
+}
diff --git a/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
new file mode 100644
index 0000000..02ec71a
--- /dev/null
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -0,0 +1,117 @@
+package ports
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{
+		Provider: &gophercloud.ProviderClient{
+			TokenID: TokenID,
+		},
+		Endpoint: th.Endpoint(),
+	}
+}
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+
+		fmt.Fprintf(w, `
+{
+    "ports": [
+        {
+            "status": "ACTIVE",
+            "binding:host_id": "devstack",
+            "name": "",
+            "allowed_address_pairs": [],
+            "admin_state_up": true,
+            "network_id": "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+            "tenant_id": "",
+            "extra_dhcp_opts": [],
+            "binding:vif_details": {
+                "port_filter": true,
+                "ovs_hybrid_plug": true
+            },
+            "binding:vif_type": "ovs",
+            "device_owner": "network:router_gateway",
+            "mac_address": "fa:16:3e:58:42:ed",
+            "binding:profile": {},
+            "binding:vnic_type": "normal",
+            "fixed_ips": [
+                {
+                    "subnet_id": "008ba151-0b8c-4a67-98b5-0d2b87666062",
+                    "ip_address": "172.24.4.2"
+                }
+            ],
+            "id": "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+            "security_groups": [],
+            "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824"
+        }
+    ]
+}
+      `)
+	})
+
+	count := 0
+
+	List(ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractPorts(page)
+		if err != nil {
+			t.Errorf("Failed to extract subnets: %v", err)
+			return false, nil
+		}
+
+		expected := []Port{
+			Port{
+				Status:              "ACTIVE",
+				Name:                "",
+				AllowedAddressPairs: []interface{}(nil),
+				AdminStateUp:        true,
+				NetworkID:           "70c1db1f-b701-45bd-96e0-a313ee3430b3",
+				TenantID:            "",
+				ExtraDHCPOpts:       []interface{}{},
+				DeviceOwner:         "network:router_gateway",
+				MACAddress:          "fa:16:3e:58:42:ed",
+				FixedIPs: []IP{
+					IP{
+						SubnetID:  "008ba151-0b8c-4a67-98b5-0d2b87666062",
+						IPAddress: "172.24.4.2",
+					},
+				},
+				ID:                "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b",
+				SecurityGroups:    []string{},
+				DeviceID:          "9ae135f4-b6e0-4dad-9e91-3c223e385824",
+				BindingHostID:     "devstack",
+				BindingVIFDetails: map[string]interface{}{"port_filter": true, "ovs_hybrid_plug": true},
+				BindingVIFType:    "ovs",
+				BindingProfile:    map[string]interface{}{},
+				BindingVNICType:   "normal",
+			},
+		}
+
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go
new file mode 100644
index 0000000..54390dc
--- /dev/null
+++ b/openstack/networking/v2/ports/results.go
@@ -0,0 +1,84 @@
+package ports
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+type IP struct {
+	SubnetID  string `mapstructure:"subnet_id" json:"subnet_id"`
+	IPAddress string `mapstructure:"ip_address" json:"ip_address"`
+}
+
+type Port struct {
+	Status              string        `mapstructure:"status" json:"status"`
+	Name                string        `mapstructure:"name" json:"name"`
+	AllowedAddressPairs []interface{} `mapstructure:"allowed" json:"allowed"`
+	AdminStateUp        bool          `mapstructure:"admin_state_up" json:"admin_state_up"`
+	NetworkID           string        `mapstructure:"network_id" json:"network_id"`
+	TenantID            string        `mapstructure:"tenant_id" json:"tenant_id"`
+	ExtraDHCPOpts       interface{}   `mapstructure:"extra_dhcp_opts" json:"extra_dhcp_opts"`
+	DeviceOwner         string        `mapstructure:"device_owner" json:"device_owner"`
+	MACAddress          string        `mapstructure:"mac_address" json:"mac_address"`
+	FixedIPs            []IP          `mapstructure:"fixed_ips" json:"fixed_ips"`
+	ID                  string        `mapstructure:"id" json:"id"`
+	SecurityGroups      []string      `mapstructure:"security_groups" json:"security_groups"`
+	DeviceID            string        `mapstructure:"device_id" json:"device_id"`
+	BindingHostID       string        `mapstructure:"binding:host_id" json:"binding:host_id"`
+	BindingVIFDetails   interface{}   `mapstructure:"binding:vif_details" json:"binding:vif_details"`
+	BindingVIFType      string        `mapstructure:"binding:vif_type" json:"binding:vif_type"`
+	BindingProfile      interface{}   `mapstructure:"binding:profile" json:"binding:profile"`
+	BindingVNICType     string        `mapstructure:"binding:vnic_type" json:"binding:vnic_type"`
+}
+
+type PortPage struct {
+	pagination.LinkedPageBase
+}
+
+func (current PortPage) NextPageURL() (string, error) {
+	type resp struct {
+		Links []struct {
+			Href string `mapstructure:"href"`
+			Rel  string `mapstructure:"rel"`
+		} `mapstructure:"ports_links"`
+	}
+
+	var r resp
+	err := mapstructure.Decode(current.Body, &r)
+	if err != nil {
+		return "", err
+	}
+
+	var url string
+	for _, l := range r.Links {
+		if l.Rel == "next" {
+			url = l.Href
+		}
+	}
+	if url == "" {
+		return "", nil
+	}
+
+	return url, nil
+}
+
+func (r PortPage) IsEmpty() (bool, error) {
+	is, err := ExtractPorts(r)
+	if err != nil {
+		return true, nil
+	}
+	return len(is) == 0, nil
+}
+
+func ExtractPorts(page pagination.Page) ([]Port, error) {
+	var resp struct {
+		Ports []Port `mapstructure:"ports" json:"ports"`
+	}
+
+	err := mapstructure.Decode(page.(PortPage).Body, &resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Ports, nil
+}
diff --git a/openstack/networking/v2/ports/urls.go b/openstack/networking/v2/ports/urls.go
new file mode 100644
index 0000000..5dc4696
--- /dev/null
+++ b/openstack/networking/v2/ports/urls.go
@@ -0,0 +1,33 @@
+package ports
+
+import "github.com/rackspace/gophercloud"
+
+const Version = "v2.0"
+
+func ResourceURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(Version, "ports", id)
+}
+
+func RootURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(Version, "ports")
+}
+
+func ListURL(c *gophercloud.ServiceClient) string {
+	return RootURL(c)
+}
+
+func GetURL(c *gophercloud.ServiceClient, id string) string {
+	return ResourceURL(c, id)
+}
+
+func CreateURL(c *gophercloud.ServiceClient) string {
+	return RootURL(c)
+}
+
+func UpdateURL(c *gophercloud.ServiceClient, id string) string {
+	return ResourceURL(c, id)
+}
+
+func DeleteURL(c *gophercloud.ServiceClient, id string) string {
+	return ResourceURL(c, id)
+}
diff --git a/openstack/networking/v2/ports/urls_tests.go b/openstack/networking/v2/ports/urls_tests.go
new file mode 100644
index 0000000..089725d
--- /dev/null
+++ b/openstack/networking/v2/ports/urls_tests.go
@@ -0,0 +1,44 @@
+package ports
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+	th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const Endpoint = "http://localhost:57909/"
+
+func EndpointClient() *gophercloud.ServiceClient {
+	return &gophercloud.ServiceClient{Endpoint: Endpoint}
+}
+
+func TestListURL(t *testing.T) {
+	actual := ListURL(EndpointClient())
+	expected := Endpoint + "v2.0/ports"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+	actual := GetURL(EndpointClient(), "foo")
+	expected := Endpoint + "v2.0/ports/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+	actual := CreateURL(EndpointClient())
+	expected := Endpoint + "v2.0/ports"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+	actual := UpdateURL(EndpointClient(), "foo")
+	expected := Endpoint + "v2.0/ports/foo"
+	th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+	actual := DeleteURL(EndpointClient(), "foo")
+	expected := Endpoint + "v2.0/ports/foo"
+	th.AssertEquals(t, expected, actual)
+}