os-networks extension

This commit adds the os-networks extention. This can be used to view
details about the nova-network-based networks that a tenant has access
to.
diff --git a/openstack/compute/v2/extensions/networks/doc.go b/openstack/compute/v2/extensions/networks/doc.go
new file mode 100644
index 0000000..fafe4a0
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/doc.go
@@ -0,0 +1,2 @@
+// Package network provides the ability to manage nova-networks
+package networks
diff --git a/openstack/compute/v2/extensions/networks/fixtures.go b/openstack/compute/v2/extensions/networks/fixtures.go
new file mode 100644
index 0000000..12b9485
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/fixtures.go
@@ -0,0 +1,209 @@
+// +build fixtures
+
+package networks
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+// ListOutput is a sample response to a List call.
+const ListOutput = `
+{
+    "networks": [
+        {
+            "bridge": "br100",
+            "bridge_interface": "eth0",
+            "broadcast": "10.0.0.7",
+            "cidr": "10.0.0.0/29",
+            "cidr_v6": null,
+            "created_at": "2011-08-15 06:19:19.387525",
+            "deleted": false,
+            "deleted_at": null,
+            "dhcp_start": "10.0.0.3",
+            "dns1": null,
+            "dns2": null,
+            "gateway": "10.0.0.1",
+            "gateway_v6": null,
+            "host": "nsokolov-desktop",
+            "id": "20c8acc0-f747-4d71-a389-46d078ebf047",
+            "injected": false,
+            "label": "mynet_0",
+            "multi_host": false,
+            "netmask": "255.255.255.248",
+            "netmask_v6": null,
+            "priority": null,
+            "project_id": "1234",
+            "rxtx_base": null,
+            "updated_at": "2011-08-16 09:26:13.048257",
+            "vlan": 100,
+            "vpn_private_address": "10.0.0.2",
+            "vpn_public_address": "127.0.0.1",
+            "vpn_public_port": 1000
+        },
+        {
+            "bridge": "br101",
+            "bridge_interface": "eth0",
+            "broadcast": "10.0.0.15",
+            "cidr": "10.0.0.10/29",
+            "cidr_v6": null,
+            "created_at": "2011-08-15 06:19:19.885495",
+            "deleted": false,
+            "deleted_at": null,
+            "dhcp_start": "10.0.0.11",
+            "dns1": null,
+            "dns2": null,
+            "gateway": "10.0.0.9",
+            "gateway_v6": null,
+            "host": null,
+            "id": "20c8acc0-f747-4d71-a389-46d078ebf000",
+            "injected": false,
+            "label": "mynet_1",
+            "multi_host": false,
+            "netmask": "255.255.255.248",
+            "netmask_v6": null,
+            "priority": null,
+            "project_id": null,
+            "rxtx_base": null,
+            "updated_at": null,
+            "vlan": 101,
+            "vpn_private_address": "10.0.0.10",
+            "vpn_public_address": null,
+            "vpn_public_port": 1001
+        }
+    ]
+}
+`
+
+// GetOutput is a sample response to a Get call.
+const GetOutput = `
+{
+    "network": {
+			"bridge": "br101",
+			"bridge_interface": "eth0",
+			"broadcast": "10.0.0.15",
+			"cidr": "10.0.0.10/29",
+			"cidr_v6": null,
+			"created_at": "2011-08-15 06:19:19.885495",
+			"deleted": false,
+			"deleted_at": null,
+			"dhcp_start": "10.0.0.11",
+			"dns1": null,
+			"dns2": null,
+			"gateway": "10.0.0.9",
+			"gateway_v6": null,
+			"host": null,
+			"id": "20c8acc0-f747-4d71-a389-46d078ebf000",
+			"injected": false,
+			"label": "mynet_1",
+			"multi_host": false,
+			"netmask": "255.255.255.248",
+			"netmask_v6": null,
+			"priority": null,
+			"project_id": null,
+			"rxtx_base": null,
+			"updated_at": null,
+			"vlan": 101,
+			"vpn_private_address": "10.0.0.10",
+			"vpn_public_address": null,
+			"vpn_public_port": 1001
+		}
+}
+`
+
+// FirstNetwork is the first result in ListOutput.
+var nilTime time.Time
+var FirstNetwork = Network{
+	Bridge:            "br100",
+	BridgeInterface:   "eth0",
+	Broadcast:         "10.0.0.7",
+	CIDR:              "10.0.0.0/29",
+	CIDRv6:            "",
+	CreatedAt:         time.Date(2011, 8, 15, 6, 19, 19, 387525000, time.UTC),
+	Deleted:           false,
+	DeletedAt:         nilTime,
+	DHCPStart:         "10.0.0.3",
+	DNS1:              "",
+	DNS2:              "",
+	Gateway:           "10.0.0.1",
+	Gatewayv6:         "",
+	Host:              "nsokolov-desktop",
+	ID:                "20c8acc0-f747-4d71-a389-46d078ebf047",
+	Injected:          false,
+	Label:             "mynet_0",
+	MultiHost:         false,
+	Netmask:           "255.255.255.248",
+	Netmaskv6:         "",
+	Priority:          0,
+	ProjectID:         "1234",
+	RXTXBase:          0,
+	UpdatedAt:         time.Date(2011, 8, 16, 9, 26, 13, 48257000, time.UTC),
+	VLAN:              100,
+	VPNPrivateAddress: "10.0.0.2",
+	VPNPublicAddress:  "127.0.0.1",
+	VPNPublicPort:     1000,
+}
+
+// SecondNetwork is the second result in ListOutput.
+var SecondNetwork = Network{
+	Bridge:            "br101",
+	BridgeInterface:   "eth0",
+	Broadcast:         "10.0.0.15",
+	CIDR:              "10.0.0.10/29",
+	CIDRv6:            "",
+	CreatedAt:         time.Date(2011, 8, 15, 6, 19, 19, 885495000, time.UTC),
+	Deleted:           false,
+	DeletedAt:         nilTime,
+	DHCPStart:         "10.0.0.11",
+	DNS1:              "",
+	DNS2:              "",
+	Gateway:           "10.0.0.9",
+	Gatewayv6:         "",
+	Host:              "",
+	ID:                "20c8acc0-f747-4d71-a389-46d078ebf000",
+	Injected:          false,
+	Label:             "mynet_1",
+	MultiHost:         false,
+	Netmask:           "255.255.255.248",
+	Netmaskv6:         "",
+	Priority:          0,
+	ProjectID:         "",
+	RXTXBase:          0,
+	UpdatedAt:         nilTime,
+	VLAN:              101,
+	VPNPrivateAddress: "10.0.0.10",
+	VPNPublicAddress:  "",
+	VPNPublicPort:     1001,
+}
+
+// ExpectedNetworkSlice is the slice of results that should be parsed
+// from ListOutput, in the expected order.
+var ExpectedNetworkSlice = []Network{FirstNetwork, SecondNetwork}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-networks", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, ListOutput)
+	})
+}
+
+// HandleGetSuccessfully configures the test server to respond to a Get request
+// for an existing network.
+func HandleGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/os-networks/20c8acc0-f747-4d71-a389-46d078ebf000", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, GetOutput)
+	})
+}
diff --git a/openstack/compute/v2/extensions/networks/requests.go b/openstack/compute/v2/extensions/networks/requests.go
new file mode 100644
index 0000000..eb20387
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/requests.go
@@ -0,0 +1,22 @@
+package networks
+
+import (
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// List returns a Pager that allows you to iterate over a collection of Network.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	url := listURL(client)
+	createPage := func(r pagination.PageResult) pagination.Page {
+		return NetworkPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, url, createPage)
+}
+
+// Get returns data about a previously created Network.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+	var res GetResult
+	_, res.Err = client.Get(getURL(client, id), &res.Body, nil)
+	return res
+}
diff --git a/openstack/compute/v2/extensions/networks/requests_test.go b/openstack/compute/v2/extensions/networks/requests_test.go
new file mode 100644
index 0000000..722b3f0
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/requests_test.go
@@ -0,0 +1,37 @@
+package networks
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListSuccessfully(t)
+
+	count := 0
+	err := List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractNetworks(page)
+		th.AssertNoErr(t, err)
+		th.CheckDeepEquals(t, ExpectedNetworkSlice, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, count)
+}
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGetSuccessfully(t)
+
+	actual, err := Get(client.ServiceClient(), "20c8acc0-f747-4d71-a389-46d078ebf000").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &SecondNetwork, actual)
+}
diff --git a/openstack/compute/v2/extensions/networks/results.go b/openstack/compute/v2/extensions/networks/results.go
new file mode 100644
index 0000000..55b361d
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/results.go
@@ -0,0 +1,222 @@
+package networks
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/rackspace/gophercloud"
+	"github.com/rackspace/gophercloud/pagination"
+)
+
+// A Network represents a nova-network that an instance communicates on
+type Network struct {
+	// The Bridge that VIFs on this network are connected to
+	Bridge string `mapstructure:"bridge"`
+
+	// BridgeInterface is what interface is connected to the Bridge
+	BridgeInterface string `mapstructure:"bridge_interface"`
+
+	// The Broadcast address of the network.
+	Broadcast string `mapstructure:"broadcast"`
+
+	// CIDR is the IPv4 subnet.
+	CIDR string `mapstructure:"cidr"`
+
+	// CIDRv6 is the IPv6 subnet.
+	CIDRv6 string `mapstructure:"cidr_v6"`
+
+	// CreatedAt is when the network was created..
+	CreatedAt time.Time `mapstructure:"-"`
+
+	// Deleted shows if the network has been deleted.
+	Deleted bool `mapstructure:"deleted"`
+
+	// DeletedAt is the time when the network was deleted.
+	DeletedAt time.Time `mapstructure:"-"`
+
+	// DHCPStart is the start of the DHCP address range.
+	DHCPStart string `mapstructure:"dhcp_start"`
+
+	// DNS1 is the first DNS server to use through DHCP.
+	DNS1 string `mapstructure:"dns_1"`
+
+	// DNS2 is the first DNS server to use through DHCP.
+	DNS2 string `mapstructure:"dns_2"`
+
+	// Gateway is the network gateway.
+	Gateway string `mapstructure:"gateway"`
+
+	// Gatewayv6 is the IPv6 network gateway.
+	Gatewayv6 string `mapstructure:"gateway_v6"`
+
+	// Host is the host that the network service is running on.
+	Host string `mapstructure:"host"`
+
+	// ID is the UUID of the network.
+	ID string `mapstructure:"id"`
+
+	// Injected determines if network information is injected into the host.
+	Injected bool `mapstructure:"injected"`
+
+	// Label is the common name that the network has..
+	Label string `mapstructure:"label"`
+
+	// MultiHost is if multi-host networking is enablec..
+	MultiHost bool `mapstructure:"multi_host"`
+
+	// Netmask is the network netmask.
+	Netmask string `mapstructure:"netmask"`
+
+	// Netmaskv6 is the IPv6 netmask.
+	Netmaskv6 string `mapstructure:"netmask_v6"`
+
+	// Priority is the network interface priority.
+	Priority int `mapstructure:"priority"`
+
+	// ProjectID is the project associated with this network.
+	ProjectID string `mapstructure:"project_id"`
+
+	// RXTXBase configures bandwidth entitlement.
+	RXTXBase int `mapstructure:"rxtx_base"`
+
+	// UpdatedAt is the time when the network was last updated.
+	UpdatedAt time.Time `mapstructure:"-"`
+
+	// VLAN is the vlan this network runs on.
+	VLAN int `mapstructure:"vlan"`
+
+	// VPNPrivateAddress is the private address of the CloudPipe VPN.
+	VPNPrivateAddress string `mapstructure:"vpn_private_address"`
+
+	// VPNPublicAddress is the public address of the CloudPipe VPN.
+	VPNPublicAddress string `mapstructure:"vpn_public_address"`
+
+	// VPNPublicPort is the port of the CloudPipe VPN.
+	VPNPublicPort int `mapstructure:"vpn_public_port"`
+}
+
+// NetworkPage stores a single, only page of Networks
+// results from a List call.
+type NetworkPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a NetworkPage is empty.
+func (page NetworkPage) IsEmpty() (bool, error) {
+	va, err := ExtractNetworks(page)
+	return len(va) == 0, err
+}
+
+// ExtractNetworks interprets a page of results as a slice of Networks
+func ExtractNetworks(page pagination.Page) ([]Network, error) {
+	var res struct {
+		Networks []Network `mapstructure:"networks"`
+	}
+
+	err := mapstructure.Decode(page.(NetworkPage).Body, &res)
+
+	var rawNetworks []interface{}
+	body := page.(NetworkPage).Body
+	switch body.(type) {
+	case map[string]interface{}:
+		rawNetworks = body.(map[string]interface{})["networks"].([]interface{})
+	case map[string][]interface{}:
+		rawNetworks = body.(map[string][]interface{})["networks"]
+	default:
+		return res.Networks, fmt.Errorf("Unknown type")
+	}
+
+	for i := range rawNetworks {
+		thisNetwork := rawNetworks[i].(map[string]interface{})
+		if t, ok := thisNetwork["created_at"].(string); ok && t != "" {
+			createdAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+			if err != nil {
+				return res.Networks, err
+			}
+			res.Networks[i].CreatedAt = createdAt
+		}
+
+		if t, ok := thisNetwork["updated_at"].(string); ok && t != "" {
+			updatedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+			if err != nil {
+				return res.Networks, err
+			}
+			res.Networks[i].UpdatedAt = updatedAt
+		}
+
+		if t, ok := thisNetwork["deleted_at"].(string); ok && t != "" {
+			deletedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+			if err != nil {
+				return res.Networks, err
+			}
+			res.Networks[i].DeletedAt = deletedAt
+		}
+	}
+
+	return res.Networks, err
+}
+
+type NetworkResult struct {
+	gophercloud.Result
+}
+
+// Extract is a method that attempts to interpret any Network resource
+// response as a Network struct.
+func (r NetworkResult) Extract() (*Network, error) {
+	if r.Err != nil {
+		return nil, r.Err
+	}
+
+	var res struct {
+		Network *Network `json:"network" mapstructure:"network"`
+	}
+
+	config := &mapstructure.DecoderConfig{
+		Result:           &res,
+		WeaklyTypedInput: true,
+	}
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := decoder.Decode(r.Body); err != nil {
+		return nil, err
+	}
+
+	b := r.Body.(map[string]interface{})["network"].(map[string]interface{})
+
+	if t, ok := b["created_at"].(string); ok && t != "" {
+		createdAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+		if err != nil {
+			return res.Network, err
+		}
+		res.Network.CreatedAt = createdAt
+	}
+
+	if t, ok := b["updated_at"].(string); ok && t != "" {
+		updatedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+		if err != nil {
+			return res.Network, err
+		}
+		res.Network.UpdatedAt = updatedAt
+	}
+
+	if t, ok := b["deleted_at"].(string); ok && t != "" {
+		deletedAt, err := time.Parse("2006-01-02 15:04:05.000000", t)
+		if err != nil {
+			return res.Network, err
+		}
+		res.Network.DeletedAt = deletedAt
+	}
+
+	return res.Network, err
+
+}
+
+// GetResult is the response from a Get operation. Call its Extract method to interpret it
+// as a Network.
+type GetResult struct {
+	NetworkResult
+}
diff --git a/openstack/compute/v2/extensions/networks/urls.go b/openstack/compute/v2/extensions/networks/urls.go
new file mode 100644
index 0000000..6966462
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/urls.go
@@ -0,0 +1,17 @@
+package networks
+
+import "github.com/rackspace/gophercloud"
+
+const resourcePath = "os-networks"
+
+func resourceURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL(resourcePath)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+	return resourceURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+	return c.ServiceURL(resourcePath, id)
+}
diff --git a/openstack/compute/v2/extensions/networks/urls_test.go b/openstack/compute/v2/extensions/networks/urls_test.go
new file mode 100644
index 0000000..be54c90
--- /dev/null
+++ b/openstack/compute/v2/extensions/networks/urls_test.go
@@ -0,0 +1,25 @@
+package networks
+
+import (
+	"testing"
+
+	th "github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+
+	th.CheckEquals(t, c.Endpoint+"os-networks", listURL(c))
+}
+
+func TestGetURL(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	c := client.ServiceClient()
+	id := "1"
+
+	th.CheckEquals(t, c.Endpoint+"os-networks/"+id, getURL(c, id))
+}