Merge pull request #378 from jrperritt/list-server-addresses

List Server Addresses; Closes #286
diff --git a/acceptance/openstack/compute/v2/compute_test.go b/acceptance/openstack/compute/v2/compute_test.go
index 33e49fe..c1bbf79 100644
--- a/acceptance/openstack/compute/v2/compute_test.go
+++ b/acceptance/openstack/compute/v2/compute_test.go
@@ -56,6 +56,9 @@
 	// FlavorIDResize contains the ID of a different flavor available on the same OpenStack installation, that is distinct
 	// from FlavorID.
 	FlavorIDResize string
+
+	// NetworkName is the name of a network to launch the instance on.
+	NetworkName string
 }
 
 // ComputeChoicesFromEnv populates a ComputeChoices struct from environment variables.
@@ -64,6 +67,7 @@
 	imageID := os.Getenv("OS_IMAGE_ID")
 	flavorID := os.Getenv("OS_FLAVOR_ID")
 	flavorIDResize := os.Getenv("OS_FLAVOR_ID_RESIZE")
+	networkName := os.Getenv("OS_NETWORK_NAME")
 
 	missing := make([]string, 0, 3)
 	if imageID == "" {
@@ -75,6 +79,9 @@
 	if flavorIDResize == "" {
 		missing = append(missing, "OS_FLAVOR_ID_RESIZE")
 	}
+	if networkName == "" {
+		networkName = "public"
+	}
 
 	notDistinct := ""
 	if flavorID == flavorIDResize {
@@ -93,5 +100,5 @@
 		return nil, fmt.Errorf(text)
 	}
 
-	return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize}, nil
+	return &ComputeChoices{ImageID: imageID, FlavorID: flavorID, FlavorIDResize: flavorIDResize, NetworkName: networkName}, nil
 }
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index d52a9d3..7b928e9 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -57,7 +57,6 @@
 	}
 
 	return openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
-		Name:   "neutron",
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 }
@@ -74,7 +73,10 @@
 		t.Fatalf("Unable to create a networking client: %v", err)
 	}
 
-	pager := networks.List(networkingClient, networks.ListOpts{Name: "public", Limit: 1})
+	pager := networks.List(networkingClient, networks.ListOpts{
+		Name:  choices.NetworkName,
+		Limit: 1,
+	})
 	pager.EachPage(func(page pagination.Page) (bool, error) {
 		networks, err := networks.ExtractNetworks(page)
 		if err != nil {
@@ -138,6 +140,32 @@
 	if err = waitForStatus(client, server, "ACTIVE"); err != nil {
 		t.Fatalf("Unable to wait for server: %v", err)
 	}
+
+	pager := servers.ListAddresses(client, server.ID)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		networks, err := servers.ExtractAddresses(page)
+		if err != nil {
+			return false, err
+		}
+
+		for n, a := range networks {
+			t.Logf("%s: %+v\n", n, a)
+		}
+		return true, nil
+	})
+
+	pager = servers.ListAddressesByNetwork(client, server.ID, choices.NetworkName)
+	pager.EachPage(func(page pagination.Page) (bool, error) {
+		addresses, err := servers.ExtractNetworkAddresses(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, a := range addresses {
+			t.Logf("%+v\n", a)
+		}
+		return true, nil
+	})
 }
 
 func TestUpdateServer(t *testing.T) {
diff --git a/openstack/compute/v2/servers/fixtures.go b/openstack/compute/v2/servers/fixtures.go
index 6125d53..e47bc0e 100644
--- a/openstack/compute/v2/servers/fixtures.go
+++ b/openstack/compute/v2/servers/fixtures.go
@@ -567,3 +567,87 @@
 		w.Write([]byte(`{ "metadata": {"foo":"baz", "this":"those"}}`))
 	})
 }
