Repository: gophercloud/gophercloud

Issue:
Compute: Get Hypervisor with server list
### What is missing?

The Show Hypervisor Details api call introduce a query parameter `with_servers` in microversion 2.53.
See: https://docs.openstack.org/api-ref/compute/#show-hypervisor-details

It is supported in List but not the `Get` request.



Discussion:

Solution patch:
diff --git a/openstack/compute/v2/hypervisors/requests.go b/openstack/compute/v2/hypervisors/requests.go
index b17dd3578..2483d7340 100644
--- a/openstack/compute/v2/hypervisors/requests.go
+++ b/openstack/compute/v2/hypervisors/requests.go
@@ -66,9 +66,42 @@ func GetStatistics(ctx context.Context, client *gophercloud.ServiceClient) (r St
 	return
 }
 
+// GetOptsBuilder allows extensions to add additional parameters to the
+// Get request.
+type GetOptsBuilder interface {
+	ToHypervisorGetQuery() (string, error)
+}
+
+// GetOpts allows the opt-in to add the servers to the response
+type GetOpts struct {
+	// WithServers is a bool to include all servers which belong to the hypervisor
+	// This requires microversion 2.53 or later
+	WithServers *bool `q:"with_servers"`
+}
+
+// ToHypervisorGetQuery formats a GetOpts into a query string.
+func (opts GetOpts) ToHypervisorGetQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
 // Get makes a request against the API to get details for specific hypervisor.
 func Get(ctx context.Context, client *gophercloud.ServiceClient, hypervisorID string) (r HypervisorResult) {
-	resp, err := client.Get(ctx, hypervisorsGetURL(client, hypervisorID), &r.Body, &gophercloud.RequestOpts{
+	return GetExt(ctx, client, hypervisorID, nil)
+}
+
+// Show makes a request against the API to get details for specific hypervisor with optional query parameters
+func GetExt(ctx context.Context, client *gophercloud.ServiceClient, hypervisorID string, opts GetOptsBuilder) (r HypervisorResult) {
+	url := hypervisorsGetURL(client, hypervisorID)
+	if opts != nil {
+		query, err := opts.ToHypervisorGetQuery()
+		if err != nil {
+			return HypervisorResult{gophercloud.Result{Err: err}}
+		}
+		url += query
+	}
+
+	resp, err := client.Get(ctx, url, &r.Body, &gophercloud.RequestOpts{
 		OkCodes: []int{200},
 	})
 	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)



Test patch:
diff --git a/openstack/compute/v2/hypervisors/testing/fixtures_test.go b/openstack/compute/v2/hypervisors/testing/fixtures_test.go
index ebfd816c0..166c41510 100644
--- a/openstack/compute/v2/hypervisors/testing/fixtures_test.go
+++ b/openstack/compute/v2/hypervisors/testing/fixtures_test.go
@@ -329,6 +329,62 @@ const HypervisorGetBody = `
 }
 `
 
+// HypervisorGetPost253Body represents a raw hypervisor GET result with Pike+
+// release with optional server list
+const HypervisorGetPost253Body = `
+{
+    "hypervisor":{
+        "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",
+        "servers": [
+            {
+                "name": "test_server1",
+                "uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
+            },
+            {
+                "name": "test_server2",
+                "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
+            }
+        ],
+        "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":"c48f6247-abe4-4a24-824e-ea39e108874f",
+        "local_gb":1028,
+        "local_gb_used":0,
+        "memory_mb":8192,
+        "memory_mb_used":512,
+        "running_vms":2,
+        "service":{
+            "host":"e6a37ee802d74863ab8b91ade8f12a67",
+            "id":"9c2566e7-7a54-4777-a1ae-c2662f0c407c",
+            "disabled_reason":null
+        },
+        "vcpus":1,
+        "vcpus_used":0
+    }
+}
+`
+
 // HypervisorGetEmptyCPUInfoBody represents a raw hypervisor GET result with
 // no cpu_info
 const HypervisorGetEmptyCPUInfoBody = `
@@ -487,6 +543,56 @@ var (
 		VCPUsUsed: 0,
 	}
 
+	HypervisorFakeWithServers = 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",
+		Servers: &[]hypervisors.Server{
+			{
+				Name: "test_server1",
+				UUID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
+			},
+			{
+				Name: "test_server2",
+				UUID: "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
+			},
+		},
+		DiskAvailableLeast: 0,
+		HostIP:             "1.1.1.1",
+		FreeDiskGB:         1028,
+		FreeRamMB:          7680,
+		HypervisorHostname: "fake-mini",
+		HypervisorType:     "fake",
+		HypervisorVersion:  2002000,
+		ID:                 "c48f6247-abe4-4a24-824e-ea39e108874f",
+		LocalGB:            1028,
+		LocalGBUsed:        0,
+		MemoryMB:           8192,
+		MemoryMBUsed:       512,
+		RunningVMs:         2,
+		Service: hypervisors.Service{
+			Host:           "e6a37ee802d74863ab8b91ade8f12a67",
+			ID:             "9c2566e7-7a54-4777-a1ae-c2662f0c407c",
+			DisabledReason: "",
+		},
+		VCPUs:     1,
+		VCPUsUsed: 0,
+	}
+
 	HypervisorFakeWithParameters = hypervisors.Hypervisor{
 		CPUInfo: hypervisors.CPUInfo{
 			Arch:   "x86_64",
@@ -676,6 +782,20 @@ func HandleHypervisorGetSuccessfully(t *testing.T, fakeServer th.FakeServer) {
 	})
 }
 
+func HandleHypervisorGetPost253Successfully(t *testing.T, fakeServer th.FakeServer) {
+	fakeServer.Mux.HandleFunc("/os-hypervisors/"+HypervisorFake.ID, 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")
+		if r.URL.Query().Get("with_servers") == "true" {
+			fmt.Fprint(w, HypervisorGetPost253Body)
+		} else {
+			fmt.Fprint(w, HypervisorGetBody)
+		}
+	})
+}
+
 func HandleHypervisorGetEmptyCPUInfoSuccessfully(t *testing.T, fakeServer th.FakeServer) {
 	fakeServer.Mux.HandleFunc("/os-hypervisors/"+HypervisorFake.ID, func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
diff --git a/openstack/compute/v2/hypervisors/testing/requests_test.go b/openstack/compute/v2/hypervisors/testing/requests_test.go
index a2b2183e4..c6c7d9141 100644
--- a/openstack/compute/v2/hypervisors/testing/requests_test.go
+++ b/openstack/compute/v2/hypervisors/testing/requests_test.go
@@ -135,6 +135,18 @@ func TestGetHypervisor(t *testing.T) {
 	th.CheckDeepEquals(t, &expected, actual)
 }
 
+func TestGetWithServersHypervisor(t *testing.T) {
+	fakeServer := th.SetupHTTP()
+	defer fakeServer.Teardown()
+	HandleHypervisorGetPost253Successfully(t, fakeServer)
+
+	expected := HypervisorFakeWithServers
+	withServers := true
+	actual, err := hypervisors.GetExt(context.TODO(), client.ServiceClient(fakeServer), expected.ID, hypervisors.GetOpts{WithServers: &withServers}).Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, &expected, actual)
+}
+
 func TestGetHypervisorEmptyCPUInfo(t *testing.T) {
 	fakeServer := th.SetupHTTP()
 	defer fakeServer.Teardown()