master
go 490 lines 14.4 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package client
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "net/http"
10 "net/url"
11 "sync"
12 "time"
13
14 "github.com/vmware/govmomi"
15 "github.com/vmware/govmomi/object"
16 "github.com/vmware/govmomi/performance"
17 "github.com/vmware/govmomi/property"
18 "github.com/vmware/govmomi/session"
19 "github.com/vmware/govmomi/vapi/rest"
20 "github.com/vmware/govmomi/vapi/tags"
21 "github.com/vmware/govmomi/view"
22 "github.com/vmware/govmomi/vim25"
23 "github.com/vmware/govmomi/vim25/mo"
24 "github.com/vmware/govmomi/vim25/soap"
25 "github.com/vmware/govmomi/vim25/types"
26 vsanapi "github.com/vmware/govmomi/vsan"
27 vsanmethods "github.com/vmware/govmomi/vsan/methods"
28 vsantypes "github.com/vmware/govmomi/vsan/types"
29
30 "github.com/netdata/netdata/go/plugins/pkg/tlscfg"
31 )
32
33 const (
34 datacenter = "Datacenter"
35 folder = "Folder"
36 computeResource = "ComputeResource"
37 hostSystem = "HostSystem"
38 virtualMachine = "VirtualMachine"
39 datastoreType = "Datastore"
40 networkType = "Network"
41 storagePodType = "StoragePod"
42 resourcePoolType = "ResourcePool"
43
44 maxIdleConnections = 32
45 )
46
47 type Config struct {
48 URL string
49 User string
50 Password string
51 tlscfg.TLSConfig
52 Timeout time.Duration
53 }
54
55 type Client struct {
56 client *govmomi.Client
57 root *view.ContainerView
58 perf *performance.Manager
59 userInfo *url.Userinfo
60 rest *rest.Client
61 tags *tags.Manager
62 vsan *vsanapi.Client
63 lazyMu sync.Mutex
64 }
65
66 func newSoapClient(config Config) (*soap.Client, error) {
67 soapURL, err := soap.ParseURL(config.URL)
68 if err != nil {
69 return nil, fmt.Errorf("parse config option url for vSphere SOAP endpoint: %w", err)
70 }
71 if soapURL == nil {
72 return nil, errors.New("parse config option url for vSphere SOAP endpoint: empty SOAP URL")
73 }
74 soapURL.User = url.UserPassword(config.User, config.Password)
75 soapClient := soap.NewClient(soapURL, config.TLSConfig.InsecureSkipVerify)
76
77 tlsConfig, err := tlscfg.NewTLSConfig(config.TLSConfig)
78 if err != nil {
79 return nil, fmt.Errorf("build TLS configuration from tls_* options: %w", err)
80 }
81 if tlsConfig != nil && len(tlsConfig.Certificates) > 0 {
82 soapClient.SetCertificate(tlsConfig.Certificates[0])
83 }
84 if config.TLSConfig.TLSCA != "" {
85 if err := soapClient.SetRootCAs(config.TLSConfig.TLSCA); err != nil {
86 return nil, fmt.Errorf("load tls_ca certificate bundle %q for vSphere SOAP client: %w", config.TLSConfig.TLSCA, err)
87 }
88 }
89
90 if t, ok := soapClient.Transport.(*http.Transport); ok {
91 t.MaxIdleConnsPerHost = maxIdleConnections
92 t.TLSHandshakeTimeout = config.Timeout
93 }
94 soapClient.Timeout = config.Timeout
95
96 return soapClient, nil
97 }
98
99 func newContainerView(ctx context.Context, client *govmomi.Client) (*view.ContainerView, error) {
100 viewManager := view.NewManager(client.Client)
101 return viewManager.CreateContainerView(ctx, client.ServiceContent.RootFolder, []string{}, true)
102 }
103
104 var createContainerView = newContainerView
105
106 func newPerformanceManager(client *vim25.Client) *performance.Manager {
107 perfManager := performance.NewManager(client)
108 perfManager.Sort = true
109 return perfManager
110 }
111
112 func New(config Config) (*Client, error) {
113 ctx := context.Background()
114 soapClient, err := newSoapClient(config)
115 if err != nil {
116 return nil, fmt.Errorf("initialize vSphere SOAP client: %w", err)
117 }
118
119 vimClient, err := vim25.NewClient(ctx, soapClient)
120 if err != nil {
121 return nil, fmt.Errorf("initialize vSphere vim25 client and retrieve service content: %w", err)
122 }
123
124 vmomiClient := &govmomi.Client{
125 Client: vimClient,
126 SessionManager: session.NewManager(vimClient),
127 }
128
129 userInfo := url.UserPassword(config.User, config.Password)
130 addKeepAlive(vmomiClient, userInfo)
131
132 err = vmomiClient.Login(ctx, userInfo)
133 if err != nil {
134 return nil, fmt.Errorf("login to vSphere API with configured username: %w", err)
135 }
136
137 containerView, err := createContainerView(ctx, vmomiClient)
138 if err != nil {
139 return nil, fmt.Errorf("create root vSphere container view: %w", errors.Join(err, vmomiClient.Logout(ctx)))
140 }
141
142 perfManager := newPerformanceManager(vimClient)
143
144 client := &Client{
145 client: vmomiClient,
146 perf: perfManager,
147 root: containerView,
148 userInfo: userInfo,
149 }
150
151 return client, nil
152 }
153
154 func (c *Client) IsSessionActive() (bool, error) {
155 active, err := c.client.SessionManager.SessionIsActive(context.Background())
156 if err != nil {
157 return false, fmt.Errorf("check vSphere SOAP session activity: %w", err)
158 }
159 return active, nil
160 }
161
162 func (c *Client) Version() string {
163 return c.client.ServiceContent.About.Version
164 }
165
166 func (c *Client) InstanceUUID() string {
167 return c.client.ServiceContent.About.InstanceUuid
168 }
169
170 func (c *Client) Login(userinfo *url.Userinfo) error {
171 if err := c.client.Login(context.Background(), userinfo); err != nil {
172 return fmt.Errorf("login to vSphere SOAP API: %w", err)
173 }
174 return nil
175 }
176
177 func (c *Client) Logout() error {
178 if err := c.client.Logout(context.Background()); err != nil {
179 return fmt.Errorf("logout from vSphere SOAP API: %w", err)
180 }
181 return nil
182 }
183
184 func (c *Client) Close() error {
185 if c == nil {
186 return nil
187 }
188
189 ctx := context.Background()
190 var err error
191 if c.root != nil {
192 if e := c.root.Destroy(ctx); e != nil {
193 err = errors.Join(err, fmt.Errorf("destroy root vSphere container view: %w", e))
194 }
195 c.root = nil
196 }
197 c.lazyMu.Lock()
198 if c.rest != nil {
199 if e := c.rest.Logout(ctx); e != nil {
200 err = errors.Join(err, fmt.Errorf("logout from vSphere REST API: %w", e))
201 }
202 c.rest = nil
203 c.tags = nil
204 }
205 c.vsan = nil
206 c.userInfo = nil
207 c.lazyMu.Unlock()
208 if c.client != nil {
209 if e := c.client.Logout(ctx); e != nil {
210 err = errors.Join(err, fmt.Errorf("logout from vSphere SOAP API: %w", e))
211 }
212 }
213 return err
214 }
215
216 func (c *Client) PerformanceMetrics(pqs []types.PerfQuerySpec) ([]performance.EntityMetric, error) {
217 metrics, err := c.perf.Query(context.Background(), pqs)
218 if err != nil {
219 return nil, fmt.Errorf("query vSphere performance manager for %d perf query specs: %w", len(pqs), err)
220 }
221 series, err := c.perf.ToMetricSeries(context.Background(), metrics)
222 if err != nil {
223 return nil, fmt.Errorf("convert vSphere performance samples for %d perf query specs: %w", len(pqs), err)
224 }
225 return series, nil
226 }
227
228 func (c *Client) Datacenters(pathSet ...string) (dcs []mo.Datacenter, err error) {
229 err = c.root.Retrieve(context.Background(), []string{datacenter}, pathSet, &dcs)
230 return
231 }
232
233 func (c *Client) Folders(pathSet ...string) (folders []mo.Folder, err error) {
234 err = c.root.Retrieve(context.Background(), []string{folder}, pathSet, &folders)
235 return
236 }
237
238 func (c *Client) ComputeResources(pathSet ...string) (computes []mo.ComputeResource, err error) {
239 err = c.root.Retrieve(context.Background(), []string{computeResource}, pathSet, &computes)
240 return
241 }
242
243 func (c *Client) Hosts(pathSet ...string) (hosts []mo.HostSystem, err error) {
244 err = c.root.Retrieve(context.Background(), []string{hostSystem}, pathSet, &hosts)
245 return
246 }
247
248 func (c *Client) VirtualMachines(pathSet ...string) (vms []mo.VirtualMachine, err error) {
249 err = c.root.Retrieve(context.Background(), []string{virtualMachine}, pathSet, &vms)
250 return
251 }
252
253 func (c *Client) Datastores(pathSet ...string) (datastores []mo.Datastore, err error) {
254 err = c.root.Retrieve(context.Background(), []string{datastoreType}, pathSet, &datastores)
255 return
256 }
257
258 func (c *Client) Networks(pathSet ...string) (networks []mo.Network, err error) {
259 err = c.root.Retrieve(context.Background(), []string{networkType}, pathSet, &networks)
260 return
261 }
262
263 func (c *Client) StoragePods(pathSet ...string) (pods []mo.StoragePod, err error) {
264 err = c.root.Retrieve(context.Background(), []string{storagePodType}, pathSet, &pods)
265 return
266 }
267
268 func (c *Client) DatastoresByRef(refs []types.ManagedObjectReference, pathSet ...string) ([]mo.Datastore, error) {
269 if len(refs) == 0 {
270 return nil, nil
271 }
272 var datastores []mo.Datastore
273 pc := property.DefaultCollector(c.client.Client)
274 err := pc.Retrieve(context.Background(), refs, pathSet, &datastores)
275 if err != nil {
276 return nil, fmt.Errorf("retrieve datastore properties for %d refs pathSet=%v: %w", len(refs), pathSet, err)
277 }
278 return datastores, nil
279 }
280
281 func (c *Client) ClustersByRef(refs []types.ManagedObjectReference, pathSet ...string) ([]mo.ClusterComputeResource, error) {
282 if len(refs) == 0 {
283 return nil, nil
284 }
285 var clusters []mo.ClusterComputeResource
286 pc := property.DefaultCollector(c.client.Client)
287 err := pc.Retrieve(context.Background(), refs, pathSet, &clusters)
288 if err != nil {
289 return nil, fmt.Errorf("retrieve cluster properties for %d refs pathSet=%v: %w", len(refs), pathSet, err)
290 }
291 return clusters, nil
292 }
293
294 func (c *Client) ResourcePools(pathSet ...string) (pools []mo.ResourcePool, err error) {
295 err = c.root.Retrieve(context.Background(), []string{resourcePoolType}, pathSet, &pools)
296 return
297 }
298
299 func (c *Client) ResourcePoolsByRef(refs []types.ManagedObjectReference, pathSet ...string) ([]mo.ResourcePool, error) {
300 if len(refs) == 0 {
301 return nil, nil
302 }
303 var pools []mo.ResourcePool
304 pc := property.DefaultCollector(c.client.Client)
305 err := pc.Retrieve(context.Background(), refs, pathSet, &pools)
306 if err != nil {
307 return nil, fmt.Errorf("retrieve resource pool properties for %d refs pathSet=%v: %w", len(refs), pathSet, err)
308 }
309 return pools, nil
310 }
311
312 func (c *Client) CustomFields() ([]types.CustomFieldDef, error) {
313 m, err := object.GetCustomFieldsManager(c.client.Client)
314 if err != nil {
315 return nil, fmt.Errorf("get vSphere custom fields manager: %w", err)
316 }
317 fields, err := m.Field(context.Background())
318 if err != nil {
319 return nil, fmt.Errorf("list vSphere custom field definitions: %w", err)
320 }
321 return fields, nil
322 }
323
324 func (c *Client) TagsByRef(refs []types.ManagedObjectReference) (map[types.ManagedObjectReference]map[string][]string, error) {
325 if len(refs) == 0 {
326 return nil, nil
327 }
328
329 ctx := context.Background()
330 manager, err := c.tagManager(ctx)
331 if err != nil {
332 return nil, fmt.Errorf("initialize vSphere tag manager: %w", err)
333 }
334
335 categories, err := manager.GetCategories(ctx)
336 if err != nil {
337 return nil, fmt.Errorf("list vSphere tag categories: %w", err)
338 }
339 categoriesByID := make(map[string]string, len(categories))
340 for _, category := range categories {
341 categoriesByID[category.ID] = category.Name
342 }
343
344 tagList, err := manager.GetTags(ctx)
345 if err != nil {
346 return nil, fmt.Errorf("list vSphere tags: %w", err)
347 }
348 tagsByID := make(map[string]tags.Tag, len(tagList))
349 for _, tag := range tagList {
350 tagsByID[tag.ID] = tag
351 }
352
353 out := make(map[types.ManagedObjectReference]map[string][]string)
354 for i := 0; i < len(refs); i += maxTagAssociationBatchSize {
355 end := min(i+maxTagAssociationBatchSize, len(refs))
356 batch := make([]mo.Reference, 0, end-i)
357 for _, ref := range refs[i:end] {
358 batch = append(batch, ref)
359 }
360
361 attached, err := manager.ListAttachedTagsOnObjects(ctx, batch)
362 if err != nil {
363 return nil, fmt.Errorf("list vSphere tag attachments for refs batch offset=%d size=%d: %w", i, len(batch), err)
364 }
365 for _, objectTags := range attached {
366 ref := objectTags.ObjectID.Reference()
367 for _, tagID := range objectTags.TagIDs {
368 tag, ok := tagsByID[tagID]
369 if !ok || tag.Name == "" {
370 continue
371 }
372 category := categoriesByID[tag.CategoryID]
373 if category == "" {
374 continue
375 }
376 if out[ref] == nil {
377 out[ref] = make(map[string][]string)
378 }
379 out[ref][category] = append(out[ref][category], tag.Name)
380 }
381 }
382 }
383
384 return out, nil
385 }
386
387 func (c *Client) tagManager(ctx context.Context) (*tags.Manager, error) {
388 c.lazyMu.Lock()
389 defer c.lazyMu.Unlock()
390
391 if c.tags != nil {
392 return c.tags, nil
393 }
394 restClient := rest.NewClient(c.client.Client)
395 if err := restClient.Login(ctx, c.userInfo); err != nil {
396 return nil, fmt.Errorf("login to vSphere REST API for tag collection: %w", err)
397 }
398 c.rest = restClient
399 c.tags = tags.NewManager(restClient)
400 return c.tags, nil
401 }
402
403 func (c *Client) VSANPerfMetrics(cluster types.ManagedObjectReference, specs []vsantypes.VsanPerfQuerySpec) ([]vsantypes.VsanPerfEntityMetricCSV, error) {
404 if len(specs) == 0 {
405 return nil, nil
406 }
407 ctx := context.Background()
408 cli, err := c.vsanClient(ctx)
409 if err != nil {
410 return nil, fmt.Errorf("initialize vSAN client for cluster %s: %w", cluster.Value, err)
411 }
412 metrics, err := cli.VsanPerfQueryPerf(ctx, &cluster, specs)
413 if err != nil {
414 return nil, fmt.Errorf("query vSAN performance metrics for cluster %s with %d specs: %w", cluster.Value, len(specs), err)
415 }
416 return metrics, nil
417 }
418
419 func (c *Client) VSANSpaceUsage(cluster types.ManagedObjectReference) (*vsantypes.VsanSpaceUsage, error) {
420 ctx := context.Background()
421 cli, err := c.vsanClient(ctx)
422 if err != nil {
423 return nil, fmt.Errorf("initialize vSAN client for cluster %s: %w", cluster.Value, err)
424 }
425 req := vsantypes.VsanQuerySpaceUsage{
426 This: vsanSpaceReportSystemInstance,
427 Cluster: cluster,
428 }
429 res, err := vsanmethods.VsanQuerySpaceUsage(ctx, cli, &req)
430 if err != nil {
431 return nil, fmt.Errorf("query vSAN space usage for cluster %s: %w", cluster.Value, err)
432 }
433 return &res.Returnval, nil
434 }
435
436 func (c *Client) VSANHealth(cluster types.ManagedObjectReference) (string, error) {
437 ctx := context.Background()
438 cli, err := c.vsanClient(ctx)
439 if err != nil {
440 return "", fmt.Errorf("initialize vSAN client for cluster %s: %w", cluster.Value, err)
441 }
442 fetchFromCache := true
443 req := vsantypes.VsanQueryVcClusterHealthSummary{
444 This: vsanClusterHealthSystemInstance,
445 Cluster: &cluster,
446 Fields: []string{"overallHealth", "overallHealthDescription"},
447 FetchFromCache: &fetchFromCache,
448 }
449 res, err := vsanmethods.VsanQueryVcClusterHealthSummary(ctx, cli, &req)
450 if err != nil {
451 return "", fmt.Errorf("query vSAN health summary for cluster %s: %w", cluster.Value, err)
452 }
453 return res.Returnval.OverallHealth, nil
454 }
455
456 func (c *Client) vsanClient(ctx context.Context) (*vsanapi.Client, error) {
457 c.lazyMu.Lock()
458 defer c.lazyMu.Unlock()
459
460 if c.vsan != nil {
461 return c.vsan, nil
462 }
463 cli, err := vsanapi.NewClient(ctx, c.client.Client)
464 if err != nil {
465 return nil, fmt.Errorf("create govmomi vSAN API client: %w", err)
466 }
467 c.vsan = cli
468 return c.vsan, nil
469 }
470
471 func (c *Client) CounterInfoByName() (map[string]*types.PerfCounterInfo, error) {
472 counters, err := c.perf.CounterInfoByName(context.Background())
473 if err != nil {
474 return nil, fmt.Errorf("list vSphere performance counter registry: %w", err)
475 }
476 return counters, nil
477 }
478
479 const maxTagAssociationBatchSize = 2000
480
481 var (
482 vsanSpaceReportSystemInstance = types.ManagedObjectReference{
483 Type: "VsanSpaceReportSystem",
484 Value: "vsan-cluster-space-report-system",
485 }
486 vsanClusterHealthSystemInstance = types.ManagedObjectReference{
487 Type: "VsanVcClusterHealthSystem",
488 Value: "vsan-cluster-health-system",
489 }
490 )