master
go 438 lines 12.6 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package vsphere
4
5 import (
6 "context"
7 "fmt"
8 "sort"
9
10 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
11 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
12 rs "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/resources"
13 scrapepkg "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/vsphere/scrape"
14 )
15
16 const (
17 readinessMethodID = "readiness"
18 readinessMethodHelp = "Reports vSphere collector readiness, cached discovery state, and optional capability gates."
19 )
20
21 type readinessStatus string
22
23 const (
24 readinessStatusOK readinessStatus = "ok"
25 readinessStatusWarning readinessStatus = "warning"
26 readinessStatusDisabled readinessStatus = "disabled"
27 readinessStatusNotReady readinessStatus = "not_ready"
28 )
29
30 type readinessRow struct {
31 check string
32 scope string
33 status readinessStatus
34 details string
35 }
36
37 type readinessColumn struct {
38 funcapi.ColumnMeta
39 value func(readinessRow) any
40 }
41
42 var readinessColumns = []readinessColumn{
43 {
44 ColumnMeta: funcapi.ColumnMeta{
45 Name: "check",
46 Tooltip: "Check",
47 Type: funcapi.FieldTypeString,
48 Visible: true,
49 Sortable: true,
50 Sticky: true,
51 Summary: funcapi.FieldSummaryCount,
52 Filter: funcapi.FieldFilterMultiselect,
53 Transform: funcapi.FieldTransformText,
54 Visualization: funcapi.FieldVisualValue,
55 UniqueKey: true,
56 },
57 value: func(row readinessRow) any { return row.check },
58 },
59 {
60 ColumnMeta: funcapi.ColumnMeta{
61 Name: "scope",
62 Tooltip: "Scope",
63 Type: funcapi.FieldTypeString,
64 Visible: true,
65 Sortable: true,
66 Summary: funcapi.FieldSummaryCount,
67 Filter: funcapi.FieldFilterMultiselect,
68 Transform: funcapi.FieldTransformText,
69 Visualization: funcapi.FieldVisualValue,
70 },
71 value: func(row readinessRow) any { return row.scope },
72 },
73 {
74 ColumnMeta: funcapi.ColumnMeta{
75 Name: "status",
76 Tooltip: "Status",
77 Type: funcapi.FieldTypeString,
78 Visible: true,
79 Sortable: true,
80 Summary: funcapi.FieldSummaryCount,
81 Filter: funcapi.FieldFilterMultiselect,
82 Transform: funcapi.FieldTransformText,
83 Visualization: funcapi.FieldVisualPill,
84 },
85 value: func(row readinessRow) any { return string(row.status) },
86 },
87 {
88 ColumnMeta: funcapi.ColumnMeta{
89 Name: "details",
90 Tooltip: "Details",
91 Type: funcapi.FieldTypeString,
92 Visible: true,
93 Sortable: false,
94 Summary: funcapi.FieldSummaryCount,
95 Filter: funcapi.FieldFilterNone,
96 Transform: funcapi.FieldTransformText,
97 Visualization: funcapi.FieldVisualValue,
98 FullWidth: true,
99 Wrap: true,
100 },
101 value: func(row readinessRow) any { return row.details },
102 },
103 }
104
105 type funcReadiness struct {
106 collector *Collector
107 }
108
109 var _ funcapi.MethodHandler = (*funcReadiness)(nil)
110
111 func vsphereMethods() []funcapi.MethodConfig {
112 return []funcapi.MethodConfig{{
113 ID: readinessMethodID,
114 Name: "vSphere Readiness",
115 UpdateEvery: 30,
116 Help: readinessMethodHelp,
117 RequireCloud: true,
118 }, vsphereTopologyMethodConfig()}
119 }
120
121 func vsphereMethodHandler(job collectorapi.RuntimeJob) funcapi.MethodHandler {
122 c, ok := job.Collector().(*Collector)
123 if !ok {
124 return nil
125 }
126 return &funcVSphere{
127 readiness: &funcReadiness{collector: c},
128 topology: &funcTopology{collector: c, agentID: job.FullName(), jobName: job.Name()},
129 }
130 }
131
132 type funcVSphere struct {
133 readiness *funcReadiness
134 topology *funcTopology
135 }
136
137 var _ funcapi.MethodHandler = (*funcVSphere)(nil)
138
139 func (f *funcVSphere) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
140 switch method {
141 case readinessMethodID:
142 return f.readiness.MethodParams(ctx, method)
143 case topologyMethodID:
144 return f.topology.MethodParams(ctx, method)
145 default:
146 return nil, nil
147 }
148 }
149
150 func (f *funcVSphere) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
151 switch method {
152 case readinessMethodID:
153 return f.readiness.Handle(ctx, method, params)
154 case topologyMethodID:
155 return f.topology.Handle(ctx, method, params)
156 default:
157 return funcapi.NotFoundResponse(method)
158 }
159 }
160
161 func (f *funcVSphere) Cleanup(ctx context.Context) {
162 f.readiness.Cleanup(ctx)
163 f.topology.Cleanup(ctx)
164 }
165
166 func (f *funcReadiness) MethodParams(context.Context, string) ([]funcapi.ParamConfig, error) {
167 return nil, nil
168 }
169
170 func (f *funcReadiness) Handle(_ context.Context, method string, _ funcapi.ResolvedParams) *funcapi.FunctionResponse {
171 if method != readinessMethodID {
172 return funcapi.NotFoundResponse(method)
173 }
174 if f.collector == nil {
175 return funcapi.UnavailableResponse("collector is not initialized")
176 }
177
178 rows := f.collector.readinessRows()
179 sort.SliceStable(rows, func(i, j int) bool {
180 if rows[i].scope != rows[j].scope {
181 return rows[i].scope < rows[j].scope
182 }
183 return rows[i].check < rows[j].check
184 })
185
186 cs := funcapi.Columns(readinessColumns, func(col readinessColumn) funcapi.ColumnMeta {
187 return col.ColumnMeta
188 })
189 data := make([][]any, 0, len(rows))
190 for _, row := range rows {
191 values := make([]any, len(readinessColumns))
192 for i, col := range readinessColumns {
193 values[i] = col.value(row)
194 }
195 data = append(data, values)
196 }
197
198 return &funcapi.FunctionResponse{
199 Status: 200,
200 Help: readinessMethodHelp,
201 Columns: cs.BuildColumns(),
202 Data: data,
203 DefaultSortColumn: "status",
204 }
205 }
206
207 func (f *funcReadiness) Cleanup(context.Context) {
208 // No per-invocation resources are allocated by the readiness function.
209 }
210
211 func (c *Collector) readinessRows() []readinessRow {
212 c.collectionLock.RLock()
213 defer c.collectionLock.RUnlock()
214
215 rows := []readinessRow{
216 configReadinessRow("target_url", "target", c.URL != "", "target URL is configured", "target URL is not configured"),
217 configReadinessRow("credentials", "target", c.Username != "" && c.Password != "", "credentials are configured", "username or password is not configured"),
218 configReadinessRow("client", "target", c.vsClient != nil && c.discoverer != nil && c.scraper != nil, "vSphere client, discoverer, and scraper are initialized", "collector has not completed initialization"),
219 }
220
221 if c.resources == nil {
222 rows = append(rows,
223 readinessRow{
224 check: "inventory_cache",
225 scope: "discovery",
226 status: readinessStatusNotReady,
227 details: "initial discovery has not completed successfully yet",
228 },
229 readinessRow{
230 check: "performance_counters",
231 scope: "discovery",
232 status: readinessStatusNotReady,
233 details: "performance counter lists are unavailable until discovery succeeds",
234 },
235 )
236 } else {
237 rows = append(rows,
238 readinessRow{
239 check: "inventory_cache",
240 scope: "discovery",
241 status: readinessStatusOK,
242 details: resourceCounts(c.resources),
243 },
244 c.performanceCountersReadinessRow(),
245 )
246 }
247
248 rows = append(rows,
249 c.userMetadataReadinessRow(),
250 c.optionalMetricReadinessRow("datastore_clusters", "metrics", c.CollectDatastoreClusters, len(c.DatastoreClustersInclude)),
251 c.booleanReadinessRow("network_topology", "topology", c.CollectNetworkTopology, "vSphere Network topology discovery is enabled", "vSphere Network topology discovery is disabled"),
252 c.vsanReadinessRow(),
253 )
254
255 return rows
256 }
257
258 func configReadinessRow(check, scope string, ok bool, okDetails, notOKDetails string) readinessRow {
259 if ok {
260 return readinessRow{check: check, scope: scope, status: readinessStatusOK, details: okDetails}
261 }
262 return readinessRow{check: check, scope: scope, status: readinessStatusNotReady, details: notOKDetails}
263 }
264
265 func (c *Collector) booleanReadinessRow(check, scope string, enabled bool, enabledDetails, disabledDetails string) readinessRow {
266 if enabled {
267 return readinessRow{check: check, scope: scope, status: readinessStatusOK, details: enabledDetails}
268 }
269 return readinessRow{check: check, scope: scope, status: readinessStatusDisabled, details: disabledDetails}
270 }
271
272 func (c *Collector) optionalMetricReadinessRow(check, scope string, enabled bool, includePatterns int) readinessRow {
273 if !enabled {
274 return readinessRow{
275 check: check,
276 scope: scope,
277 status: readinessStatusDisabled,
278 details: "optional collection is disabled",
279 }
280 }
281 return readinessRow{
282 check: check,
283 scope: scope,
284 status: readinessStatusOK,
285 details: fmt.Sprintf("enabled with include_patterns=%d", includePatterns),
286 }
287 }
288
289 func (c *Collector) userMetadataReadinessRow() readinessRow {
290 tags := len(c.TagCategories)
291 attrs := len(c.CustomAttributes)
292 if tags == 0 && attrs == 0 {
293 return readinessRow{
294 check: "user_metadata_labels",
295 scope: "labels",
296 status: readinessStatusDisabled,
297 details: "vSphere tag and custom-attribute labels are disabled",
298 }
299 }
300 return readinessRow{
301 check: "user_metadata_labels",
302 scope: "labels",
303 status: readinessStatusOK,
304 details: fmt.Sprintf("enabled for %d tag category pattern(s), %d custom attribute pattern(s)", tags, attrs),
305 }
306 }
307
308 func (c *Collector) vsanReadinessRow() readinessRow {
309 if !c.CollectVSAN {
310 return readinessRow{
311 check: "vsan",
312 scope: "metrics",
313 status: readinessStatusDisabled,
314 details: "vSAN collection is disabled",
315 }
316 }
317 if c.resources == nil {
318 return readinessRow{
319 check: "vsan",
320 scope: "metrics",
321 status: readinessStatusNotReady,
322 details: "vSAN collection is enabled, but discovery has not completed",
323 }
324 }
325
326 clusters, hosts, vms := c.vsanResources()
327 if len(clusters) == 0 {
328 return readinessRow{
329 check: "vsan",
330 scope: "metrics",
331 status: readinessStatusWarning,
332 details: "vSAN collection is enabled, but no vSAN-enabled clusters match the vSAN selectors",
333 }
334 }
335 if c.vsanMetrics == nil {
336 return readinessRow{
337 check: "vsan",
338 scope: "metrics",
339 status: readinessStatusNotReady,
340 details: fmt.Sprintf("vSAN collection is enabled for %d cluster(s), %d host(s), and %d VM(s) after selectors, but no vSAN scrape data is cached yet", len(clusters), len(hosts), len(vms)),
341 }
342 }
343 if vsanMetricsEmpty(c.vsanMetrics) {
344 return readinessRow{
345 check: "vsan",
346 scope: "metrics",
347 status: readinessStatusWarning,
348 details: fmt.Sprintf("vSAN collection is enabled for %d cluster(s), %d host(s), and %d VM(s) after selectors, but the last vSAN scrape returned no data", len(clusters), len(hosts), len(vms)),
349 }
350 }
351
352 return readinessRow{
353 check: "vsan",
354 scope: "metrics",
355 status: readinessStatusOK,
356 details: fmt.Sprintf(
357 "vSAN data cached for %d cluster metric group(s), %d host metric group(s), %d VM metric group(s), %d space result(s), and %d health result(s)",
358 len(c.vsanMetrics.Clusters),
359 len(c.vsanMetrics.Hosts),
360 len(c.vsanMetrics.VMs),
361 len(c.vsanMetrics.Space),
362 len(c.vsanMetrics.Health),
363 ),
364 }
365 }
366
367 func vsanMetricsEmpty(metrics *scrapepkg.VSANMetrics) bool {
368 return metrics == nil ||
369 len(metrics.Clusters)+len(metrics.Hosts)+len(metrics.VMs)+len(metrics.Space)+len(metrics.Health) == 0
370 }
371
372 func (c *Collector) performanceCountersReadinessRow() readinessRow {
373 var hosts, hostsWithMetrics, vms, vmsWithMetrics, datastores, datastoresWithMetrics, clusters, clustersWithMetrics int
374 for _, host := range c.resources.Hosts {
375 hosts++
376 if len(host.MetricList) > 0 {
377 hostsWithMetrics++
378 }
379 }
380 for _, vm := range c.resources.VMs {
381 vms++
382 if len(vm.MetricList) > 0 {
383 vmsWithMetrics++
384 }
385 }
386 for _, datastore := range c.resources.Datastores {
387 datastores++
388 if len(datastore.MetricList) > 0 {
389 datastoresWithMetrics++
390 }
391 }
392 for _, cluster := range c.resources.Clusters {
393 clusters++
394 if len(cluster.MetricList) > 0 {
395 clustersWithMetrics++
396 }
397 }
398
399 status := readinessStatusOK
400 if (hosts > 0 && hostsWithMetrics == 0) ||
401 (vms > 0 && vmsWithMetrics == 0) ||
402 (datastores > 0 && datastoresWithMetrics == 0) ||
403 (clusters > 0 && clustersWithMetrics == 0) {
404 status = readinessStatusWarning
405 }
406
407 return readinessRow{
408 check: "performance_counters",
409 scope: "discovery",
410 status: status,
411 details: fmt.Sprintf(
412 "metric lists: %d/%d hosts, %d/%d VMs, %d/%d datastores, %d/%d clusters",
413 hostsWithMetrics,
414 hosts,
415 vmsWithMetrics,
416 vms,
417 datastoresWithMetrics,
418 datastores,
419 clustersWithMetrics,
420 clusters,
421 ),
422 }
423 }
424
425 func resourceCounts(resources *rs.Resources) string {
426 return fmt.Sprintf(
427 "discovered %d datacenter(s), %d folder(s), %d cluster(s), %d host(s), %d VM(s), %d datastore(s), %d network(s), %d datastore cluster(s), and %d resource pool(s)",
428 len(resources.DataCenters),
429 len(resources.Folders),
430 len(resources.Clusters),
431 len(resources.Hosts),
432 len(resources.VMs),
433 len(resources.Datastores),
434 len(resources.Networks),
435 len(resources.StoragePods),
436 len(resources.ResourcePools),
437 )
438 }