Merge "Nova Hypervisors API"
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")
+}