| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package vsphere |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "strings" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/netdata/netdata/go/plugins/pkg/funcapi" |
| 11 | topologyv1 "github.com/netdata/netdata/go/plugins/pkg/topology/v1" |
| 12 | ) |
| 13 | |
| 14 | const ( |
| 15 | topologyMethodID = "topology:vsphere" |
| 16 | topologyMethodHelp = "Reports cached vSphere inventory topology for datacenters, clusters, hosts, VMs, datastores, networks, datastore clusters, and resource pools." |
| 17 | |
| 18 | vsphereTopologySource = "vsphere" |
| 19 | vsphereTopologyLayer = "virtualization" |
| 20 | vsphereTopologyView = "vsphere-inventory" |
| 21 | |
| 22 | vsphereTopologyDetailTable = "vsphere_object_detail" |
| 23 | vsphereTopologyLabelsTable = "actor_labels" |
| 24 | vsphereTopologyOwnershipLink = "vsphere_ownership" |
| 25 | vsphereTopologyRunsOnLink = "vsphere_runs_on" |
| 26 | vsphereTopologyNetworkLink = "vsphere_connected_to" |
| 27 | vsphereOwnershipEvidenceType = "vsphere_ownership_evidence" |
| 28 | vsphereRunsOnEvidenceType = "vsphere_runs_on_evidence" |
| 29 | vsphereNetworkConnectionEvidence = "vsphere_network_connection_evidence" |
| 30 | |
| 31 | vsphereDatastoreUtilizationOverlay = "datastore_space_utilization" |
| 32 | vsphereOverlaySelectorCollectJob = "collect_job" |
| 33 | vsphereOverlaySelectorID = "id" |
| 34 | ) |
| 35 | |
| 36 | type funcTopology struct { |
| 37 | collector *Collector |
| 38 | agentID string |
| 39 | jobName string |
| 40 | } |
| 41 | |
| 42 | var _ funcapi.MethodHandler = (*funcTopology)(nil) |
| 43 | |
| 44 | func vsphereTopologyMethodConfig() funcapi.MethodConfig { |
| 45 | return funcapi.MethodConfig{ |
| 46 | ID: topologyMethodID, |
| 47 | Aliases: []string{topologyMethodID}, |
| 48 | Name: "vSphere Topology", |
| 49 | UpdateEvery: 30, |
| 50 | Help: topologyMethodHelp, |
| 51 | RequireCloud: true, |
| 52 | ResponseType: "topology", |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | func (f *funcTopology) MethodParams(context.Context, string) ([]funcapi.ParamConfig, error) { |
| 57 | return nil, nil |
| 58 | } |
| 59 | |
| 60 | func (f *funcTopology) Handle(_ context.Context, method string, _ funcapi.ResolvedParams) *funcapi.FunctionResponse { |
| 61 | if method != topologyMethodID { |
| 62 | return funcapi.NotFoundResponse(method) |
| 63 | } |
| 64 | if f.collector == nil { |
| 65 | return funcapi.UnavailableResponse("collector is not initialized") |
| 66 | } |
| 67 | |
| 68 | data, ok := f.collector.topologyData(f.agentID, f.jobName) |
| 69 | if !ok { |
| 70 | return funcapi.UnavailableResponse("topology data not available yet, please retry after discovery") |
| 71 | } |
| 72 | |
| 73 | return &funcapi.FunctionResponse{ |
| 74 | Status: 200, |
| 75 | Help: topologyMethodHelp, |
| 76 | ResponseType: "topology", |
| 77 | Data: data, |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func (f *funcTopology) Cleanup(context.Context) { |
| 82 | // No per-invocation resources are allocated by the topology function. |
| 83 | } |
| 84 | |
| 85 | func (c *Collector) topologyData(agentID, jobName string) (topologyv1.Data, bool) { |
| 86 | c.collectionLock.RLock() |
| 87 | defer c.collectionLock.RUnlock() |
| 88 | |
| 89 | if c.resources == nil { |
| 90 | return topologyv1.Data{}, false |
| 91 | } |
| 92 | |
| 93 | builder := newVSphereTopologyBuilder(jobName) |
| 94 | |
| 95 | for _, dc := range sortedDatacenters(c.resources.DataCenters) { |
| 96 | builder.addActor("vsphere_datacenter", dc.ID, dc.Name, "", vsphereActorDetail{ |
| 97 | objectType: "datacenter", |
| 98 | }, nil) |
| 99 | } |
| 100 | for _, cluster := range sortedClusters(c.resources.Clusters) { |
| 101 | builder.addActor("vsphere_cluster", cluster.ID, cluster.Name, cluster.Hier.DC.ID, vsphereActorDetail{ |
| 102 | objectType: "cluster", |
| 103 | datacenter: cluster.Hier.DC.Name, |
| 104 | overallStatus: cluster.OverallStatus, |
| 105 | drsEnabled: cluster.DrsEnabled, |
| 106 | haEnabled: cluster.HaEnabled, |
| 107 | vsanEnabled: cluster.VSANEnabled, |
| 108 | cpuCapacityMHz: int64(cluster.TotalCpu), |
| 109 | memoryCapacityMiB: cluster.TotalMemory / 1024 / 1024, |
| 110 | }, mergeStringMaps(cluster.Labels, map[string]string{ |
| 111 | "datacenter": cluster.Hier.DC.Name, |
| 112 | })) |
| 113 | if c.resources.DataCenters.Get(cluster.Hier.DC.ID) != nil { |
| 114 | builder.addLink(cluster.Hier.DC.ID, cluster.ID, vsphereTopologyOwnershipLink, "contains") |
| 115 | } |
| 116 | } |
| 117 | for _, host := range sortedHosts(c.resources.Hosts) { |
| 118 | parentID := firstExistingResourceID(func(id string) bool { |
| 119 | if id == host.Hier.Cluster.ID { |
| 120 | return c.resources.Clusters.Get(id) != nil |
| 121 | } |
| 122 | return c.resources.DataCenters.Get(id) != nil |
| 123 | }, host.Hier.Cluster.ID, host.Hier.DC.ID) |
| 124 | builder.addActor("vsphere_host", host.ID, host.Name, parentID, vsphereActorDetail{ |
| 125 | objectType: "host", |
| 126 | datacenter: host.Hier.DC.Name, |
| 127 | cluster: host.Hier.Cluster.Name, |
| 128 | connectionState: host.ConnectionState, |
| 129 | powerState: host.PowerState, |
| 130 | inMaintenanceMode: host.InMaintenanceMode, |
| 131 | overallStatus: host.OverallStatus, |
| 132 | }, mergeStringMaps(host.Labels, map[string]string{ |
| 133 | "datacenter": host.Hier.DC.Name, |
| 134 | "cluster": host.Hier.Cluster.Name, |
| 135 | })) |
| 136 | if c.resources.Clusters.Get(host.Hier.Cluster.ID) != nil { |
| 137 | builder.addLink(host.Hier.Cluster.ID, host.ID, vsphereTopologyOwnershipLink, "contains") |
| 138 | } else if c.resources.DataCenters.Get(host.Hier.DC.ID) != nil { |
| 139 | builder.addLink(host.Hier.DC.ID, host.ID, vsphereTopologyOwnershipLink, "contains") |
| 140 | } |
| 141 | } |
| 142 | for _, vm := range sortedVMs(c.resources.VMs) { |
| 143 | parentID := firstExistingResourceID(func(id string) bool { |
| 144 | switch id { |
| 145 | case vm.Hier.Host.ID: |
| 146 | return c.resources.Hosts.Get(id) != nil |
| 147 | case vm.Hier.Cluster.ID: |
| 148 | return c.resources.Clusters.Get(id) != nil |
| 149 | default: |
| 150 | return c.resources.DataCenters.Get(id) != nil |
| 151 | } |
| 152 | }, vm.Hier.Host.ID, vm.Hier.Cluster.ID, vm.Hier.DC.ID) |
| 153 | builder.addActor("vsphere_vm", vm.ID, vm.Name, parentID, vsphereActorDetail{ |
| 154 | objectType: "vm", |
| 155 | datacenter: vm.Hier.DC.Name, |
| 156 | cluster: vm.Hier.Cluster.Name, |
| 157 | host: vm.Hier.Host.Name, |
| 158 | connectionState: vm.ConnectionState, |
| 159 | powerState: vm.PowerState, |
| 160 | overallStatus: vm.OverallStatus, |
| 161 | toolsRunningStatus: vm.ToolsRunningStatus, |
| 162 | toolsVersionStatus: vm.ToolsVersionStatus, |
| 163 | consolidationNeeded: vm.ConsolidationNeeded, |
| 164 | snapshotCount: vm.SnapshotCount, |
| 165 | snapshotChainDepth: vm.SnapshotMaxChainDepth, |
| 166 | configuredVCPUs: vm.ConfigCPU, |
| 167 | configuredMemoryMiB: vm.ConfigMemory, |
| 168 | }, mergeStringMaps(vm.Labels, map[string]string{ |
| 169 | "datacenter": vm.Hier.DC.Name, |
| 170 | "cluster": vm.Hier.Cluster.Name, |
| 171 | "host": vm.Hier.Host.Name, |
| 172 | })) |
| 173 | switch { |
| 174 | case c.resources.Hosts.Get(vm.Hier.Host.ID) != nil: |
| 175 | builder.addLink(vm.ID, vm.Hier.Host.ID, vsphereTopologyRunsOnLink, "runs_on") |
| 176 | case c.resources.Clusters.Get(vm.Hier.Cluster.ID) != nil: |
| 177 | builder.addLink(vm.Hier.Cluster.ID, vm.ID, vsphereTopologyOwnershipLink, "contains") |
| 178 | case c.resources.DataCenters.Get(vm.Hier.DC.ID) != nil: |
| 179 | builder.addLink(vm.Hier.DC.ID, vm.ID, vsphereTopologyOwnershipLink, "contains") |
| 180 | } |
| 181 | } |
| 182 | for _, datastore := range sortedDatastores(c.resources.Datastores) { |
| 183 | actor := builder.addActor("vsphere_datastore", datastore.ID, datastore.Name, datastore.Hier.DC.ID, vsphereActorDetail{ |
| 184 | objectType: "datastore", |
| 185 | datacenter: datastore.Hier.DC.Name, |
| 186 | overallStatus: datastore.OverallStatus, |
| 187 | datastoreType: datastore.Type, |
| 188 | accessible: datastore.Accessible, |
| 189 | maintenanceMode: datastore.MaintenanceMode, |
| 190 | capacityBytes: datastore.Capacity, |
| 191 | freeSpaceBytes: datastore.FreeSpace, |
| 192 | uncommittedBytes: datastore.Uncommitted, |
| 193 | multipleHostAccess: optionalBool(datastore.MultipleHostAccess), |
| 194 | }, mergeStringMaps(datastore.Labels, map[string]string{ |
| 195 | "datacenter": datastore.Hier.DC.Name, |
| 196 | "type": datastore.Type, |
| 197 | })) |
| 198 | builder.addDatastoreUtilizationOverlay(actor, datastore.ID) |
| 199 | if c.resources.DataCenters.Get(datastore.Hier.DC.ID) != nil { |
| 200 | builder.addLink(datastore.Hier.DC.ID, datastore.ID, vsphereTopologyOwnershipLink, "contains") |
| 201 | } |
| 202 | } |
| 203 | for _, network := range sortedNetworks(c.resources.Networks) { |
| 204 | builder.addActor("vsphere_network", network.ID, network.Name, network.Hier.DC.ID, vsphereActorDetail{ |
| 205 | objectType: "network", |
| 206 | datacenter: network.Hier.DC.Name, |
| 207 | networkType: network.Type, |
| 208 | accessible: network.Accessible, |
| 209 | ipPoolName: network.IPPoolName, |
| 210 | overallStatus: network.OverallStatus, |
| 211 | networkHosts: len(network.HostIDs), |
| 212 | networkVMs: len(network.VMIDs), |
| 213 | }, mergeStringMaps(network.Labels, map[string]string{ |
| 214 | "datacenter": network.Hier.DC.Name, |
| 215 | "type": network.Type, |
| 216 | })) |
| 217 | if c.resources.DataCenters.Get(network.Hier.DC.ID) != nil { |
| 218 | builder.addLink(network.Hier.DC.ID, network.ID, vsphereTopologyOwnershipLink, "contains") |
| 219 | } |
| 220 | for _, hostID := range sortedStrings(network.HostIDs) { |
| 221 | if c.resources.Hosts.Get(hostID) != nil { |
| 222 | builder.addLink(hostID, network.ID, vsphereTopologyNetworkLink, "connected_to") |
| 223 | } |
| 224 | } |
| 225 | for _, vmID := range sortedStrings(network.VMIDs) { |
| 226 | if c.resources.VMs.Get(vmID) != nil { |
| 227 | builder.addLink(vmID, network.ID, vsphereTopologyNetworkLink, "connected_to") |
| 228 | } |
| 229 | } |
| 230 | } |
| 231 | for _, pod := range sortedStoragePods(c.resources.StoragePods) { |
| 232 | builder.addActor("vsphere_datastore_cluster", pod.ID, pod.Name, pod.Hier.DC.ID, vsphereActorDetail{ |
| 233 | objectType: "datastore_cluster", |
| 234 | datacenter: pod.Hier.DC.Name, |
| 235 | overallStatus: pod.OverallStatus, |
| 236 | capacityBytes: pod.Capacity, |
| 237 | freeSpaceBytes: pod.FreeSpace, |
| 238 | storageDRSEnabled: optionalBool(pod.StorageDRSEnabled), |
| 239 | }, mergeStringMaps(pod.Labels, map[string]string{ |
| 240 | "datacenter": pod.Hier.DC.Name, |
| 241 | })) |
| 242 | if c.resources.DataCenters.Get(pod.Hier.DC.ID) != nil { |
| 243 | builder.addLink(pod.Hier.DC.ID, pod.ID, vsphereTopologyOwnershipLink, "contains") |
| 244 | } |
| 245 | } |
| 246 | for _, pool := range sortedResourcePools(c.resources.ResourcePools) { |
| 247 | builder.addActor("vsphere_resource_pool", pool.ID, pool.Name, pool.Hier.Cluster.ID, vsphereActorDetail{ |
| 248 | objectType: "resource_pool", |
| 249 | datacenter: pool.Hier.DC.Name, |
| 250 | cluster: pool.Hier.Cluster.Name, |
| 251 | resourcePool: pool.Name, |
| 252 | overallStatus: pool.OverallStatus, |
| 253 | cpuLimitMHz: pool.CpuLimit, |
| 254 | memoryLimitMiB: pool.MemLimit, |
| 255 | cpuReservation: pool.CpuReservation, |
| 256 | memReservation: pool.MemReservation, |
| 257 | }, mergeStringMaps(pool.Labels, map[string]string{ |
| 258 | "datacenter": pool.Hier.DC.Name, |
| 259 | "cluster": pool.Hier.Cluster.Name, |
| 260 | "resource_pool": pool.Name, |
| 261 | })) |
| 262 | if c.resources.Clusters.Get(pool.Hier.Cluster.ID) != nil { |
| 263 | builder.addLink(pool.Hier.Cluster.ID, pool.ID, vsphereTopologyOwnershipLink, "contains") |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | data, err := builder.data(strings.TrimSpace(agentID), time.Now().UTC(), map[string]any{ |
| 268 | "datacenters": len(c.resources.DataCenters), |
| 269 | "clusters": len(c.resources.Clusters), |
| 270 | "hosts": len(c.resources.Hosts), |
| 271 | "vms": len(c.resources.VMs), |
| 272 | "datastores": len(c.resources.Datastores), |
| 273 | "networks": len(c.resources.Networks), |
| 274 | "datastore_clusters": len(c.resources.StoragePods), |
| 275 | "resource_pools": len(c.resources.ResourcePools), |
| 276 | }) |
| 277 | if err != nil { |
| 278 | return topologyv1.Data{}, false |
| 279 | } |
| 280 | return data, true |
| 281 | } |
| 282 | |
| 283 | func optionalBool(value *bool) any { |
| 284 | if value == nil { |
| 285 | return nil |
| 286 | } |
| 287 | return *value |
| 288 | } |