fix(vmware): send VI JSON _typeName on data-object arguments

The vSphere VI JSON API documents _typeName on request data objects. Calls whose only argument is a bare ManagedObjectReference (QueryPerfProviderSummary, QueryAvailablePerfMetric) succeed against vCenter 8.0.3, but every call that passes a composite data object (PerfQuerySpec/PerfMetricId for QueryPerf, EventFilterSpec/EventFilterSpecByEntity for QueryEvents) is rejected with HTTP 500. Issue #2070 shows exactly that split, so add the discriminators the documented schema carries to those request bodies. Morefs now carry _typeName too, matching the same schema. Focused tests pin the serialized request bodies.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-20 01:47:42 +01:00
parent 9444da374f
commit 5da761f968
3 changed files with 130 additions and 13 deletions
+20 -10
View File
@@ -38,6 +38,7 @@ type viJSONPerfProviderSummary struct {
}
type viJSONPerfMetricID struct {
TypeName string `json:"_typeName,omitempty"`
CounterID int `json:"counterId"`
Instance string `json:"instance"`
}
@@ -232,9 +233,10 @@ func (c *Client) queryPerfProviderSummary(
var summary viJSONPerfProviderSummary
path := fmt.Sprintf("/sdk/vim25/%s/PerformanceManager/%s/QueryPerfProviderSummary", release, perfManagerMoID)
body := map[string]any{
"entity": map[string]string{
"type": strings.TrimSpace(entityType),
"value": strings.TrimSpace(entityMoID),
"entity": viJSONReference{
TypeName: "ManagedObjectReference",
Type: strings.TrimSpace(entityType),
Value: strings.TrimSpace(entityMoID),
},
}
if err := c.postVIJSONJSON(ctx, sessionID, path, "vmware performance provider summary", body, &summary); err != nil {
@@ -254,9 +256,10 @@ func (c *Client) queryAvailablePerfMetrics(
) ([]viJSONPerfMetricID, error) {
path := fmt.Sprintf("/sdk/vim25/%s/PerformanceManager/%s/QueryAvailablePerfMetric", release, perfManagerMoID)
body := map[string]any{
"entity": map[string]string{
"type": strings.TrimSpace(entityType),
"value": strings.TrimSpace(entityMoID),
"entity": viJSONReference{
TypeName: "ManagedObjectReference",
Type: strings.TrimSpace(entityType),
Value: strings.TrimSpace(entityMoID),
},
"intervalId": intervalID,
}
@@ -278,15 +281,22 @@ func (c *Client) queryPerfMetrics(
metricIDs []viJSONPerfMetricID,
) ([]viJSONPerfEntityMetric, error) {
path := fmt.Sprintf("/sdk/vim25/%s/PerformanceManager/%s/QueryPerf", release, perfManagerMoID)
metricRequests := make([]viJSONPerfMetricID, len(metricIDs))
for i, metricID := range metricIDs {
metricID.TypeName = "PerfMetricId"
metricRequests[i] = metricID
}
body := map[string]any{
"querySpec": []map[string]any{{
"entity": map[string]string{
"type": strings.TrimSpace(entityType),
"value": strings.TrimSpace(entityMoID),
"_typeName": "PerfQuerySpec",
"entity": viJSONReference{
TypeName: "ManagedObjectReference",
Type: strings.TrimSpace(entityType),
Value: strings.TrimSpace(entityMoID),
},
"intervalId": intervalID,
"maxSample": 1,
"metricId": metricIDs,
"metricId": metricRequests,
}},
}
var result []viJSONPerfEntityMetric
+12 -3
View File
@@ -11,9 +11,14 @@ import (
const vmwareSignalEnrichmentConcurrency = 8
// viJSONReference is a ManagedObjectReference. TypeName carries the VI JSON
// API _typeName discriminator; the documented request schema includes it, and
// the API rejects a composite data object that omits its discriminator while
// accepting a bare moref without one.
type viJSONReference struct {
Type string `json:"type"`
Value string `json:"value"`
TypeName string `json:"_typeName,omitempty"`
Type string `json:"type"`
Value string `json:"value"`
}
type viJSONLocalizedMessage struct {
@@ -39,11 +44,13 @@ type viJSONTaskInfo struct {
}
type viJSONEventFilterSpec struct {
TypeName string `json:"_typeName,omitempty"`
Entity *viJSONEventFilterEntity `json:"entity,omitempty"`
MaxCount int `json:"maxCount,omitempty"`
}
type viJSONEventFilterEntity struct {
TypeName string `json:"_typeName,omitempty"`
Entity viJSONReference `json:"entity"`
Recursion string `json:"recursion,omitempty"`
}
@@ -530,8 +537,10 @@ func (c *Client) collectRecentEvents(ctx context.Context, release, sessionID, ev
path := fmt.Sprintf("/sdk/vim25/%s/EventManager/%s/QueryEvents", release, eventManagerMoID)
body := map[string]any{
"filter": viJSONEventFilterSpec{
TypeName: "EventFilterSpec",
Entity: &viJSONEventFilterEntity{
Entity: viJSONReference{Type: strings.TrimSpace(managedType), Value: strings.TrimSpace(managedObjectID)},
TypeName: "EventFilterSpecByEntity",
Entity: viJSONReference{TypeName: "ManagedObjectReference", Type: strings.TrimSpace(managedType), Value: strings.TrimSpace(managedObjectID)},
Recursion: "self",
},
MaxCount: 3,
@@ -0,0 +1,98 @@
package vmware
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// The vSphere VI JSON API documents a `_typeName` discriminator on request data
// objects. A bare ManagedObjectReference is accepted without one, but the
// composite argument objects (PerfQuerySpec, PerfMetricId, EventFilterSpec and
// its nested EventFilterSpecByEntity) are rejected with HTTP 500 when the
// discriminator is absent. These tests pin the exact request bodies that
// vCenter 8.0.3 rejected for issue #2070.
func newVIJSONRequestClient(t *testing.T, handler http.Handler) (*Client, func()) {
t.Helper()
server := httptest.NewTLSServer(handler)
client, err := NewClient(ClientConfig{
Host: server.URL,
Username: "admin",
Password: "secret",
InsecureSkipVerify: true,
Timeout: 5 * time.Second,
})
if err != nil {
server.Close()
t.Fatalf("NewClient: %v", err)
}
return client, server.Close
}
func TestQueryPerfRequestCarriesTypeNameDiscriminators(t *testing.T) {
var raw []byte
mux := http.NewServeMux()
mux.HandleFunc("/sdk/vim25/9.0.0.0/PerformanceManager/PerformanceManager/QueryPerf", func(w http.ResponseWriter, r *http.Request) {
raw, _ = io.ReadAll(r.Body)
writeJSON(w, []map[string]any{})
})
client, closeServer := newVIJSONRequestClient(t, mux)
defer closeServer()
_, err := client.queryPerfMetrics(
context.Background(),
"9.0.0.0",
"vi-session",
"PerformanceManager",
"HostSystem",
"host-101",
20,
[]viJSONPerfMetricID{{CounterID: 1, Instance: ""}},
)
if err != nil {
t.Fatalf("queryPerfMetrics: %v", err)
}
body := string(raw)
for _, want := range []string{
`"_typeName":"PerfQuerySpec"`,
`"_typeName":"ManagedObjectReference"`,
`"_typeName":"PerfMetricId"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("QueryPerf request %s missing %s", body, want)
}
}
}
func TestQueryEventsRequestCarriesTypeNameDiscriminators(t *testing.T) {
var raw []byte
mux := http.NewServeMux()
mux.HandleFunc("/sdk/vim25/9.0.0.0/EventManager/EventManager/QueryEvents", func(w http.ResponseWriter, r *http.Request) {
raw, _ = io.ReadAll(r.Body)
writeJSON(w, []map[string]any{})
})
client, closeServer := newVIJSONRequestClient(t, mux)
defer closeServer()
_, err := client.collectRecentEvents(context.Background(), "9.0.0.0", "vi-session", "EventManager", "Network", "network-14")
if err != nil {
t.Fatalf("collectRecentEvents: %v", err)
}
body := string(raw)
for _, want := range []string{
`"_typeName":"EventFilterSpec"`,
`"_typeName":"EventFilterSpecByEntity"`,
`"_typeName":"ManagedObjectReference"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("QueryEvents request %s missing %s", body, want)
}
}
}