| 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 | } |