diff options
Diffstat (limited to 'src/go/collectors/go.d.plugin/modules/vsphere/client')
3 files changed, 400 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/vsphere/client/client.go b/src/go/collectors/go.d.plugin/modules/vsphere/client/client.go new file mode 100644 index 000000000..827351cf8 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/vsphere/client/client.go @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package client + +import ( + "context" + "net/http" + "net/url" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/tlscfg" + + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/performance" + "github.com/vmware/govmomi/session" + "github.com/vmware/govmomi/view" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/soap" + "github.com/vmware/govmomi/vim25/types" +) + +const ( + datacenter = "Datacenter" + folder = "Folder" + computeResource = "ComputeResource" + hostSystem = "HostSystem" + virtualMachine = "VirtualMachine" + + maxIdleConnections = 32 +) + +type Config struct { + URL string + User string + Password string + tlscfg.TLSConfig + Timeout time.Duration +} + +type Client struct { + client *govmomi.Client + root *view.ContainerView + perf *performance.Manager +} + +func newSoapClient(config Config) (*soap.Client, error) { + soapURL, err := soap.ParseURL(config.URL) + if err != nil || soapURL == nil { + return nil, err + } + soapURL.User = url.UserPassword(config.User, config.Password) + soapClient := soap.NewClient(soapURL, config.TLSConfig.InsecureSkipVerify) + + tlsConfig, err := tlscfg.NewTLSConfig(config.TLSConfig) + if err != nil { + return nil, err + } + if tlsConfig != nil && len(tlsConfig.Certificates) > 0 { + soapClient.SetCertificate(tlsConfig.Certificates[0]) + } + if config.TLSConfig.TLSCA != "" { + if err := soapClient.SetRootCAs(config.TLSConfig.TLSCA); err != nil { + return nil, err + } + } + + if t, ok := soapClient.Transport.(*http.Transport); ok { + t.MaxIdleConnsPerHost = maxIdleConnections + t.TLSHandshakeTimeout = config.Timeout + } + soapClient.Timeout = config.Timeout + + return soapClient, nil +} + +func newContainerView(ctx context.Context, client *govmomi.Client) (*view.ContainerView, error) { + viewManager := view.NewManager(client.Client) + return viewManager.CreateContainerView(ctx, client.ServiceContent.RootFolder, []string{}, true) +} + +func newPerformanceManager(client *vim25.Client) *performance.Manager { + perfManager := performance.NewManager(client) + perfManager.Sort = true + return perfManager +} + +func New(config Config) (*Client, error) { + ctx := context.Background() + soapClient, err := newSoapClient(config) + if err != nil { + return nil, err + } + + vimClient, err := vim25.NewClient(ctx, soapClient) + if err != nil { + return nil, err + } + + vmomiClient := &govmomi.Client{ + Client: vimClient, + SessionManager: session.NewManager(vimClient), + } + + userInfo := url.UserPassword(config.User, config.Password) + addKeepAlive(vmomiClient, userInfo) + + err = vmomiClient.Login(ctx, userInfo) + if err != nil { + return nil, err + } + + containerView, err := newContainerView(ctx, vmomiClient) + if err != nil { + return nil, err + } + + perfManager := newPerformanceManager(vimClient) + + client := &Client{ + client: vmomiClient, + perf: perfManager, + root: containerView, + } + + return client, nil +} + +func (c *Client) IsSessionActive() (bool, error) { + return c.client.SessionManager.SessionIsActive(context.Background()) +} + +func (c *Client) Version() string { + return c.client.ServiceContent.About.Version +} + +func (c *Client) Login(userinfo *url.Userinfo) error { + return c.client.Login(context.Background(), userinfo) +} + +func (c *Client) Logout() error { + return c.client.Logout(context.Background()) +} + +func (c *Client) PerformanceMetrics(pqs []types.PerfQuerySpec) ([]performance.EntityMetric, error) { + metrics, err := c.perf.Query(context.Background(), pqs) + if err != nil { + return nil, err + } + return c.perf.ToMetricSeries(context.Background(), metrics) +} + +func (c *Client) Datacenters(pathSet ...string) (dcs []mo.Datacenter, err error) { + err = c.root.Retrieve(context.Background(), []string{datacenter}, pathSet, &dcs) + return +} + +func (c *Client) Folders(pathSet ...string) (folders []mo.Folder, err error) { + err = c.root.Retrieve(context.Background(), []string{folder}, pathSet, &folders) + return +} + +func (c *Client) ComputeResources(pathSet ...string) (computes []mo.ComputeResource, err error) { + err = c.root.Retrieve(context.Background(), []string{computeResource}, pathSet, &computes) + return +} + +func (c *Client) Hosts(pathSet ...string) (hosts []mo.HostSystem, err error) { + err = c.root.Retrieve(context.Background(), []string{hostSystem}, pathSet, &hosts) + return +} + +func (c *Client) VirtualMachines(pathSet ...string) (vms []mo.VirtualMachine, err error) { + err = c.root.Retrieve(context.Background(), []string{virtualMachine}, pathSet, &vms) + return +} + +func (c *Client) CounterInfoByName() (map[string]*types.PerfCounterInfo, error) { + return c.perf.CounterInfoByName(context.Background()) +} diff --git a/src/go/collectors/go.d.plugin/modules/vsphere/client/client_test.go b/src/go/collectors/go.d.plugin/modules/vsphere/client/client_test.go new file mode 100644 index 000000000..163829f41 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/vsphere/client/client_test.go @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package client + +import ( + "crypto/tls" + "net/url" + "testing" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/tlscfg" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" +) + +func TestNew(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + v, err := client.IsSessionActive() + assert.NoError(t, err) + assert.True(t, v) +} + +func TestClient_Version(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + assert.NotEmpty(t, client.Version()) +} + +func TestClient_CounterInfoByName(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + v, err := client.CounterInfoByName() + assert.NoError(t, err) + assert.IsType(t, map[string]*types.PerfCounterInfo{}, v) + assert.NotEmpty(t, v) +} + +func TestClient_IsSessionActive(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + v, err := client.IsSessionActive() + assert.NoError(t, err) + assert.True(t, v) +} + +func TestClient_Login(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + assert.NoError(t, client.Logout()) + + err := client.Login(url.UserPassword("admin", "password")) + assert.NoError(t, err) + + ok, err := client.IsSessionActive() + assert.NoError(t, err) + assert.True(t, ok) +} + +func TestClient_Logout(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + assert.NoError(t, client.Logout()) + + v, err := client.IsSessionActive() + assert.NoError(t, err) + assert.False(t, v) +} + +func TestClient_Datacenters(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + dcs, err := client.Datacenters() + assert.NoError(t, err) + assert.NotEmpty(t, dcs) +} + +func TestClient_Folders(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + folders, err := client.Folders() + assert.NoError(t, err) + assert.NotEmpty(t, folders) +} + +func TestClient_ComputeResources(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + computes, err := client.ComputeResources() + assert.NoError(t, err) + assert.NotEmpty(t, computes) +} + +func TestClient_Hosts(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + hosts, err := client.Hosts() + assert.NoError(t, err) + assert.NotEmpty(t, hosts) +} + +func TestClient_VirtualMachines(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + vms, err := client.VirtualMachines() + assert.NoError(t, err) + assert.NotEmpty(t, vms) +} + +func TestClient_PerformanceMetrics(t *testing.T) { + client, teardown := prepareClient(t) + defer teardown() + + hosts, err := client.Hosts() + require.NoError(t, err) + metrics, err := client.PerformanceMetrics(hostsPerfQuerySpecs(hosts)) + require.NoError(t, err) + assert.True(t, len(metrics) > 0) +} + +func prepareClient(t *testing.T) (client *Client, teardown func()) { + model, srv := createSim(t) + teardown = func() { model.Remove(); srv.Close() } + return newClient(t, srv.URL), teardown +} + +func newClient(t *testing.T, vCenterURL *url.URL) *Client { + client, err := New(Config{ + URL: vCenterURL.String(), + User: "admin", + Password: "password", + Timeout: time.Second * 3, + TLSConfig: tlscfg.TLSConfig{InsecureSkipVerify: true}, + }) + require.NoError(t, err) + return client +} + +func createSim(t *testing.T) (*simulator.Model, *simulator.Server) { + model := simulator.VPX() + err := model.Create() + require.NoError(t, err) + model.Service.TLS = new(tls.Config) + return model, model.Service.NewServer() +} + +func hostsPerfQuerySpecs(hosts []mo.HostSystem) []types.PerfQuerySpec { + var pqs []types.PerfQuerySpec + for _, host := range hosts { + pq := types.PerfQuerySpec{ + Entity: host.Reference(), + MaxSample: 1, + MetricId: []types.PerfMetricId{{CounterId: 32, Instance: ""}}, + IntervalId: 20, + Format: "normal", + } + pqs = append(pqs, pq) + } + return pqs +} diff --git a/src/go/collectors/go.d.plugin/modules/vsphere/client/keepalive.go b/src/go/collectors/go.d.plugin/modules/vsphere/client/keepalive.go new file mode 100644 index 000000000..0ce1ef5c0 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/vsphere/client/keepalive.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package client + +import ( + "context" + "net/url" + "time" + + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/session" + "github.com/vmware/govmomi/vim25/methods" + "github.com/vmware/govmomi/vim25/soap" + "github.com/vmware/govmomi/vim25/types" +) + +const ( + keepAliveEvery = time.Second * 15 +) + +// TODO: survive vCenter reboot, it looks like we need to re New() +func addKeepAlive(client *govmomi.Client, userinfo *url.Userinfo) { + f := func(rt soap.RoundTripper) error { + _, err := methods.GetCurrentTime(context.Background(), rt) + if err == nil { + return nil + } + + if !isNotAuthenticated(err) { + return nil + } + + _ = client.Login(context.Background(), userinfo) + return nil + } + client.Client.RoundTripper = session.KeepAliveHandler(client.Client.RoundTripper, keepAliveEvery, f) +} + +func isNotAuthenticated(err error) bool { + if !soap.IsSoapFault(err) { + return false + } + _, ok := soap.ToSoapFault(err).VimFault().(*types.NotAuthenticated) + return ok +} |