Skip to content

Commit

Permalink
Merge pull request vmware-tanzu#381 from akutz/feature/refactored-pro…
Browse files Browse the repository at this point in the history
…vider-client

✨ Refactor vSphere provider client to pkg/util/vsphere/client
  • Loading branch information
akutz authored Feb 8, 2024
2 parents cb30275 + b3b6f71 commit 5018d22
Show file tree
Hide file tree
Showing 10 changed files with 1,152 additions and 702 deletions.
300 changes: 300 additions & 0 deletions pkg/util/vsphere/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// Copyright (c) 2018-2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package client

import (
"context"
"net"
"net/url"
"time"

"github.com/pkg/errors"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/session/keepalive"
"github.com/vmware/govmomi/vapi/rest"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/methods"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"

"github.com/go-logr/logr"
)

type Config struct {
Host string
Port string
Username string
Password string
CAFilePath string
Insecure bool
Datacenter string
}

type Client struct {
vimClient *vim25.Client
restClient *rest.Client
sessionManager *session.Manager
config Config

finder *find.Finder
datacenter *object.Datacenter
}

// NewClient returns a new client.
func NewClient(ctx context.Context, config Config) (*Client, error) {
vimClient, sm, err := NewVimClient(ctx, config)
if err != nil {
return nil, err
}

finder, datacenter, err := newFinder(ctx, vimClient, config)
if err != nil {
return nil, err
}

restClient, err := newRestClient(ctx, vimClient, config)
if err != nil {
return nil, err
}

return &Client{
vimClient: vimClient,
finder: finder,
datacenter: datacenter,
restClient: restClient,
sessionManager: sm,
config: config,
}, nil
}

// Idle time before a keepalive will be invoked.
const keepAliveIdleTime = 5 * time.Minute

// SoapKeepAliveHandlerFn returns a keepalive handler function suitable for use
// with the SOAP handler. In case the connectivity to VC is down long enough,
// the session expires. Further attempts to use the client yield
// NotAuthenticated fault. This handler ensures that we re-login the client in
// those scenarios.
func SoapKeepAliveHandlerFn(
ctx context.Context,
sc *soap.Client,
sm *session.Manager,
userInfo *url.Userinfo) func() error {

log := logr.FromContextOrDiscard(ctx).WithName("SoapKeepAliveHandlerFn")

return func() error {
ctx := context.Background()
if _, err := methods.GetCurrentTime(ctx, sc); err != nil && isNotAuthenticatedError(err) {
log.Info("Re-authenticating vim client")
if err = sm.Login(ctx, userInfo); err != nil {
if isInvalidLogin(err) {
log.Error(err, "Invalid login in keepalive handler", "url", sc.URL())
return err
}
}
} else if err != nil {
log.Error(err, "Error in vim25 client's keepalive handler", "url", sc.URL())
}

return nil
}
}

// RestKeepAliveHandlerFn returns a keepalive handler function suitable for use
// with the REST handler. Similar to the SOAP handler, we customize the handler
// here so we can re-login the client in case the REST session expires due to
// connectivity issues.
func RestKeepAliveHandlerFn(
ctx context.Context,
c *rest.Client,
userInfo *url.Userinfo) func() error {

log := logr.FromContextOrDiscard(ctx).WithName("RestKeepAliveHandlerFn")

return func() error {
ctx := context.Background()
if sess, err := c.Session(ctx); err == nil && sess == nil {
// session is Unauthorized.
log.Info("Re-authenticating REST client")
if err = c.Login(ctx, userInfo); err != nil {
log.Error(err, "Invalid login in keepalive handler", "url", c.URL())
return err
}
} else if err != nil {
log.Error(err, "Error in rest client's keepalive handler", "url", c.URL())
}

return nil
}
}

// newRestClient creates a rest client which is configured to use a custom keepalive handler function.
func newRestClient(
ctx context.Context,
vimClient *vim25.Client,
config Config) (*rest.Client, error) {

log := logr.FromContextOrDiscard(ctx).WithName("newRestClient")

log.Info("Creating new REST Client", "VcPNID", config.Host, "VcPort", config.Port)
restClient := rest.NewClient(vimClient)

userInfo := url.UserPassword(config.Username, config.Password)

// Set a custom keepalive handler function
restClient.Transport = keepalive.NewHandlerREST(
restClient,
keepAliveIdleTime,
RestKeepAliveHandlerFn(ctx, restClient, userInfo))

// Initial login. This will also start the keepalive.
if err := restClient.Login(ctx, userInfo); err != nil {
// Log message used by VMC LINT. Refer to before making changes
return nil, errors.Wrapf(err, "login failed for url: %v", vimClient.URL())
}

return restClient, nil
}

