master
go 416 lines 13.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package vsphere
4
5 import (
6 "context"
7 "encoding/json"
8 "os"
9 "path/filepath"
10 "testing"
11
12 topologyv1 "github.com/netdata/netdata/go/plugins/pkg/topology/v1"
13 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
14
15 "github.com/santhosh-tekuri/jsonschema/v6"
16 "github.com/stretchr/testify/require"
17 )
18
19 func TestFuncTopology_Handle(t *testing.T) {
20 tests := map[string]struct {
21 method string
22 collector func() *Collector
23 want int
24 check func(*testing.T, any)
25 }{
26 "without discovery": {
27 method: "topology:vsphere",
28 collector: New,
29 want: 503,
30 },
31 "with empty inventory cache": {
32 method: "topology:vsphere",
33 collector: func() *Collector {
34 collr := New()
35 collr.resources = &rs.Resources{}
36 return collr
37 },
38 want: 200,
39 check: func(t *testing.T, raw any) {
40 data, ok := raw.(topologyv1.Data)
41 require.True(t, ok)
42 validateVSphereTopologyV1Data(t, data)
43 require.Equal(t, 0, data.Actors.Rows)
44 require.Equal(t, 0, data.Links.Rows)
45 require.Nil(t, data.Overlays)
46 require.EqualValues(t, 0, data.Stats["hosts"])
47 require.EqualValues(t, 0, data.Stats["vms"])
48 require.EqualValues(t, 0, data.Stats["overlay_refs"])
49 },
50 },
51 "unknown method": {
52 method: "unknown",
53 collector: New,
54 want: 404,
55 },
56 }
57
58 for name, tc := range tests {
59 t.Run(name, func(t *testing.T) {
60 handler := &funcTopology{collector: tc.collector(), agentID: "vsphere_vcenter1", jobName: "vsphere_vcenter1"}
61
62 resp := handler.Handle(context.Background(), tc.method, nil)
63
64 require.Equal(t, tc.want, resp.Status)
65 if tc.check != nil {
66 tc.check(t, resp.Data)
67 }
68 })
69 }
70 }
71
72 func newVSphereTopologyTestCollector() *Collector {
73 collr := New()
74 collr.resources = &rs.Resources{
75 DataCenters: rs.DataCenters{
76 "datacenter-1": {ID: "datacenter-1", Name: "DC1"},
77 },
78 Clusters: rs.Clusters{
79 "domain-c1": {
80 ID: "domain-c1",
81 Name: "Cluster1",
82 Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
83 OverallStatus: "green",
84 DrsEnabled: true,
85 HaEnabled: true,
86 },
87 },
88 Hosts: rs.Hosts{
89 "host-1": {
90 ID: "host-1",
91 Name: "Host1",
92 Hier: rs.HostHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}},
93 ConnectionState: "connected",
94 PowerState: "poweredOn",
95 OverallStatus: "green",
96 },
97 },
98 VMs: rs.VMs{
99 "vm-1": {
100 ID: "vm-1",
101 Name: "VM1",
102 Hier: rs.VMHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}, Host: rs.HierarchyValue{ID: "host-1", Name: "Host1"}},
103 ConnectionState: "connected",
104 PowerState: "poweredOn",
105 OverallStatus: "green",
106 SnapshotCount: 2,
107 SnapshotMaxChainDepth: 3,
108 },
109 },
110 Datastores: rs.Datastores{
111 "datastore-1": {
112 ID: "datastore-1",
113 Name: "Datastore1",
114 Hier: rs.DatastoreHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
115 Type: "VMFS",
116 Accessible: true,
117 MaintenanceMode: "normal",
118 Capacity: 1000,
119 FreeSpace: 400,
120 },
121 },
122 Networks: rs.Networks{
123 "network-1": {
124 ID: "network-1",
125 Name: "VM Network",
126 Type: "Network",
127 Hier: rs.NetworkHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
128 Accessible: true,
129 HostIDs: []string{"host-1"},
130 VMIDs: []string{"vm-1"},
131 OverallStatus: "green",
132 },
133 },
134 StoragePods: rs.StoragePods{
135 "group-p1": {
136 ID: "group-p1",
137 Name: "Pod1",
138 Hier: rs.StoragePodHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}},
139 StorageDRSEnabled: new(true),
140 },
141 },
142 ResourcePools: rs.ResourcePools{
143 "resgroup-1": {
144 ID: "resgroup-1",
145 Name: "Resources",
146 Hier: rs.ResourcePoolHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}},
147 OverallStatus: "green",
148 },
149 },
150 }
151 return collr
152 }
153
154 func TestFuncTopology_HandleWithInventoryCache(t *testing.T) {
155 collr := newVSphereTopologyTestCollector()
156 handler := &funcTopology{collector: collr, agentID: "vsphere_vcenter1", jobName: "vsphere_vcenter1"}
157
158 resp := handler.Handle(context.Background(), "topology:vsphere", nil)
159
160 require.Equal(t, 200, resp.Status)
161 require.Equal(t, "topology", resp.ResponseType)
162 data, ok := resp.Data.(topologyv1.Data)
163 require.True(t, ok)
164 validateVSphereTopologyV1Data(t, data)
165 require.Equal(t, topologyv1.SchemaVersion, data.SchemaVersion)
166 require.Equal(t, "vsphere", data.Producer.Source)
167 require.Equal(t, "go.d/vsphere", data.Producer.Plugin)
168 require.Equal(t, "vsphere_vcenter1", data.Producer.Instance)
169 require.Equal(t, "virtualization", data.Types.ActorTypes["vsphere_vm"].Layer)
170 require.Equal(t, 8, data.Actors.Rows)
171 require.Equal(t, 9, data.Links.Rows)
172 require.EqualValues(t, 1, data.Stats["overlay_refs"])
173
174 actors := topologyTableRows(t, data.Actors, data.Dictionaries)
175 requireTopologyRow(t, actors, "vsphere_moid", "datacenter-1")
176 requireTopologyRow(t, actors, "vsphere_moid", "domain-c1")
177 requireTopologyRow(t, actors, "vsphere_moid", "host-1")
178 vm := requireTopologyRow(t, actors, "vsphere_moid", "vm-1")
179 datastore := requireTopologyRow(t, actors, "vsphere_moid", "datastore-1")
180 network := requireTopologyRow(t, actors, "vsphere_moid", "network-1")
181 require.Equal(t, "vsphere_vm", vm["type"])
182 require.Equal(t, "vm", vm["object_type"])
183 require.Equal(t, "VM1", vm["name"])
184 require.Equal(t, "network", network["object_type"])
185 require.Equal(t, "VM Network", network["name"])
186
187 require.NotNil(t, data.Tables)
188 details := topologyTableRows(t, data.Tables.Actor[vsphereTopologyDetailTable].Table, data.Dictionaries)
189 vmDetail := requireTopologyRow(t, details, "vsphere_moid", "vm-1")
190 require.EqualValues(t, 2, vmDetail["snapshot_count"])
191 require.EqualValues(t, 3, vmDetail["snapshot_chain_depth"])
192
193 links := topologyTableRows(t, data.Links, data.Dictionaries)
194 keys := topologyLinkKeys(t, actors, links)
195 require.Contains(t, keys, "datacenter:datacenter-1->cluster:domain-c1:vsphere_ownership")
196 require.Contains(t, keys, "cluster:domain-c1->host:host-1:vsphere_ownership")
197 require.Contains(t, keys, "vm:vm-1->host:host-1:vsphere_runs_on")
198 require.Contains(t, keys, "host:host-1->network:network-1:vsphere_connected_to")
199 require.Contains(t, keys, "vm:vm-1->network:network-1:vsphere_connected_to")
200
201 evidence := topologyTableRows(t, data.Evidence[vsphereRunsOnEvidenceType].Table, data.Dictionaries)
202 runEvidence := requireTopologyRow(t, evidence, "relationship", "runs_on")
203 require.Equal(t, "vm", runEvidence["source_type"])
204 require.Equal(t, "vm-1", runEvidence["source_moid"])
205 require.Equal(t, "host", runEvidence["target_type"])
206 require.Equal(t, "host-1", runEvidence["target_moid"])
207
208 template := data.Types.OverlayTemplates[vsphereDatastoreUtilizationOverlay]
209 require.Equal(t, topologyv1.OverlayProviderNetdataMetrics, template.Provider)
210 require.Equal(t, []string{"vsphere.datastore_space_utilization"}, template.Contexts)
211 require.Equal(t, []string{"used"}, template.Dimensions)
212 require.Equal(t, []string{vsphereOverlaySelectorCollectJob, vsphereOverlaySelectorID}, template.SelectorParams)
213 require.Equal(t, topologyv1.NewOverlayMerge(topologyv1.OverlayMergeRefsSet, topologyv1.OverlayMergeValuesLast), template.Merge)
214
215 require.NotNil(t, data.Overlays)
216 require.NotNil(t, data.Overlays.Refs)
217 refs := topologyTableRows(t, *data.Overlays.Refs, data.Dictionaries)
218 ref := requireTopologyRow(t, refs, vsphereOverlaySelectorID, "datastore-1")
219 require.Equal(t, vsphereDatastoreUtilizationOverlay, ref[topologyv1.OverlayRefsTemplateColumn])
220 require.Equal(t, topologyIntValue(t, datastore["_row"]), topologyIntValue(t, ref[topologyv1.OverlayRefsActorColumn]))
221 require.Equal(t, "vsphere_vcenter1", ref[vsphereOverlaySelectorCollectJob])
222 }
223
224 func TestFuncTopology_DoesNotLinkToFilteredActors(t *testing.T) {
225 collr := New()
226 collr.resources = &rs.Resources{
227 DataCenters: rs.DataCenters{
228 "datacenter-1": {ID: "datacenter-1", Name: "DC1"},
229 },
230 Clusters: rs.Clusters{},
231 Hosts: rs.Hosts{
232 "host-1": {
233 ID: "host-1",
234 Name: "Host1",
235 Hier: rs.HostHierarchy{
236 DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"},
237 Cluster: rs.HierarchyValue{ID: "domain-c-filtered", Name: "Filtered"},
238 },
239 },
240 },
241 VMs: rs.VMs{
242 "vm-1": {
243 ID: "vm-1",
244 Name: "VM1",
245 Hier: rs.VMHierarchy{
246 DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"},
247 Cluster: rs.HierarchyValue{ID: "domain-c-filtered", Name: "Filtered"},
248 Host: rs.HierarchyValue{ID: "host-filtered", Name: "FilteredHost"},
249 },
250 },
251 },
252 }
253
254 data, ok := collr.topologyData("agent", "job")
255
256 require.True(t, ok)
257 validateVSphereTopologyV1Data(t, data)
258 require.Equal(t, 3, data.Actors.Rows)
259 actors := topologyTableRows(t, data.Actors, data.Dictionaries)
260 keys := topologyLinkKeys(t, actors, topologyTableRows(t, data.Links, data.Dictionaries))
261 require.Contains(t, keys, "datacenter:datacenter-1->host:host-1:vsphere_ownership")
262 require.Contains(t, keys, "datacenter:datacenter-1->vm:vm-1:vsphere_ownership")
263 require.NotContains(t, keys, "cluster:domain-c-filtered->host:host-1:vsphere_ownership")
264 require.NotContains(t, keys, "vm:vm-1->host:host-filtered:vsphere_runs_on")
265 }
266
267 func TestFuncTopology_SanitizesOverlayCollectJobLikeChartLabels(t *testing.T) {
268 collr := newVSphereTopologyTestCollector()
269
270 data, ok := collr.topologyData("vsphere_vcenter1", "job'\nname\x00")
271
272 require.True(t, ok)
273 validateVSphereTopologyV1Data(t, data)
274 require.Equal(t, "vsphere_vcenter1", data.Producer.Instance)
275 require.NotNil(t, data.Overlays)
276 require.NotNil(t, data.Overlays.Refs)
277 refs := topologyTableRows(t, *data.Overlays.Refs, data.Dictionaries)
278 ref := requireTopologyRow(t, refs, vsphereOverlaySelectorID, "datastore-1")
279 require.Equal(t, "job name", ref[vsphereOverlaySelectorCollectJob])
280 }
281
282 func validateVSphereTopologyV1Data(t *testing.T, data topologyv1.Data) {
283 t.Helper()
284
285 payload, err := json.Marshal(data)
286 require.NoError(t, err)
287
288 var decodedData map[string]any
289 require.NoError(t, json.Unmarshal(payload, &decodedData))
290 require.NoError(t, topologyv1.ValidateDecodedData(decodedData))
291
292 schemaBytes, err := os.ReadFile(filepath.Clean(filepath.Join("..", "..", "..", "..", "..", "plugins.d", "FUNCTION_TOPOLOGY_SCHEMA.json")))
293 require.NoError(t, err)
294
295 var schemaDoc any
296 require.NoError(t, json.Unmarshal(schemaBytes, &schemaDoc))
297
298 compiler := jsonschema.NewCompiler()
299 require.NoError(t, compiler.AddResource("schema.json", schemaDoc))
300 schema, err := compiler.Compile("schema.json")
301 require.NoError(t, err)
302
303 response := map[string]any{
304 "status": 200,
305 "type": topologyv1.ResponseType,
306 "data": decodedData,
307 }
308 require.NoError(t, schema.Validate(response))
309 }
310
311 func topologyTableRows(t *testing.T, table topologyv1.Table, dictionaries topologyv1.Dictionaries) []map[string]any {
312 t.Helper()
313
314 rows := make([]map[string]any, table.Rows)
315 for row := range rows {
316 rows[row] = map[string]any{"_row": row}
317 }
318
319 for columnIndex, column := range table.Columns {
320 values := topologyColumnValues(t, table.Rows, table.Values[columnIndex])
321 for rowIndex, value := range values {
322 if column.Type == "string_ref" && value != nil {
323 value = resolveTopologyStringRef(t, dictionaries, column.Dictionary, value)
324 }
325 rows[rowIndex][column.ID] = value
326 }
327 }
328 return rows
329 }
330
331 func topologyColumnValues(t *testing.T, rows int, encoding topologyv1.ColumnEncoding) []any {
332 t.Helper()
333
334 switch value := encoding.(type) {
335 case topologyv1.ConstEncoding:
336 values := make([]any, rows)
337 for i := range values {
338 values[i] = value.Value
339 }
340 return values
341 case topologyv1.ValuesEncoding:
342 require.Len(t, value.Values, rows)
343 return value.Values
344 case topologyv1.DictEncoding:
345 require.Len(t, value.Indexes, rows)
346 values := make([]any, rows)
347 for i, index := range value.Indexes {
348 require.GreaterOrEqual(t, index, 0)
349 require.Less(t, index, len(value.Values))
350 values[i] = value.Values[index]
351 }
352 return values
353 default:
354 t.Fatalf("unsupported topology column encoding %T", encoding)
355 return nil
356 }
357 }
358
359 func resolveTopologyStringRef(t *testing.T, dictionaries topologyv1.Dictionaries, name string, value any) string {
360 t.Helper()
361
362 index, ok := value.(int)
363 require.True(t, ok)
364 values := dictionaries[name]
365 require.Less(t, index, len(values))
366 text, ok := values[index].(string)
367 require.True(t, ok)
368 return text
369 }
370
371 func requireTopologyRow(t *testing.T, rows []map[string]any, key string, value any) map[string]any {
372 t.Helper()
373
374 for _, row := range rows {
375 if row[key] == value {
376 return row
377 }
378 }
379 t.Fatalf("topology row with %s=%v not found", key, value)
380 return nil
381 }
382
383 func topologyLinkKeys(t *testing.T, actors, links []map[string]any) map[string]struct{} {
384 t.Helper()
385
386 actorNames := make(map[int]string, len(actors))
387 for _, actor := range actors {
388 row, ok := actor["_row"].(int)
389 require.True(t, ok)
390 actorNames[row] = actor["object_type"].(string) + ":" + actor["vsphere_moid"].(string)
391 }
392
393 keys := make(map[string]struct{}, len(links))
394 for _, link := range links {
395 src := topologyIntValue(t, link["src_actor"])
396 dst := topologyIntValue(t, link["dst_actor"])
397 keys[actorNames[src]+"->"+actorNames[dst]+":"+link["type"].(string)] = struct{}{}
398 }
399 return keys
400 }
401
402 func topologyIntValue(t *testing.T, value any) int {
403 t.Helper()
404
405 switch v := value.(type) {
406 case int:
407 return v
408 case int64:
409 return int(v)
410 case uint64:
411 return int(v)
412 default:
413 t.Fatalf("unexpected integer value %T", value)
414 return 0
415 }
416 }