| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package vsphere |
| 4 | |
| 5 | import ( |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/stretchr/testify/require" |
| 9 | "github.com/vmware/govmomi/performance" |
| 10 | |
| 11 | "github.com/netdata/netdata/go/plugins/pkg/metrix" |
| 12 | "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/match" |
| 13 | rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources" |
| 14 | scrapepkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape" |
| 15 | "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest" |
| 16 | ) |
| 17 | |
| 18 | func TestCollector_VSANMetricsDefaultOff(t *testing.T) { |
| 19 | collr := newVSANTestCollector(false) |
| 20 | cycle := mustCycleController(t, collr.MetricStore()) |
| 21 | cycle.BeginCycle() |
| 22 | collr.writeVSANMetrics() |
| 23 | require.NoError(t, cycle.CommitCycleSuccess()) |
| 24 | |
| 25 | reader := collr.MetricStore().Read(metrix.ReadRaw()) |
| 26 | require.Zero(t, countMetricSeries(reader, vsanClusterSpaceUsageTotalMetric)) |
| 27 | require.Zero(t, countMetricSeries(reader, vsanHostOperationsReadMetric)) |
| 28 | require.Zero(t, countMetricSeries(reader, vsanVMOperationsReadMetric)) |
| 29 | } |
| 30 | |
| 31 | func TestCollector_VSANMetricsOptInEmitsCharts(t *testing.T) { |
| 32 | collr := newVSANTestCollector(true) |
| 33 | cycle := mustCycleController(t, collr.MetricStore()) |
| 34 | cycle.BeginCycle() |
| 35 | collr.writeVSANMetrics() |
| 36 | require.NoError(t, cycle.CommitCycleSuccess()) |
| 37 | |
| 38 | cluster := collr.resources.Clusters.Get("domain-c1") |
| 39 | host := collr.resources.Hosts.Get("host-1") |
| 40 | vm := collr.resources.VMs.Get("vm-1") |
| 41 | reader := collr.MetricStore().Read(metrix.ReadRaw()) |
| 42 | |
| 43 | clusterLabels := labelsFromMetrix(collr.vsanClusterLabels(cluster)) |
| 44 | requireMetricValue(t, reader, vsanClusterSpaceUsageTotalMetric, clusterLabels, 1000) |
| 45 | requireMetricValue(t, reader, vsanClusterSpaceUsageFreeMetric, clusterLabels, 400) |
| 46 | requireMetricValue(t, reader, vsanClusterSpaceUsageUsedMetric, clusterLabels, 600) |
| 47 | requireMetricValue(t, reader, vsanClusterSpaceUtilizationUsedMetric, clusterLabels, 6000) |
| 48 | requireMetricValue(t, reader, vsanClusterHealthStatusGreenMetric, clusterLabels, 1) |
| 49 | requireMetricValue(t, reader, vsanClusterOperationsReadMetric, clusterLabels, 10) |
| 50 | requireMetricValue(t, reader, vsanClusterThroughputReadMetric, clusterLabels, 11) |
| 51 | requireMetricValue(t, reader, vsanClusterLatencyReadMetric, clusterLabels, 12) |
| 52 | requireMetricValue(t, reader, vsanClusterCongestionsMetric, clusterLabels, 13) |
| 53 | requireMetricValue(t, reader, vsanClusterOperationsWriteMetric, clusterLabels, 14) |
| 54 | requireMetricValue(t, reader, vsanClusterThroughputWriteMetric, clusterLabels, 15) |
| 55 | requireMetricValue(t, reader, vsanClusterLatencyWriteMetric, clusterLabels, 16) |
| 56 | |
| 57 | hostLabels := labelsFromMetrix(collr.vsanHostLabels(host)) |
| 58 | requireMetricValue(t, reader, vsanHostOperationsReadMetric, hostLabels, 20) |
| 59 | requireMetricValue(t, reader, vsanHostThroughputReadMetric, hostLabels, 21) |
| 60 | requireMetricValue(t, reader, vsanHostLatencyReadMetric, hostLabels, 22) |
| 61 | requireMetricValue(t, reader, vsanHostCongestionsMetric, hostLabels, 23) |
| 62 | requireMetricValue(t, reader, vsanHostCacheHitRateMetric, hostLabels, 95) |
| 63 | requireMetricValue(t, reader, vsanHostOperationsWriteMetric, hostLabels, 24) |
| 64 | requireMetricValue(t, reader, vsanHostThroughputWriteMetric, hostLabels, 25) |
| 65 | requireMetricValue(t, reader, vsanHostLatencyWriteMetric, hostLabels, 26) |
| 66 | |
| 67 | vmLabels := labelsFromMetrix(collr.vsanVMLabels(vm)) |
| 68 | requireMetricValue(t, reader, vsanVMOperationsReadMetric, vmLabels, 30) |
| 69 | requireMetricValue(t, reader, vsanVMThroughputReadMetric, vmLabels, 31) |
| 70 | requireMetricValue(t, reader, vsanVMLatencyReadMetric, vmLabels, 32) |
| 71 | requireMetricValue(t, reader, vsanVMOperationsWriteMetric, vmLabels, 33) |
| 72 | requireMetricValue(t, reader, vsanVMThroughputWriteMetric, vmLabels, 34) |
| 73 | requireMetricValue(t, reader, vsanVMLatencyWriteMetric, vmLabels, 35) |
| 74 | |
| 75 | createdCharts, createdDims := v2CreatedChartsAndDims(buildV2PlanForTest(t, collr)) |
| 76 | chartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vsan_cluster_space_usage", map[string]string{"id": cluster.ID}) |
| 77 | require.Contains(t, createdDims[chartID], "used") |
| 78 | hostChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vsan_host_operations", map[string]string{"id": host.ID}) |
| 79 | require.Contains(t, createdDims[hostChartID], "read") |
| 80 | vmChartID := findChartIDByLabelsAndContext(t, createdCharts, "vsphere.vsan_vm_operations", map[string]string{"id": vm.ID}) |
| 81 | require.Contains(t, createdDims[vmChartID], "read") |
| 82 | collecttest.AssertChartCoverage(t, collr, collecttest.ChartCoverageExpectation{}) |
| 83 | requireChartSelectorsMatchSeries(t, collr, "vsphere.vsan_") |
| 84 | } |
| 85 | |
| 86 | func TestCollector_VSANSpaceUsageEdgeCases(t *testing.T) { |
| 87 | tests := map[string]struct { |
| 88 | space scrapepkg.VSANSpaceUsage |
| 89 | want map[string]int64 |
| 90 | }{ |
| 91 | "zero total": { |
| 92 | space: scrapepkg.VSANSpaceUsage{Total: 0, Free: 0}, |
| 93 | want: map[string]int64{ |
| 94 | vsanClusterSpaceUsageUsedMetric: 0, |
| 95 | vsanClusterSpaceUtilizationUsedMetric: 0, |
| 96 | }, |
| 97 | }, |
| 98 | "free greater than total": { |
| 99 | space: scrapepkg.VSANSpaceUsage{Total: 100, Free: 200}, |
| 100 | want: map[string]int64{ |
| 101 | vsanClusterSpaceUsageUsedMetric: 0, |
| 102 | vsanClusterSpaceUtilizationUsedMetric: 0, |
| 103 | }, |
| 104 | }, |
| 105 | } |
| 106 | |
| 107 | for name, tc := range tests { |
| 108 | t.Run(name, func(t *testing.T) { |
| 109 | collr := newVSANTestCollector(true) |
| 110 | collr.vsanMetrics.Space["domain-c1"] = tc.space |
| 111 | cycle := mustCycleController(t, collr.MetricStore()) |
| 112 | cycle.BeginCycle() |
| 113 | collr.writeVSANMetrics() |
| 114 | require.NoError(t, cycle.CommitCycleSuccess()) |
| 115 | |
| 116 | labels := labelsFromMetrix(collr.vsanClusterLabels(collr.resources.Clusters.Get("domain-c1"))) |
| 117 | reader := collr.MetricStore().Read(metrix.ReadRaw()) |
| 118 | for metric, want := range tc.want { |
| 119 | requireMetricValue(t, reader, metric, labels, want) |
| 120 | } |
| 121 | }) |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestCollector_CollectVSANUsesSelectors(t *testing.T) { |
| 126 | collr := New() |
| 127 | collr.URL = "https://127.0.0.1" |
| 128 | collr.Username = "user" |
| 129 | collr.Password = "pass" |
| 130 | collr.CollectVSAN = true |
| 131 | collr.VSANClustersInclude = match.VSANClusterIncludes{"vsan_uuid:cluster-uuid-2"} |
| 132 | collr.VSANHostsInclude = match.VSANHostIncludes{"vsan_node_uuid:host-uuid-2"} |
| 133 | collr.VSANVMsInclude = match.VSANVMIncludes{"instance_uuid:vm-uuid-2"} |
| 134 | collr.resources = newVSANFilterTestResources() |
| 135 | scraper := &capturingVSANScraper{} |
| 136 | collr.scraper = scraper |
| 137 | |
| 138 | require.NoError(t, collr.validateConfig()) |
| 139 | collr.collectVSAN() |
| 140 | |
| 141 | require.NotNil(t, collr.vsanMetrics) |
| 142 | require.Contains(t, scraper.clusters, "domain-c2") |
| 143 | require.NotContains(t, scraper.clusters, "domain-c1") |
| 144 | require.Contains(t, scraper.hosts, "host-2") |
| 145 | require.NotContains(t, scraper.hosts, "host-1") |
| 146 | require.Contains(t, scraper.vms, "vm-2") |
| 147 | require.NotContains(t, scraper.vms, "vm-1") |
| 148 | require.Len(t, scraper.clusters, 1) |
| 149 | require.Len(t, scraper.hosts, 1) |
| 150 | require.Len(t, scraper.vms, 1) |
| 151 | } |
| 152 | |
| 153 | func newVSANTestCollector(enabled bool) *Collector { |
| 154 | collr := New() |
| 155 | collr.CollectVSAN = enabled |
| 156 | collr.resources = &rs.Resources{ |
| 157 | Clusters: rs.Clusters{ |
| 158 | "domain-c1": &rs.Cluster{ |
| 159 | ID: "domain-c1", |
| 160 | Name: "Cluster1", |
| 161 | Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}}, |
| 162 | VSANEnabled: true, |
| 163 | VSANUUID: "cluster-uuid", |
| 164 | }, |
| 165 | }, |
| 166 | Hosts: rs.Hosts{ |
| 167 | "host-1": &rs.Host{ |
| 168 | ID: "host-1", |
| 169 | Name: "Host1", |
| 170 | Hier: rs.HostHierarchy{DC: rs.HierarchyValue{ID: "datacenter-1", Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}}, |
| 171 | VSANNodeUUID: "host-uuid", |
| 172 | }, |
| 173 | }, |
| 174 | VMs: rs.VMs{ |
| 175 | "vm-1": &rs.VM{ |
| 176 | ID: "vm-1", |
| 177 | Name: "VM1", |
| 178 | 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"}}, |
| 179 | InstanceUUID: "vm-uuid", |
| 180 | }, |
| 181 | }, |
| 182 | } |
| 183 | collr.vsanMetrics = &scrapepkg.VSANMetrics{ |
| 184 | Clusters: map[string]scrapepkg.VSANEntityMetrics{ |
| 185 | "domain-c1": { |
| 186 | "read_operations": 10, |
| 187 | "read_throughput": 11, |
| 188 | "read_latency": 12, |
| 189 | "congestions": 13, |
| 190 | "write_operations": 14, |
| 191 | "write_throughput": 15, |
| 192 | "write_latency": 16, |
| 193 | }, |
| 194 | }, |
| 195 | Hosts: map[string]scrapepkg.VSANEntityMetrics{ |
| 196 | "host-1": { |
| 197 | "read_operations": 20, |
| 198 | "read_throughput": 21, |
| 199 | "read_latency": 22, |
| 200 | "congestions": 23, |
| 201 | "write_operations": 24, |
| 202 | "write_throughput": 25, |
| 203 | "write_latency": 26, |
| 204 | "cache_hit_rate": 95, |
| 205 | }, |
| 206 | }, |
| 207 | VMs: map[string]scrapepkg.VSANEntityMetrics{ |
| 208 | "vm-1": { |
| 209 | "read_operations": 30, |
| 210 | "read_throughput": 31, |
| 211 | "read_latency": 32, |
| 212 | "write_operations": 33, |
| 213 | "write_throughput": 34, |
| 214 | "write_latency": 35, |
| 215 | }, |
| 216 | }, |
| 217 | Space: map[string]scrapepkg.VSANSpaceUsage{ |
| 218 | "domain-c1": {Total: 1000, Free: 400}, |
| 219 | }, |
| 220 | Health: map[string]string{ |
| 221 | "domain-c1": "green", |
| 222 | }, |
| 223 | } |
| 224 | return collr |
| 225 | } |
| 226 | |
| 227 | func newVSANFilterTestResources() *rs.Resources { |
| 228 | return &rs.Resources{ |
| 229 | Clusters: rs.Clusters{ |
| 230 | "domain-c1": &rs.Cluster{ |
| 231 | ID: "domain-c1", |
| 232 | Name: "Cluster1", |
| 233 | Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{Name: "DC1"}}, |
| 234 | VSANEnabled: true, |
| 235 | VSANUUID: "cluster-uuid-1", |
| 236 | }, |
| 237 | "domain-c2": &rs.Cluster{ |
| 238 | ID: "domain-c2", |
| 239 | Name: "Cluster2", |
| 240 | Hier: rs.ClusterHierarchy{DC: rs.HierarchyValue{Name: "DC1"}}, |
| 241 | VSANEnabled: true, |
| 242 | VSANUUID: "cluster-uuid-2", |
| 243 | }, |
| 244 | }, |
| 245 | Hosts: rs.Hosts{ |
| 246 | "host-1": &rs.Host{ |
| 247 | ID: "host-1", |
| 248 | Name: "Host1", |
| 249 | Hier: rs.HostHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}}, |
| 250 | VSANNodeUUID: "host-uuid-1", |
| 251 | }, |
| 252 | "host-2": &rs.Host{ |
| 253 | ID: "host-2", |
| 254 | Name: "Host2", |
| 255 | Hier: rs.HostHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}}, |
| 256 | VSANNodeUUID: "host-uuid-2", |
| 257 | }, |
| 258 | "host-3": &rs.Host{ |
| 259 | ID: "host-3", |
| 260 | Name: "Host3", |
| 261 | Hier: rs.HostHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}}, |
| 262 | VSANNodeUUID: "host-uuid-3", |
| 263 | }, |
| 264 | }, |
| 265 | VMs: rs.VMs{ |
| 266 | "vm-1": &rs.VM{ |
| 267 | ID: "vm-1", |
| 268 | Name: "VM1", |
| 269 | Hier: rs.VMHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c1", Name: "Cluster1"}, Host: rs.HierarchyValue{ID: "host-1", Name: "Host1"}}, |
| 270 | InstanceUUID: "vm-uuid-1", |
| 271 | }, |
| 272 | "vm-2": &rs.VM{ |
| 273 | ID: "vm-2", |
| 274 | Name: "VM2", |
| 275 | Hier: rs.VMHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}, Host: rs.HierarchyValue{ID: "host-2", Name: "Host2"}}, |
| 276 | InstanceUUID: "vm-uuid-2", |
| 277 | }, |
| 278 | "vm-3": &rs.VM{ |
| 279 | ID: "vm-3", |
| 280 | Name: "VM3", |
| 281 | Hier: rs.VMHierarchy{DC: rs.HierarchyValue{Name: "DC1"}, Cluster: rs.HierarchyValue{ID: "domain-c2", Name: "Cluster2"}, Host: rs.HierarchyValue{ID: "host-3", Name: "Host3"}}, |
| 282 | InstanceUUID: "vm-uuid-3", |
| 283 | }, |
| 284 | }, |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | type capturingVSANScraper struct { |
| 289 | clusters rs.Clusters |
| 290 | hosts rs.Hosts |
| 291 | vms rs.VMs |
| 292 | } |
| 293 | |
| 294 | func (s *capturingVSANScraper) ScrapeHosts(rs.Hosts) []performance.EntityMetric { |
| 295 | return nil |
| 296 | } |
| 297 | |
| 298 | func (s *capturingVSANScraper) ScrapeVMs(rs.VMs) []performance.EntityMetric { |
| 299 | return nil |
| 300 | } |
| 301 | |
| 302 | func (s *capturingVSANScraper) ScrapeDatastores(rs.Datastores) []performance.EntityMetric { |
| 303 | return nil |
| 304 | } |
| 305 | |
| 306 | func (s *capturingVSANScraper) ScrapeClusters(rs.Clusters) []performance.EntityMetric { |
| 307 | return nil |
| 308 | } |
| 309 | |
| 310 | func (s *capturingVSANScraper) ScrapeVSAN(clusters rs.Clusters, hosts rs.Hosts, vms rs.VMs) *scrapepkg.VSANMetrics { |
| 311 | s.clusters = clusters |
| 312 | s.hosts = hosts |
| 313 | s.vms = vms |
| 314 | return &scrapepkg.VSANMetrics{} |
| 315 | } |
| 316 | |
| 317 | func labelsFromMetrix(labels []metrix.Label) metrix.Labels { |
| 318 | out := make(metrix.Labels, len(labels)) |
| 319 | for _, label := range labels { |
| 320 | out[label.Key] = label.Value |
| 321 | } |
| 322 | return out |
| 323 | } |