// NewVimClient creates a new vim25 client which is configured to use a custom
// keepalive handler function.
func NewVimClient(
ctx context.Context,
config Config) (*vim25.Client, *session.Manager, error) {

log := logr.FromContextOrDiscard(ctx).WithName("NewVimClient")

log.Info("Creating new vim Client", "VcPNID", config.Host, "VcPort", config.Port)
soapURL, err := soap.ParseURL(net.JoinHostPort(config.Host, config.Port))
if err != nil {
return nil, nil, errors.Wrapf(
err, "failed to parse %s:%s", config.Host, config.Port)
}

soapClient := soap.NewClient(soapURL, config.Insecure)
if config.CAFilePath != "" {
err = soapClient.SetRootCAs(config.CAFilePath)
if err != nil {
return nil, nil, errors.Wrapf(
err, "failed to set root CA %s", config.CAFilePath)
}
}

vimClient, err := vim25.NewClient(ctx, soapClient)
if err != nil {
return nil, nil, errors.Wrapf(
err, "error creating a new vim client for url: %v", soapURL)
}

if err := vimClient.UseServiceVersion(); err != nil {
return nil, nil, errors.Wrapf(
err, "error setting vim client version for url: %v", soapURL)
}

userInfo := url.UserPassword(config.Username, config.Password)
sm := session.NewManager(vimClient)

// Set a custom keepalive handler function
vimClient.RoundTripper = keepalive.NewHandlerSOAP(
soapClient,
keepAliveIdleTime,
SoapKeepAliveHandlerFn(ctx, soapClient, sm, userInfo))

// Initial login. This will also start the keepalive.
if err = sm.Login(ctx, userInfo); err != nil {
// Log message used by VMC LINT. Refer to before making changes
return nil, nil, errors.Wrapf(
err, "login failed for url: %v", soapURL)
}

return vimClient, sm, err
}

func newFinder(
ctx context.Context,
vimClient *vim25.Client,
config Config) (*find.Finder, *object.Datacenter, error) {

finder := find.NewFinder(vimClient, false)

dcRef, err := finder.ObjectReference(
ctx,
types.ManagedObjectReference{
Type: "Datacenter",
Value: config.Datacenter,
})
if err != nil {
return nil, nil, errors.Wrapf(
err, "failed to find Datacenter %q", config.Datacenter)
}

dc := dcRef.(*object.Datacenter)
finder.SetDatacenter(dc)

return finder, dc, nil
}

func isNotAuthenticatedError(err error) bool {
if soap.IsSoapFault(err) {
vimFault := soap.ToSoapFault(err).VimFault()
if _, ok := vimFault.(types.NotAuthenticated); ok {
return true
}
}

return false
}

func isInvalidLogin(err error) bool {
if soap.IsSoapFault(err) {
vimFault := soap.ToSoapFault(err).VimFault()
if _, ok := vimFault.(types.InvalidLogin); ok {
return true
}
}

return false
}

func (c *Client) VimClient() *vim25.Client {
return c.vimClient
}

func (c *Client) Finder() *find.Finder {
return c.finder
}

func (c *Client) Datacenter() *object.Datacenter {
return c.datacenter
}

func (c *Client) RestClient() *rest.Client {
return c.restClient
}

func (c *Client) Config() Config {
return c.config
}

func (c *Client) Logout(ctx context.Context) {
log := logr.FromContextOrDiscard(ctx).WithName("Logout")

clientURL := c.vimClient.URL()
log.Info("vsphere client logging out from", "VC", clientURL.Host)

if err := c.sessionManager.Logout(ctx); err != nil {
log.Error(err, "Error logging out the vim25 session",
"username", clientURL.User.Username(),
"host", clientURL.Host)
}

if err := c.restClient.Logout(ctx); err != nil {
log.Error(err, "Error logging out the rest session",
"username", clientURL.User.Username(),
"host", clientURL.Host)
}
}
16 changes: 16 additions & 0 deletions pkg/util/vsphere/client/client_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2019-2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package client_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestClient(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "vSphere Client Suite")
}
Loading

0 comments on commit 5018d22

Please sign in to comment.