master
go 658 lines 19.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package discover
4
5 import (
6 "crypto/tls"
7 "errors"
8 "net/url"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14 "github.com/vmware/govmomi/performance"
15 "github.com/vmware/govmomi/simulator"
16 "github.com/vmware/govmomi/vim25/mo"
17 "github.com/vmware/govmomi/vim25/types"
18
19 "github.com/netdata/netdata/go/plugins/logger"
20 "github.com/netdata/netdata/go/plugins/pkg/matcher"
21 "github.com/netdata/netdata/go/plugins/pkg/tlscfg"
22 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/client"
23 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
24 )
25
26 func TestDiscoverer_Discover(t *testing.T) {
27 d, _, teardown := prepareDiscovererSim(t)
28 defer teardown()
29
30 res, err := d.Discover()
31
32 require.NoError(t, err)
33 assert.True(t, len(res.DataCenters) > 0)
34 assert.True(t, len(res.Folders) > 0)
35 assert.True(t, len(res.Clusters) > 0)
36 assert.True(t, len(res.Hosts) > 0)
37 assert.True(t, len(res.VMs) > 0)
38 assert.True(t, len(res.Datastores) > 0)
39 assert.True(t, isHierarchySet(res))
40 assert.True(t, isMetricListsCollected(res))
41 }
42
43 func TestDiscoverer_DiscoverNetworkTopologyOptIn(t *testing.T) {
44 d, _, teardown := prepareDiscovererSim(t)
45 defer teardown()
46 d.CollectNetworkTopology = true
47
48 res, err := d.Discover()
49
50 require.NoError(t, err)
51 require.NotEmpty(t, res.Networks)
52 assert.True(t, isHierarchySet(res))
53 }
54
55 func TestDiscoverer_networkPathSetIncludesOverallStatus(t *testing.T) {
56 assert.Contains(t, Discoverer{}.networkPathSet(), "overallStatus")
57 }
58
59 func TestDiscoverer_DiscoverFailSoftOptionalSurfaces(t *testing.T) {
60 tests := map[string]struct {
61 setup func(*Discoverer)
62 check func(*testing.T, *rs.Resources)
63 }{
64 "network topology": {
65 setup: func(d *Discoverer) {
66 d.CollectNetworkTopology = true
67 d.Client = networksErrorClient{Client: d.Client}
68 },
69 check: func(t *testing.T, res *rs.Resources) {
70 require.Empty(t, res.Networks)
71 assert.NotEmpty(t, res.Datastores)
72 },
73 },
74 "datastore clusters": {
75 setup: func(d *Discoverer) {
76 d.CollectDatastoreClusters = true
77 d.Client = storagePodsErrorClient{Client: d.Client}
78 },
79 check: func(t *testing.T, res *rs.Resources) {
80 require.Empty(t, res.StoragePods)
81 assert.NotEmpty(t, res.Datastores)
82 },
83 },
84 "custom attributes": {
85 setup: func(d *Discoverer) {
86 d.Client = customFieldsErrorClient{Client: d.Client}
87 d.CustomAttributeMatcher = matcher.TRUE()
88 },
89 },
90 "tags": {
91 setup: func(d *Discoverer) {
92 d.Client = tagsByRefErrorClient{Client: d.Client}
93 d.TagCategoryMatcher = matcher.TRUE()
94 },
95 },
96 }
97
98 for name, tc := range tests {
99 t.Run(name, func(t *testing.T) {
100 d, _, teardown := prepareDiscovererSim(t)
101 defer teardown()
102 tc.setup(d)
103
104 res, err := d.Discover()
105
106 require.NoError(t, err)
107 require.NotNil(t, res)
108 assert.NotEmpty(t, res.Hosts)
109 assert.NotEmpty(t, res.VMs)
110 if tc.check != nil {
111 tc.check(t, res)
112 }
113 })
114 }
115 }
116
117 func TestDiscoverer_discover(t *testing.T) {
118 d, model, teardown := prepareDiscovererSim(t)
119 defer teardown()
120
121 raw, err := d.discover()
122
123 require.NoError(t, err)
124 count := model.Count()
125 assert.Lenf(t, raw.dcs, count.Datacenter, "datacenters")
126 assert.Lenf(t, raw.folders, count.Folder-1, "folders") // minus root folder
127 dummyClusters := model.Host * count.Datacenter
128 assert.Lenf(t, raw.clusters, count.Cluster+dummyClusters, "clusters")
129 assert.Lenf(t, raw.hosts, count.Host, "hosts")
130 assert.Lenf(t, raw.vms, count.Machine, "vms")
131 assert.Lenf(t, raw.datastores, count.Datastore, "datastores")
132 }
133
134 func TestDiscoverer_build(t *testing.T) {
135 d, _, teardown := prepareDiscovererSim(t)
136 defer teardown()
137
138 raw, err := d.discover()
139 require.NoError(t, err)
140
141 res := d.build(raw)
142
143 assert.Lenf(t, res.DataCenters, len(raw.dcs), "datacenters")
144 assert.Lenf(t, res.Folders, len(raw.folders), "folders")
145 assert.Lenf(t, res.Clusters, len(raw.clusters), "clusters")
146 assert.Lenf(t, res.Hosts, len(raw.hosts), "hosts")
147 assert.Lenf(t, res.VMs, len(raw.vms), "vms")
148 assert.Lenf(t, res.Datastores, len(raw.datastores), "datastores")
149 }
150
151 func TestResourceBuildersSkipMissingParent(t *testing.T) {
152 tests := map[string]struct {
153 build func() any
154 }{
155 "folder": {build: func() any { return newFolder(mo.Folder{}) }},
156 "cluster": {build: func() any { return newCluster(mo.ComputeResource{}) }},
157 "host": {build: func() any { return newHost(mo.HostSystem{}) }},
158 "datastore": {build: func() any { return newDatastore(mo.Datastore{}) }},
159 "storage pod": {build: func() any { return newStoragePod(mo.StoragePod{}) }},
160 }
161
162 for name, tc := range tests {
163 t.Run(name, func(t *testing.T) {
164 assert.Nil(t, tc.build())
165 })
166 }
167 }
168
169 func TestDiscoverer_buildHostsKeepsNonPoweredHosts(t *testing.T) {
170 raw := []mo.HostSystem{
171 {
172 ManagedEntity: mo.ManagedEntity{
173 ExtensibleManagedObject: mo.ExtensibleManagedObject{
174 Self: types.ManagedObjectReference{Type: "HostSystem", Value: "host-1"},
175 },
176 Parent: &types.ManagedObjectReference{Type: "ComputeResource", Value: "domain-c1"},
177 Name: "host1",
178 },
179 Runtime: types.HostRuntimeInfo{PowerState: types.HostSystemPowerStatePoweredOn},
180 },
181 {
182 ManagedEntity: mo.ManagedEntity{
183 ExtensibleManagedObject: mo.ExtensibleManagedObject{
184 Self: types.ManagedObjectReference{Type: "HostSystem", Value: "host-2"},
185 },
186 Parent: &types.ManagedObjectReference{Type: "ComputeResource", Value: "domain-c1"},
187 Name: "host2",
188 },
189 Runtime: types.HostRuntimeInfo{
190 ConnectionState: types.HostSystemConnectionStateNotResponding,
191 PowerState: types.HostSystemPowerStatePoweredOff,
192 InMaintenanceMode: true,
193 },
194 },
195 }
196
197 hosts := Discoverer{}.buildHosts(raw)
198 assert.NotNil(t, hosts.Get("host-1"))
199 assert.NotNil(t, hosts.Get("host-2"))
200 host := hosts.Get("host-2")
201 require.NotNil(t, host)
202 assert.Equal(t, "poweredOff", host.PowerState)
203 assert.Equal(t, "notResponding", host.ConnectionState)
204 assert.True(t, host.InMaintenanceMode)
205 }
206
207 func TestDiscoverer_buildVMsKeepsNonPoweredVMsAndNilHost(t *testing.T) {
208 raw := []mo.VirtualMachine{
209 {
210 ManagedEntity: mo.ManagedEntity{
211 ExtensibleManagedObject: mo.ExtensibleManagedObject{
212 Self: types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-1"},
213 },
214 Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-v1"},
215 Name: "vm1",
216 },
217 Runtime: types.VirtualMachineRuntimeInfo{PowerState: types.VirtualMachinePowerStatePoweredOn},
218 },
219 {
220 ManagedEntity: mo.ManagedEntity{
221 ExtensibleManagedObject: mo.ExtensibleManagedObject{
222 Self: types.ManagedObjectReference{Type: "VirtualMachine", Value: "vm-2"},
223 },
224 Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-v1"},
225 Name: "vm2",
226 CustomValue: []types.BaseCustomFieldValue{
227 &types.CustomFieldStringValue{
228 CustomFieldValue: types.CustomFieldValue{Key: 7},
229 Value: "owner-a",
230 },
231 },
232 },
233 Runtime: types.VirtualMachineRuntimeInfo{
234 ConnectionState: types.VirtualMachineConnectionStateInaccessible,
235 PowerState: types.VirtualMachinePowerStatePoweredOff,
236 ConsolidationNeeded: true,
237 },
238 Summary: types.VirtualMachineSummary{
239 Guest: &types.VirtualMachineGuestSummary{
240 ToolsRunningStatus: string(types.VirtualMachineToolsRunningStatusGuestToolsRunning),
241 ToolsVersionStatus2: string(types.VirtualMachineToolsVersionStatusGuestToolsTooOld),
242 },
243 Config: types.VirtualMachineConfigSummary{
244 NumCpu: 4,
245 MemorySizeMB: 8192,
246 NumVirtualDisks: 2,
247 NumEthernetCards: 3,
248 },
249 Storage: &types.VirtualMachineStorageSummary{
250 Committed: 100,
251 Uncommitted: 200,
252 Unshared: 300,
253 },
254 },
255 },
256 }
257 hostRef := types.ManagedObjectReference{Type: "HostSystem", Value: "host-1"}
258 raw[0].Runtime.Host = &hostRef
259
260 vms := Discoverer{}.buildVMs(raw)
261 assert.NotNil(t, vms.Get("vm-1"))
262 vm := vms.Get("vm-2")
263 require.NotNil(t, vm)
264 assert.Empty(t, vm.ParentID)
265 assert.Equal(t, "group-v1", vm.FolderParentID)
266 assert.Equal(t, "poweredOff", vm.PowerState)
267 assert.Equal(t, "inaccessible", vm.ConnectionState)
268 assert.Equal(t, string(types.VirtualMachineToolsRunningStatusGuestToolsRunning), vm.ToolsRunningStatus)
269 assert.Equal(t, string(types.VirtualMachineToolsVersionStatusGuestToolsTooOld), vm.ToolsVersionStatus)
270 assert.True(t, vm.ConsolidationNeeded)
271 assert.EqualValues(t, 4, vm.ConfigCPU)
272 assert.EqualValues(t, 8192, vm.ConfigMemory)
273 assert.EqualValues(t, 2, vm.ConfigDisks)
274 assert.EqualValues(t, 3, vm.ConfigNICs)
275 assert.EqualValues(t, 100, vm.StorageCommitted)
276 assert.EqualValues(t, 200, vm.StorageUncommitted)
277 assert.EqualValues(t, 300, vm.StorageUnshared)
278 assert.Equal(t, map[int32]string{7: "owner-a"}, vm.CustomValues)
279 }
280
281 func TestDiscoverer_buildDatastoresKeepsInaccessible(t *testing.T) {
282 yes := true
283 raw := []mo.Datastore{
284 {
285 ManagedEntity: mo.ManagedEntity{
286 ExtensibleManagedObject: mo.ExtensibleManagedObject{
287 Self: types.ManagedObjectReference{Type: "Datastore", Value: "datastore-1"},
288 },
289 Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-s1"},
290 Name: "Datastore1",
291 OverallStatus: types.ManagedEntityStatusGray,
292 },
293 Summary: types.DatastoreSummary{
294 Capacity: 1000,
295 FreeSpace: 400,
296 Uncommitted: 250,
297 Accessible: false,
298 MultipleHostAccess: &yes,
299 Type: "VMFS",
300 MaintenanceMode: string(types.DatastoreSummaryMaintenanceModeStateInMaintenance),
301 },
302 },
303 }
304
305 datastores := Discoverer{}.buildDatastores(raw)
306
307 ds := datastores.Get("datastore-1")
308 require.NotNil(t, ds)
309 assert.False(t, ds.Accessible)
310 assert.EqualValues(t, 250, ds.Uncommitted)
311 assert.Equal(t, string(types.DatastoreSummaryMaintenanceModeStateInMaintenance), ds.MaintenanceMode)
312 require.NotNil(t, ds.MultipleHostAccess)
313 assert.True(t, *ds.MultipleHostAccess)
314 }
315
316 func TestNewNetwork(t *testing.T) {
317 network := newNetwork(mo.Network{
318 ManagedEntity: mo.ManagedEntity{
319 ExtensibleManagedObject: mo.ExtensibleManagedObject{
320 Self: types.ManagedObjectReference{Type: "Network", Value: "network-1"},
321 },
322 Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-n1"},
323 OverallStatus: types.ManagedEntityStatusGreen,
324 },
325 Name: "VM Network",
326 Summary: &types.NetworkSummary{
327 Accessible: true,
328 IpPoolName: "pool1",
329 },
330 Host: []types.ManagedObjectReference{{Type: "HostSystem", Value: "host-1"}},
331 Vm: []types.ManagedObjectReference{{Type: "VirtualMachine", Value: "vm-1"}},
332 })
333
334 require.NotNil(t, network)
335 assert.Equal(t, "network-1", network.ID)
336 assert.Equal(t, "VM Network", network.Name)
337 assert.Equal(t, "Network", network.Type)
338 assert.Equal(t, "group-n1", network.ParentID)
339 assert.True(t, network.Accessible)
340 assert.Equal(t, "pool1", network.IPPoolName)
341 assert.Equal(t, "green", network.OverallStatus)
342 assert.Equal(t, []string{"host-1"}, network.HostIDs)
343 assert.Equal(t, []string{"vm-1"}, network.VMIDs)
344 }
345
346 func TestNewStoragePod(t *testing.T) {
347 pod := newStoragePod(mo.StoragePod{
348 Folder: mo.Folder{
349 ManagedEntity: mo.ManagedEntity{
350 ExtensibleManagedObject: mo.ExtensibleManagedObject{
351 Self: types.ManagedObjectReference{Type: "StoragePod", Value: "group-p1"},
352 },
353 Parent: &types.ManagedObjectReference{Type: "Folder", Value: "group-s1"},
354 Name: "DC0_POD0",
355 OverallStatus: types.ManagedEntityStatusYellow,
356 },
357 },
358 Summary: &types.StoragePodSummary{
359 Capacity: 1000,
360 FreeSpace: 400,
361 },
362 PodStorageDrsEntry: &types.PodStorageDrsEntry{
363 StorageDrsConfig: types.StorageDrsConfigInfo{
364 PodConfig: types.StorageDrsPodConfigInfo{Enabled: true},
365 },
366 },
367 })
368
369 require.NotNil(t, pod)
370 assert.Equal(t, "group-p1", pod.ID)
371 assert.Equal(t, "DC0_POD0", pod.Name)
372 assert.Equal(t, "group-s1", pod.ParentID)
373 assert.EqualValues(t, 1000, pod.Capacity)
374 assert.EqualValues(t, 400, pod.FreeSpace)
375 require.NotNil(t, pod.StorageDRSEnabled)
376 assert.True(t, *pod.StorageDRSEnabled)
377 assert.Equal(t, "yellow", pod.OverallStatus)
378 }
379
380 func TestDiscoverer_setHierarchy(t *testing.T) {
381 d, _, teardown := prepareDiscovererSim(t)
382 defer teardown()
383
384 raw, err := d.discover()
385 require.NoError(t, err)
386 res := d.build(raw)
387
388 err = d.setHierarchy(res)
389
390 require.NoError(t, err)
391 assert.True(t, isHierarchySet(res))
392 }
393
394 func TestDiscoverer_setVMHierarchyUsesFolderDatacenterWhenHostMissing(t *testing.T) {
395 res := &rs.Resources{
396 DataCenters: rs.DataCenters{
397 "datacenter-1": &rs.Datacenter{ID: "datacenter-1", Name: "DC1"},
398 },
399 Folders: rs.Folders{
400 "group-v1": &rs.Folder{ID: "group-v1", ParentID: "datacenter-1", Name: "vm"},
401 },
402 Hosts: rs.Hosts{},
403 VMs: rs.VMs{
404 "vm-1": &rs.VM{ID: "vm-1", FolderParentID: "group-v1"},
405 },
406 }
407
408 assert.True(t, setVMHierarchy(res.VMs.Get("vm-1"), res))
409 assert.Equal(t, "DC1", res.VMs.Get("vm-1").Hier.DC.Name)
410 assert.Empty(t, res.VMs.Get("vm-1").Hier.Cluster.Name)
411 assert.Empty(t, res.VMs.Get("vm-1").Hier.Host.Name)
412 }
413
414 func TestFindFolderRootIDStopsOnCycles(t *testing.T) {
415 folders := rs.Folders{
416 "group-v1": &rs.Folder{ID: "group-v1", ParentID: "group-v2", Name: "vm"},
417 "group-v2": &rs.Folder{ID: "group-v2", ParentID: "group-v1", Name: "nested"},
418 }
419
420 assert.Empty(t, findVMDcID("group-v1", folders))
421 }
422
423 func TestDiscoverer_removeUnmatched(t *testing.T) {
424 d, _, teardown := prepareDiscovererSim(t)
425 defer teardown()
426
427 d.HostMatcher = falseHostMatcher{}
428 d.VMMatcher = falseVMMatcher{}
429 raw, err := d.discover()
430 require.NoError(t, err)
431 res := d.build(raw)
432
433 numClusters := len(res.Clusters)
434 numVMs, numHosts := len(res.VMs), len(res.Hosts)
435 removed := d.removeUnmatched(res)
436 // dummy clusters are removed + all hosts/VMs rejected by matchers
437 remainingClusters := len(res.Clusters)
438 assert.Equal(t, (numClusters-remainingClusters)+numVMs+numHosts, removed)
439 assert.Lenf(t, res.Hosts, 0, "hosts")
440 assert.Lenf(t, res.VMs, 0, "vms")
441 }
442
443 func TestDiscoverer_removeUnmatchedStoragePods(t *testing.T) {
444 pods := rs.StoragePods{
445 "group-p1": &rs.StoragePod{ID: "group-p1"},
446 "group-p2": &rs.StoragePod{ID: "group-p2"},
447 }
448 d := Discoverer{DatastoreClusterMatcher: falseStoragePodMatcher{}}
449
450 removed := d.removeUnmatchedStoragePods(pods)
451
452 require.Equal(t, 2, removed)
453 require.Empty(t, pods)
454 }
455
456 func TestDiscoverer_collectMetricLists(t *testing.T) {
457 d, _, teardown := prepareDiscovererSim(t)
458 defer teardown()
459
460 raw, err := d.discover()
461 require.NoError(t, err)
462
463 res := d.build(raw)
464 err = d.collectMetricLists(res)
465
466 require.NoError(t, err)
467 assert.True(t, isMetricListsCollected(res))
468 }
469
470 func TestDiscoverer_warnMissingMetricCountersInitializesWarningMap(t *testing.T) {
471 d := &Discoverer{Logger: logger.New()}
472
473 d.warnMissingMetricCounters(map[string]*types.PerfCounterInfo{})
474
475 require.NotNil(t, d.missingPerfCounterWarnings)
476 require.NotEmpty(t, d.missingPerfCounterWarnings)
477 }
478
479 func TestSimpleMetricListIncludesPowerMetrics(t *testing.T) {
480 tests := map[string]struct {
481 counters map[string]*types.PerfCounterInfo
482 build func(map[string]*types.PerfCounterInfo) performance.MetricList
483 wantLen int
484 }{
485 "host": {
486 counters: map[string]*types.PerfCounterInfo{
487 "cpu.usage.average": {Key: 1},
488 "power.power.average": {Key: 2},
489 "power.powerCap.average": {Key: 3},
490 "power.energy.summation": {Key: 4},
491 "power.capacity.usage.average": {Key: 5},
492 "power.capacity.usagePct.average": {Key: 6},
493 "power.capacity.usageIdle.average": {Key: 7},
494 "power.capacity.usageSystem.average": {Key: 8},
495 "power.capacity.usageVm.average": {Key: 9},
496 },
497 build: simpleHostMetricList,
498 wantLen: 9,
499 },
500 "VM": {
501 counters: map[string]*types.PerfCounterInfo{
502 "cpu.usage.average": {Key: 1},
503 "power.power.average": {Key: 2},
504 "power.energy.summation": {Key: 3},
505 },
506 build: simpleVMMetricList,
507 wantLen: 3,
508 },
509 }
510
511 for name, tc := range tests {
512 t.Run(name, func(t *testing.T) {
513 ml := tc.build(tc.counters)
514
515 require.Len(t, ml, tc.wantLen)
516 for _, metric := range ml {
517 assert.Empty(t, metric.Instance)
518 }
519 })
520 }
521 }
522
523 func TestExpectedMetricCounterNamesSkipsOptionalCounters(t *testing.T) {
524 names := expectedMetricCounterNames()
525
526 assert.Contains(t, names, "cpu.usage.average")
527 for name, counter := range map[string]string{
528 "host power": "power.power.average",
529 "host energy": "power.energy.summation",
530 "cluster DRS score": "clusterServices.clusterDrsScore.latest",
531 "VM DRS score": "clusterServices.vmDrsScore.latest",
532 } {
533 t.Run(name, func(t *testing.T) {
534 assert.NotContains(t, names, counter)
535 })
536 }
537 }
538
539 func prepareDiscovererSim(t *testing.T) (d *Discoverer, model *simulator.Model, teardown func()) {
540 model, srv := createSim(t)
541 teardown = func() { model.Remove(); srv.Close() }
542 c := newClient(t, srv.URL)
543
544 return New(c), model, teardown
545 }
546
547 func newClient(t *testing.T, vCenterURL *url.URL) *client.Client {
548 c, err := client.New(client.Config{
549 URL: vCenterURL.String(),
550 User: "admin",
551 Password: "password",
552 Timeout: time.Second * 3,
553 TLSConfig: tlscfg.TLSConfig{InsecureSkipVerify: true},
554 })
555 require.NoError(t, err)
556 return c
557 }
558
559 func createSim(t *testing.T) (*simulator.Model, *simulator.Server) {
560 model := simulator.VPX()
561 err := model.Create()
562 require.NoError(t, err)
563 model.Service.TLS = new(tls.Config)
564 return model, model.Service.NewServer()
565 }
566
567 func isHierarchySet(res *rs.Resources) bool {
568 for _, c := range res.Clusters {
569 if !c.Hier.IsSet() {
570 return false
571 }
572 }
573 for _, h := range res.Hosts {
574 if !h.Hier.IsSet() {
575 return false
576 }
577 }
578 for _, v := range res.VMs {
579 if !v.Hier.IsSet() {
580 return false
581 }
582 }
583 for _, ds := range res.Datastores {
584 if !ds.Hier.IsSet() {
585 return false
586 }
587 }
588 for _, network := range res.Networks {
589 if !network.Hier.IsSet() {
590 return false
591 }
592 }
593 for _, rp := range res.ResourcePools {
594 if !rp.Hier.IsSet() {
595 return false
596 }
597 }
598 return true
599 }
600
601 func isMetricListsCollected(res *rs.Resources) bool {
602 for _, h := range res.Hosts {
603 if h.MetricList == nil {
604 return false
605 }
606 }
607 for _, v := range res.VMs {
608 if v.MetricList == nil {
609 return false
610 }
611 }
612 // Datastore metric lists may be empty if perf counters are not available (e.g., vSAN)
613 return true
614 }
615
616 type falseHostMatcher struct{}
617
618 func (falseHostMatcher) Match(*rs.Host) bool { return false }
619
620 type falseVMMatcher struct{}
621
622 func (falseVMMatcher) Match(*rs.VM) bool { return false }
623
624 type falseStoragePodMatcher struct{}
625
626 func (falseStoragePodMatcher) Match(*rs.StoragePod) bool { return false }
627
628 type storagePodsErrorClient struct {
629 Client
630 }
631
632 func (storagePodsErrorClient) StoragePods(...string) ([]mo.StoragePod, error) {
633 return nil, errors.New("storage pod permission denied")
634 }
635
636 type networksErrorClient struct {
637 Client
638 }
639
640 func (networksErrorClient) Networks(...string) ([]mo.Network, error) {
641 return nil, errors.New("network permission denied")
642 }
643
644 type customFieldsErrorClient struct {
645 Client
646 }
647
648 func (customFieldsErrorClient) CustomFields() ([]types.CustomFieldDef, error) {
649 return nil, errors.New("custom field permission denied")
650 }
651
652 type tagsByRefErrorClient struct {
653 Client
654 }
655
656 func (tagsByRefErrorClient) TagsByRef([]types.ManagedObjectReference) (map[types.ManagedObjectReference]map[string][]string, error) {
657 return nil, errors.New("tag permission denied")
658 }