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