+
+// ListAddressesExpected represents an expected repsonse from a ListAddresses request.
+var ListAddressesExpected = map[string][]Address{
+	"public": []Address{
+		Address{
+			Version: 4,
+			Address: "80.56.136.39",
+		},
+		Address{
+			Version: 6,
+			Address: "2001:4800:790e:510:be76:4eff:fe04:82a8",
+		},
+	},
+	"private": []Address{
+		Address{
+			Version: 4,
+			Address: "10.880.3.154",
+		},
+	},
+}
+
+// HandleAddressListSuccessfully sets up the test server to respond to a ListAddresses request.
+func HandleAddressListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/asdfasdfasdf/ips", 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, `{
+			"addresses": {
+				"public": [
+				{
+					"version": 4,
+					"addr": "50.56.176.35"
+				},
+				{
+					"version": 6,
+					"addr": "2001:4800:780e:510:be76:4eff:fe04:84a8"
+				}
+				],
+				"private": [
+				{
+					"version": 4,
+					"addr": "10.180.3.155"
+				}
+				]
+			}
+		}`)
+	})
+}
+
+// ListNetworkAddressesExpected represents an expected repsonse from a ListAddressesByNetwork request.
+var ListNetworkAddressesExpected = []Address{
+	Address{
+		Version: 4,
+		Address: "50.56.176.35",
+	},
+	Address{
+		Version: 6,
+		Address: "2001:4800:780e:510:be76:4eff:fe04:84a8",
+	},
+}
+
+// HandleNetworkAddressListSuccessfully sets up the test server to respond to a ListAddressesByNetwork request.
+func HandleNetworkAddressListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/servers/asdfasdfasdf/ips/public", 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, `{
+			"public": [
+			{
+				"version": 4,
+				"addr": "50.56.176.35"
+				},
+				{
+					"version": 6,
+					"addr": "2001:4800:780e:510:be76:4eff:fe04:84a8"
+				}
+			]
+			}`)
+	})
+}
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index b7c1611..de112cf 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -719,3 +719,20 @@
 	})
 	return res
 }
+
+// ListAddresses makes a request against the API to list the servers IP addresses.
+func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return AddressPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, listAddressesURL(client, id), createPageFn)
+}
+
+// ListAddressesByNetwork makes a request against the API to list the servers IP addresses
+// for the given network.
+func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager {
+	createPageFn := func(r pagination.PageResult) pagination.Page {
+		return NetworkAddressPage{pagination.SinglePageBase(r)}
+	}
+	return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), createPageFn)
+}
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index 253a179..62b89e0 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -277,3 +277,51 @@
 	th.AssertNoErr(t, err)
 	th.AssertDeepEquals(t, expected, actual)
 }
+
+func TestListAddresses(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAddressListSuccessfully(t)
+
+	expected := ListAddressesExpected
+	pages := 0
+	err := ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 networks, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}
+
+func TestListAddressesByNetwork(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleNetworkAddressListSuccessfully(t)
+
+	expected := ListNetworkAddressesExpected
+	pages := 0
+	err := ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractNetworkAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 addresses, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index 1b22f21..e2be6ba 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -194,7 +194,6 @@
 
 	err = decoder.Decode(casted)
 
-	//err := mapstructure.Decode(casted, &response)
 	return response.Servers, err
 }
 
@@ -272,3 +271,77 @@
 	}
 	return data, nil
 }
+
+// Address represents an IP address.
+type Address struct {
+	Version int    `mapstructure:"version"`
+	Address string `mapstructure:"addr"`
+}
+
+// AddressPage abstracts the raw results of making a ListAddresses() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned
+// to the client, you may only safely access the data provided through the ExtractAddresses call.
+type AddressPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if an AddressPage contains no networks.
+func (r AddressPage) IsEmpty() (bool, error) {
+	addresses, err := ExtractAddresses(r)
+	if err != nil {
+		return true, err
+	}
+	return len(addresses) == 0, nil
+}
+
+// ExtractAddresses interprets the results of a single page from a ListAddresses() call,
+// producing a map of addresses.
+func ExtractAddresses(page pagination.Page) (map[string][]Address, error) {
+	casted := page.(AddressPage).Body
+
+	var response struct {
+		Addresses map[string][]Address `mapstructure:"addresses"`
+	}
+
+	err := mapstructure.Decode(casted, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response.Addresses, err
+}
+
+// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() request against the API.
+// As OpenStack extensions may freely alter the response bodies of structures returned
+// to the client, you may only safely access the data provided through the ExtractAddresses call.
+type NetworkAddressPage struct {
+	pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a NetworkAddressPage contains no addresses.
+func (r NetworkAddressPage) IsEmpty() (bool, error) {
+	addresses, err := ExtractNetworkAddresses(r)
+	if err != nil {
+		return true, err
+	}
+	return len(addresses) == 0, nil
+}
+
+// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call,
+// producing a slice of addresses.
+func ExtractNetworkAddresses(page pagination.Page) ([]Address, error) {
+	casted := page.(NetworkAddressPage).Body
+
+	var response map[string][]Address
+	err := mapstructure.Decode(casted, &response)
+	if err != nil {
+		return nil, err
+	}
+
+	var key string
+	for k := range response {
+		key = k
+	}
+
+	return response[key], err
+}
diff --git a/openstack/compute/v2/servers/urls.go b/openstack/compute/v2/servers/urls.go
index 4bc6586..8998354 100644
--- a/openstack/compute/v2/servers/urls.go
+++ b/openstack/compute/v2/servers/urls.go
@@ -37,3 +37,11 @@
 func metadataURL(client *gophercloud.ServiceClient, id string) string {
 	return client.ServiceURL("servers", id, "metadata")
 }
+
+func listAddressesURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("servers", id, "ips")
+}
+
+func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
+	return client.ServiceURL("servers", id, "ips", network)
+}
diff --git a/rackspace/compute/v2/servers/delegate.go b/rackspace/compute/v2/servers/delegate.go
index 173868e..8adfed5 100644
--- a/rackspace/compute/v2/servers/delegate.go
+++ b/rackspace/compute/v2/servers/delegate.go
@@ -64,3 +64,24 @@
 func ExtractServers(page pagination.Page) ([]os.Server, error) {
 	return os.ExtractServers(page)
 }
+
+// ListAddresses makes a request against the API to list the servers IP addresses.
+func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
+	return os.ListAddresses(client, id)
+}
+
+// ExtractAddresses interprets the results of a single page from a ListAddresses() call, producing a map of Address slices.
+func ExtractAddresses(page pagination.Page) (map[string][]os.Address, error) {
+	return os.ExtractAddresses(page)
+}
+
+// ListAddressesByNetwork makes a request against the API to list the servers IP addresses
+// for the given network.
+func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager {
+	return os.ListAddressesByNetwork(client, id, network)
+}
+
+// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, producing a map of Address slices.
+func ExtractNetworkAddresses(page pagination.Page) ([]os.Address, error) {
+	return os.ExtractNetworkAddresses(page)
+}
diff --git a/rackspace/compute/v2/servers/delegate_test.go b/rackspace/compute/v2/servers/delegate_test.go
index c3d9cc0..03e7ace 100644
--- a/rackspace/compute/v2/servers/delegate_test.go
+++ b/rackspace/compute/v2/servers/delegate_test.go
@@ -132,3 +132,51 @@
 	th.AssertNoErr(t, err)
 	th.CheckDeepEquals(t, &GophercloudServer, actual)
 }
+
+func TestListAddresses(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleAddressListSuccessfully(t)
+
+	expected := os.ListAddressesExpected
+	pages := 0
+	err := ListAddresses(client.ServiceClient(), "asdfasdfasdf").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 networks, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}
+
+func TestListAddressesByNetwork(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	os.HandleNetworkAddressListSuccessfully(t)
+
+	expected := os.ListNetworkAddressesExpected
+	pages := 0
+	err := ListAddressesByNetwork(client.ServiceClient(), "asdfasdfasdf", "public").EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := ExtractNetworkAddresses(page)
+		th.AssertNoErr(t, err)
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 addresses, got %d", len(actual))
+		}
+		th.CheckDeepEquals(t, expected, actual)
+
+		return true, nil
+	})
+	th.AssertNoErr(t, err)
+	th.CheckEquals(t, 1, pages)
+}