Nova Hypervisors API

Adds in support to list all available hypervisors (detailed) for applications
such as monitoring and capacity planning.

For #179

Fix more API weirdness

For some reason this morning storage space started to (in gophercloud only
not via curl) return sizes as scientific notation

Stylistic changes for jrperritt

Change to be a marker paged type

Revert to single page results

Our environment doesn't support marker pages, so go for the backwards compatible
option.

Adding Aggregate to hypervisors API

Change-Id: I191f5a45de29ee3eb7e7e6554dcb4614ed73b39a
diff --git a/openstack/compute/v2/hypervisors/doc.go b/openstack/compute/v2/hypervisors/doc.go
new file mode 100644
index 0000000..026f3dd
--- /dev/null
+++ b/openstack/compute/v2/hypervisors/doc.go
@@ -0,0 +1,3 @@
+// Package hypervisors gives information and control of the os-hypervisors
+// portion of the compute API
+package hypervisors
diff --git a/openstack/compute/v2/hypervisors/requests.go b/openstack/compute/v2/hypervisors/requests.go
new file mode 100644
index 0000000..3a66fcc
--- /dev/null
+++ b/openstack/compute/v2/hypervisors/requests.go
@@ -0,0 +1,21 @@
+package hypervisors
+
+import (
+	"github.com/gophercloud/gophercloud"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+// List makes a request against the API to list hypervisors.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, hypervisorsListDetailURL(client), func(r pagination.PageResult) pagination.Page {
+		return HypervisorPage{pagination.SinglePageBase(r)}
+	})
+}
+
+
+func AggregateList(client *gophercloud.ServiceClient) pagination.Pager {
+	return pagination.NewPager(client, aggregatesListURL(client), func(r pagination.PageResult) pagination.Page {
+		return AggregatePage{pagination.SinglePageBase(r)}
+	})
+}
+
diff --git a/openstack/compute/v2/hypervisors/results.go b/openstack/compute/v2/hypervisors/results.go
new file mode 100644
index 0000000..6a22517
--- /dev/null
+++ b/openstack/compute/v2/hypervisors/results.go
@@ -0,0 +1,180 @@
+package hypervisors
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/gophercloud/gophercloud/pagination"
+)
+
+type Topology struct {
+	Sockets int `json:"sockets"`
+	Cores   int `json:"cores"`
+	Threads int `json:"threads"`
+}
+
+type CPUInfo struct {
+	Vendor   string   `json:"vendor"`
+	Arch     string   `json:"arch"`
+	Model    string   `json:"model"`
+	Features []string `json:"features"`
+	Topology Topology `json:"topology"`
+}
+
+type Service struct {
+	Host           string `json:"host"`
+	ID             int    `json:"id"`
+	DisabledReason string `json:"disabled_reason"`
+}
+
+type Hypervisor struct {
+	// A structure that contains cpu information like arch, model, vendor, features and topology
+	CPUInfo CPUInfo `json:"-"`
+	// The current_workload is the number of tasks the hypervisor is responsible for.
+	// This will be equal or greater than the number of active VMs on the system
+	// (it can be greater when VMs are being deleted and the hypervisor is still cleaning up).
+	CurrentWorkload int `json:"current_workload"`
+	// Status of the hypervisor, either "enabled" or "disabled"
+	Status string `json:"status"`
+	// State of the hypervisor, either "up" or "down"
+	State string `json:"state"`
+	// Actual free disk on this hypervisor in GB
+	DiskAvailableLeast int `json:"disk_available_least"`
+	// The hypervisor's IP address
+	HostIP string `json:"host_ip"`
+	// The free disk remaining on this hypervisor in GB
+	FreeDiskGB int `json:"-"`
+	// The free RAM in this hypervisor in MB
+	FreeRamMB int `json:"free_ram_mb"`
+	// The hypervisor host name
+	HypervisorHostname string `json:"hypervisor_hostname"`
+	// The hypervisor type
+	HypervisorType string `json:"hypervisor_type"`
+	// The hypervisor version
+	HypervisorVersion int `json:"-"`
+	// Unique ID of the hypervisor
+	ID int `json:"id"`
+	// The disk in this hypervisor in GB
+	LocalGB int `json:"-"`
+	// The disk used in this hypervisor in GB
+	LocalGBUsed int `json:"local_gb_used"`
+	// The memory of this hypervisor in MB
+	MemoryMB int `json:"memory_mb"`
+	// The memory used in this hypervisor in MB
+	MemoryMBUsed int `json:"memory_mb_used"`
+	// The number of running vms on this hypervisor
+	RunningVMs int `json:"running_vms"`
+	// The hypervisor service object
+	Service Service `json:"service"`
+	// The number of vcpu in this hypervisor
+	VCPUs int `json:"vcpus"`
+	// The number of vcpu used in this hypervisor
+	VCPUsUsed int `json:"vcpus_used"`
+}
+
+type Aggregate struct{
+	Name string `json:name`
+        Hosts []string `json:hosts`
+	ID int `json:id`
+
+}
+
+func (r *Hypervisor) UnmarshalJSON(b []byte) error {
+
+	type tmp Hypervisor
+	var s *struct {
+		tmp
+		CPUInfo           interface{} `json:"cpu_info"`
+		HypervisorVersion interface{} `json:"hypervisor_version"`
+		FreeDiskGB        interface{} `json:"free_disk_gb"`
+		LocalGB           interface{} `json:"local_gb"`
+	}
+
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Hypervisor(s.tmp)
+
+	// Newer versions pass the CPU into around as the correct types, this just needs
+	// converting and copying into place. Older versions pass CPU info around as a string
+	// and can simply be unmarshalled by the json parser
+	var tmpb []byte;
+
+	switch t := s.CPUInfo.(type) {
+	case string:
+		tmpb = []byte(t)
+	case map[string]interface{}:
+		tmpb, err = json.Marshal(t)
+		if err != nil {
+			return err
+		}
+	default:
+		return fmt.Errorf("CPUInfo has unexpected type: %T", t)
+	}
+
+	err = json.Unmarshal(tmpb, &r.CPUInfo)
+	if err != nil {
+		return err
+	}
+
+	// These fields may be passed in in scientific notation
+	switch t := s.HypervisorVersion.(type) {
+	case int:
+		r.HypervisorVersion = t
+	case float64:
+		r.HypervisorVersion = int(t)
+	default:
+		return fmt.Errorf("Hypervisor version of unexpected type")
+	}
+
+	switch t := s.FreeDiskGB.(type) {
+	case int:
+		r.FreeDiskGB = t
+	case float64:
+		r.FreeDiskGB = int(t)
+	default:
+		return fmt.Errorf("Free disk GB of unexpected type")
+	}
+
+	switch t := s.LocalGB.(type) {
+	case int:
+		r.LocalGB = t
+	case float64:
+		r.LocalGB = int(t)
+	default:
+		return fmt.Errorf("Local GB of unexpected type")
+	}
+
+	return nil
+}
+
+type HypervisorPage struct {
+	pagination.SinglePageBase
+}
+
+func (page HypervisorPage) IsEmpty() (bool, error) {
+        va, err := ExtractHypervisors(page)
+        return len(va) == 0, err
+}
+
+func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) {
+	var h struct {
+		Hypervisors []Hypervisor `json:"hypervisors"`
+	}
+	err := (p.(HypervisorPage)).ExtractInto(&h)
+	return h.Hypervisors, err
+}
+
+func ExtractAggregates(p pagination.Page) ([]Aggregate, error) {
+	var h struct {
+		Aggregates []Aggregate `json:"aggregates"`
+	}
+	fmt.Printf("AA: %s\n", p)
+	err := (p.(AggregatePage)).ExtractInto(&h)
+	return h.Aggregates, err
+}
+
+type AggregatePage struct {
+       pagination.SinglePageBase
+}
diff --git a/openstack/compute/v2/hypervisors/testing/fixtures.go b/openstack/compute/v2/hypervisors/testing/fixtures.go
new file mode 100644
index 0000000..1dd0058
--- /dev/null
+++ b/openstack/compute/v2/hypervisors/testing/fixtures.go
@@ -0,0 +1,135 @@
+package testing
+
+import (
+	"fmt"
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/hypervisors"
+	"github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+	"net/http"
+	"testing"
+)
+
+// The first hypervisor represents what the specification says (~Newton)
+// The second is exactly the same, but what you can get off a real system (~Kilo)
+const HypervisorListBody = `
+{
+    "hypervisors": [
+        {
+            "cpu_info": {
+                "arch": "x86_64",
+                "model": "Nehalem",
+                "vendor": "Intel",
+                "features": [
+                    "pge",
+                    "clflush"
+                ],
+                "topology": {
+                    "cores": 1,
+                    "threads": 1,
+                    "sockets": 4
+                }
+            },
+            "current_workload": 0,
+            "status": "enabled",
+            "state": "up",
+            "disk_available_least": 0,
+            "host_ip": "1.1.1.1",
+            "free_disk_gb": 1028,
+            "free_ram_mb": 7680,
+            "hypervisor_hostname": "fake-mini",
+            "hypervisor_type": "fake",
+            "hypervisor_version": 2002000,
+            "id": 1,
+            "local_gb": 1028,
+            "local_gb_used": 0,
+            "memory_mb": 8192,
+            "memory_mb_used": 512,
+            "running_vms": 0,
+            "service": {
+                "host": "e6a37ee802d74863ab8b91ade8f12a67",
+                "id": 2,
+                "disabled_reason": null
+            },
+            "vcpus": 1,
+            "vcpus_used": 0
+        },
+        {
+            "cpu_info": "{\"arch\": \"x86_64\", \"model\": \"Nehalem\", \"vendor\": \"Intel\", \"features\": [\"pge\", \"clflush\"], \"topology\": {\"cores\": 1, \"threads\": 1, \"sockets\": 4}}",
+            "current_workload": 0,
+            "status": "enabled",
+            "state": "up",
+            "disk_available_least": 0,
+            "host_ip": "1.1.1.1",
+            "free_disk_gb": 1028,
+            "free_ram_mb": 7680,
+            "hypervisor_hostname": "fake-mini",
+            "hypervisor_type": "fake",
+            "hypervisor_version": 2.002e+06,
+            "id": 1,
+            "local_gb": 1028,
+            "local_gb_used": 0,
+            "memory_mb": 8192,
+            "memory_mb_used": 512,
+            "running_vms": 0,
+            "service": {
+                "host": "e6a37ee802d74863ab8b91ade8f12a67",
+                "id": 2,
+                "disabled_reason": null
+            },
+            "vcpus": 1,
+            "vcpus_used": 0
+        }
+    ]
+}`
+
+var (
+	HypervisorFake = hypervisors.Hypervisor{
+		CPUInfo: hypervisors.CPUInfo{
+			Arch:   "x86_64",
+			Model:  "Nehalem",
+			Vendor: "Intel",
+			Features: []string{
+				"pge",
+				"clflush",
+			},
+			Topology: hypervisors.Topology{
+				Cores:   1,
+				Threads: 1,
+				Sockets: 4,
+			},
+		},
+		CurrentWorkload:    0,
+		Status:             "enabled",
+		State:              "up",
+		DiskAvailableLeast: 0,
+		HostIP:             "1.1.1.1",
+		FreeDiskGB:         1028,
+		FreeRamMB:          7680,
+		HypervisorHostname: "fake-mini",
+		HypervisorType:     "fake",
+		HypervisorVersion:  2002000,
+		ID:                 1,
+		LocalGB:            1028,
+		LocalGBUsed:        0,
+		MemoryMB:           8192,
+		MemoryMBUsed:       512,
+		RunningVMs:         0,
+		Service: hypervisors.Service{
+			Host:           "e6a37ee802d74863ab8b91ade8f12a67",
+			ID:             2,
+			DisabledReason: "",
+		},
+		VCPUs:     1,
+		VCPUsUsed: 0,
+	}
+)
+
+func HandleHypervisorListSuccessfully(t *testing.T) {
+	testhelper.Mux.HandleFunc("/os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, HypervisorListBody)
+	})
+}
diff --git a/openstack/compute/v2/hypervisors/testing/requests_test.go b/openstack/compute/v2/hypervisors/testing/requests_test.go
new file mode 100644
index 0000000..14b0ebe
--- /dev/null
+++ b/openstack/compute/v2/hypervisors/testing/requests_test.go
@@ -0,0 +1,52 @@
+package testing
+
+import (
+	"github.com/gophercloud/gophercloud/openstack/compute/v2/hypervisors"
+	"github.com/gophercloud/gophercloud/pagination"
+	"github.com/gophercloud/gophercloud/testhelper"
+	"github.com/gophercloud/gophercloud/testhelper/client"
+	"testing"
+)
+
+func TestListHypervisors(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	HandleHypervisorListSuccessfully(t)
+
+	pages := 0
+	err := hypervisors.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := hypervisors.ExtractHypervisors(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 hypervisors, got %d", len(actual))
+		}
+		testhelper.CheckDeepEquals(t, HypervisorFake, actual[0])
+		testhelper.CheckDeepEquals(t, HypervisorFake, actual[1])
+
+		return true, nil
+	})
+
+	testhelper.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestListAllHypervisors(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+	HandleHypervisorListSuccessfully(t)
+
+	allPages, err := hypervisors.List(client.ServiceClient()).AllPages()
+	testhelper.AssertNoErr(t, err)
+	actual, err := hypervisors.ExtractHypervisors(allPages)
+	testhelper.AssertNoErr(t, err)
+	testhelper.CheckDeepEquals(t, HypervisorFake, actual[0])
+	testhelper.CheckDeepEquals(t, HypervisorFake, actual[1])
+}
diff --git a/openstack/compute/v2/hypervisors/urls.go b/openstack/compute/v2/hypervisors/urls.go
new file mode 100644
index 0000000..0cb3518
--- /dev/null
+++ b/openstack/compute/v2/hypervisors/urls.go
@@ -0,0 +1,11 @@
+package hypervisors
+
+import "github.com/gophercloud/gophercloud"
+
+func hypervisorsListDetailURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("os-hypervisors", "detail")
+}
+
+func aggregatesListURL(c *gophercloud.ServiceClient) string {
+	return c.ServiceURL("os-aggregates")
+}