454 lines
12 KiB
Go
454 lines
12 KiB
Go
// Copyright 2016-2018 VMware, Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package session caches vSphere objects to avoid having to repeatedly
|
|
// make govmomi client calls.
|
|
//
|
|
// To obtain a Session, call Create with a Config. The config
|
|
// contains the SDK URL (Service) and the desired vSphere resources.
|
|
// Create then connects to Service and stores govmomi objects for
|
|
// each corresponding value in Config. The Session is returned and
|
|
// the user can use the cached govmomi objects in the exported fields of
|
|
// Session instead of directly using a govmomi Client.
|
|
//
|
|
package session
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
|
|
"github.com/vmware/govmomi"
|
|
"github.com/vmware/govmomi/find"
|
|
"github.com/vmware/govmomi/object"
|
|
"github.com/vmware/govmomi/session"
|
|
"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/vmware/vic/lib/config"
|
|
"github.com/vmware/vic/pkg/errors"
|
|
"github.com/vmware/vic/pkg/trace"
|
|
"github.com/vmware/vic/pkg/vsphere/extraconfig"
|
|
)
|
|
|
|
const (
|
|
defaultMaxInFlight = 32
|
|
tlsHandshakeTimeout = 30 * time.Second
|
|
)
|
|
|
|
// Config contains the configuration used to create a Session.
|
|
type Config struct {
|
|
// SDK URL or proxy
|
|
Service string
|
|
// Credentials
|
|
User *url.Userinfo
|
|
// CloneTicket is used to clone an existing session
|
|
CloneTicket string
|
|
// Allow insecure connection to Service
|
|
Insecure bool
|
|
// Target thumbprint
|
|
Thumbprint string
|
|
// Keep alive duration
|
|
Keepalive time.Duration
|
|
// User-Agent to identify login sessions (see: govc session.ls)
|
|
UserAgent string
|
|
|
|
ClusterPath string
|
|
DatacenterPath string
|
|
DatastorePath string
|
|
HostPath string
|
|
PoolPath string
|
|
}
|
|
|
|
// Session caches vSphere objects obtained by querying the SDK.
|
|
type Session struct {
|
|
*govmomi.Client
|
|
|
|
*Config
|
|
|
|
Cluster *object.ComputeResource
|
|
Datacenter *object.Datacenter
|
|
Datastore *object.Datastore
|
|
Host *object.HostSystem
|
|
Pool *object.ResourcePool
|
|
|
|
// Default vSphere VMFolder
|
|
VMFolder *object.Folder
|
|
// Folder where appliance is located
|
|
VCHFolder *object.Folder
|
|
|
|
Finder *find.Finder
|
|
|
|
DRSEnabled *bool
|
|
}
|
|
|
|
// RoundTripFunc alias
|
|
type RoundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
// RoundTrip method
|
|
func (rt RoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
return rt(r)
|
|
}
|
|
|
|
// LimitConcurrency limits how many requests can be processed at once
|
|
func LimitConcurrency(rt http.RoundTripper, limit int) http.RoundTripper {
|
|
limiter := make(chan struct{}, limit)
|
|
|
|
return RoundTripFunc(func(r *http.Request) (*http.Response, error) {
|
|
// reserve a slot
|
|
limiter <- struct{}{}
|
|
|
|
// free the slot
|
|
defer func() {
|
|
<-limiter
|
|
}()
|
|
|
|
// use the given round tripper
|
|
return rt.RoundTrip(r)
|
|
})
|
|
}
|
|
|
|
// NewSession creates a new Session struct.
|
|
func NewSession(config *Config) *Session {
|
|
return &Session{Config: config}
|
|
}
|
|
|
|
// Vim25 returns the vim25.Client to the caller
|
|
func (s *Session) Vim25() *vim25.Client {
|
|
return s.Client.Client
|
|
}
|
|
|
|
// IsVC returns whether the session is backed by VC
|
|
func (s *Session) IsVC() bool {
|
|
return s.Client.IsVC()
|
|
}
|
|
|
|
// IsVSAN returns whether the datastore used in the session is backed by VSAN
|
|
func (s *Session) IsVSAN(ctx context.Context) bool {
|
|
// #nosec: Errors unhandled.
|
|
dsType, _ := s.Datastore.Type(ctx)
|
|
|
|
return dsType == types.HostFileSystemVolumeFileSystemTypeVsan
|
|
}
|
|
|
|
// Create accepts a Config and returns a Session with the cached vSphere resources.
|
|
func (s *Session) Create(ctx context.Context) (*Session, error) {
|
|
op := trace.FromContext(ctx, "Create")
|
|
|
|
var vchConfig config.VirtualContainerHostConfigSpec
|
|
var connConfig config.Connection
|
|
|
|
source, err := extraconfig.GuestInfoSource()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prefix := extraconfig.CalculateKeys(vchConfig, "Connection", "")
|
|
if len(prefix) != 1 {
|
|
return nil, fmt.Errorf("must be exactly one Connection defined in VCH configuration")
|
|
}
|
|
extraconfig.DecodeWithPrefix(source, &connConfig, prefix[0])
|
|
|
|
s.Service = connConfig.Target
|
|
s.User = url.UserPassword(connConfig.Username, connConfig.Token)
|
|
s.Thumbprint = connConfig.TargetThumbprint
|
|
|
|
_, err = s.Connect(op)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// we're treating this as an atomic behaviour, so log out if we failed
|
|
defer func() {
|
|
if err != nil {
|
|
// #nosec: Errors unhandled.
|
|
s.Client.Logout(op)
|
|
}
|
|
}()
|
|
|
|
_, err = s.Populate(op)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Connect establishes the connection for the session but nothing more
|
|
func (s *Session) Connect(ctx context.Context) (*Session, error) {
|
|
op := trace.FromContext(ctx, "Connect")
|
|
|
|
op.Debugf("Creating VMOMI session with thumbprint %s", s.Thumbprint)
|
|
|
|
soapURL, err := soap.ParseURL(s.Service)
|
|
if soapURL == nil || err != nil {
|
|
return nil, SDKURLError{
|
|
Service: s.Service,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
// Update the service URL with expanded defaults
|
|
s.Service = soapURL.String()
|
|
|
|
// VCH components do not include credentials within the target URL
|
|
if s.User != nil {
|
|
soapURL.User = s.User
|
|
}
|
|
|
|
soapClient := soap.NewClient(soapURL, s.Insecure)
|
|
|
|
soapClient.Version = "6.0" // Pin to 6.0 until we need 6.5+ specific API
|
|
|
|
var login func(context.Context) error
|
|
|
|
login = func(ctx context.Context) error {
|
|
return s.Client.Login(ctx, soapURL.User)
|
|
}
|
|
|
|
soapClient.UserAgent = s.UserAgent
|
|
if s.UserAgent == "" {
|
|
op.Debug("DEVNOTICE: Session created with default user agent.")
|
|
}
|
|
|
|
soapClient.SetThumbprint(soapURL.Host, s.Thumbprint)
|
|
|
|
maxInFlight := defaultMaxInFlight
|
|
if e := os.Getenv("VIC_MAX_IN_FLIGHT"); e != "" {
|
|
if i, err := strconv.Atoi(e); err == nil {
|
|
maxInFlight = i
|
|
}
|
|
}
|
|
// Limit the concurrency of SOAP requests
|
|
if t, ok := soapClient.Transport.(*http.Transport); ok {
|
|
t.MaxIdleConnsPerHost = maxInFlight
|
|
t.TLSHandshakeTimeout = tlsHandshakeTimeout
|
|
}
|
|
soapClient.Transport = LimitConcurrency(soapClient.Transport, maxInFlight)
|
|
|
|
// TODO: option to set http.Client.Transport.TLSClientConfig.RootCAs
|
|
vimClient, err := vim25.NewClient(op, soapClient)
|
|
if err != nil {
|
|
return nil, SoapClientError{
|
|
Host: soapURL.Host,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
if s.Keepalive != 0 {
|
|
vimClient.RoundTripper = session.KeepAliveHandler(soapClient, s.Keepalive,
|
|
func(roundTripper soap.RoundTripper) error {
|
|
cop := trace.FromOperation(op, "KeepAlive")
|
|
|
|
_, err := methods.GetCurrentTime(cop, roundTripper)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
cop.Warnf("session keepalive error: %s", err)
|
|
|
|
if isNotAuthenticated(err) {
|
|
|
|
if err = login(cop); err != nil {
|
|
cop.Errorf("session keepalive failed to re-authenticate: %s", err)
|
|
} else {
|
|
cop.Info("session keepalive re-authenticated")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// TODO: get rid of govmomi.Client usage, only provides a few helpers we don't need.
|
|
s.Client = &govmomi.Client{
|
|
Client: vimClient,
|
|
SessionManager: session.NewManager(vimClient),
|
|
}
|
|
|
|
if s.CloneTicket != "" {
|
|
// clone a user session if we have a ticket
|
|
err = s.SessionManager.CloneSession(op, s.CloneTicket)
|
|
} else {
|
|
// otherwise login to create a new one
|
|
err = login(op)
|
|
}
|
|
if err != nil {
|
|
return nil, UserPassLoginError{
|
|
Host: soapURL.Host,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
s.Finder = find.NewFinder(s.Vim25(), false)
|
|
// log high-level environment information
|
|
s.logEnvironmentInfo(op)
|
|
return s, nil
|
|
}
|
|
|
|
// Populate caches resources on the session object. These resources
|
|
// are based off of the config provided at session creation.
|
|
//
|
|
// vic specific:
|
|
// The values that end in Path (DataCenterPath, ClusterPath, etc..) are
|
|
// either from the CLI or have been retreived from the appliance extraConfig
|
|
func (s *Session) Populate(ctx context.Context) (*Session, error) {
|
|
op := trace.FromContext(ctx, "Populate")
|
|
|
|
// Populate s
|
|
var errs []string
|
|
var err error
|
|
|
|
finder := s.Finder
|
|
|
|
op.Debugf("vSphere resource cache populating...")
|
|
s.Datacenter, err = finder.DatacenterOrDefault(op, s.DatacenterPath)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("Failure finding dc (%s): %s", s.DatacenterPath, err.Error()))
|
|
} else {
|
|
finder.SetDatacenter(s.Datacenter)
|
|
op.Debugf("Cached dc: %s", s.DatacenterPath)
|
|
}
|
|
|
|
finder.SetDatacenter(s.Datacenter)
|
|
|
|
s.Cluster, err = finder.ComputeResourceOrDefault(op, s.ClusterPath)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("Failure finding cluster (%s): %s", s.ClusterPath, err.Error()))
|
|
} else {
|
|
op.Debugf("Cached cluster: %s", s.ClusterPath)
|
|
// if we have a cluster lets get DRS Status
|
|
if s.Cluster != nil && s.Cluster.Reference().Type == "ClusterComputeResource" {
|
|
cc := object.NewClusterComputeResource(s.Client.Client, s.Cluster.Reference())
|
|
clusterConfig, _ := cc.Configuration(op)
|
|
if clusterConfig != nil {
|
|
s.DRSEnabled = clusterConfig.DrsConfig.Enabled
|
|
}
|
|
}
|
|
}
|
|
|
|
s.Datastore, err = finder.DatastoreOrDefault(op, s.DatastorePath)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("Failure finding ds (%s): %s", s.DatastorePath, err.Error()))
|
|
} else {
|
|
op.Debugf("Cached ds: %s", s.DatastorePath)
|
|
}
|
|
|
|
s.Host, err = finder.HostSystemOrDefault(op, s.HostPath)
|
|
if err != nil {
|
|
if _, ok := err.(*find.DefaultMultipleFoundError); !ok || !s.IsVC() {
|
|
errs = append(errs, fmt.Sprintf("Failure finding host (%s): %s", s.HostPath, err.Error()))
|
|
}
|
|
} else {
|
|
op.Debugf("Cached host: %s", s.HostPath)
|
|
}
|
|
|
|
s.Pool, err = finder.ResourcePoolOrDefault(op, s.PoolPath)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("Failure finding pool (%s): %s", s.PoolPath, err.Error()))
|
|
} else {
|
|
op.Debugf("Cached pool: %s", s.PoolPath)
|
|
}
|
|
|
|
err = s.setDatacenterFolders(op)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("Failure finding folders (%s): %s", s.DatacenterPath, err.Error()))
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
op.Debugf("Error count populating vSphere cache: (%d)", len(errs))
|
|
return nil, errors.New(strings.Join(errs, "\n"))
|
|
}
|
|
op.Debug("vSphere resource cache populated...")
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Session) SetDatacenter(op trace.Operation, datacenter *object.Datacenter) error {
|
|
s.Datacenter = datacenter
|
|
s.Finder.SetDatacenter(datacenter)
|
|
|
|
if datacenter == nil {
|
|
s.DatacenterPath = ""
|
|
return nil
|
|
}
|
|
|
|
s.DatacenterPath = datacenter.InventoryPath
|
|
|
|
// Do what Populate would have done if datacenterPath were set
|
|
err := s.setDatacenterFolders(op)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Session) setDatacenterFolders(op trace.Operation) error {
|
|
var err error
|
|
|
|
if s.Datacenter != nil {
|
|
folders, e := s.Datacenter.Folders(op)
|
|
if e != nil {
|
|
err = e
|
|
} else {
|
|
op.Debugf("Cached folders: %s", s.DatacenterPath)
|
|
}
|
|
s.VMFolder = folders.VmFolder
|
|
// We don't persist the VCH folder location so set
|
|
// the VCH folder to the default VM folder.
|
|
// The actual location of the VCH will be determined later
|
|
// and this folder ref will be updated accordingly.
|
|
//
|
|
// This will provide standalone ESXi and backwards
|
|
// compatibility to non-folder versions.
|
|
s.VCHFolder = folders.VmFolder
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Session) logEnvironmentInfo(op trace.Operation) {
|
|
a := s.ServiceContent.About
|
|
op.WithFields(logrus.Fields{
|
|
"Name": a.Name,
|
|
"Vendor": a.Vendor,
|
|
"Version": a.Version,
|
|
"Build": a.Build,
|
|
"OS Type": a.OsType,
|
|
"API Type": a.ApiType,
|
|
"API Version": a.ApiVersion,
|
|
"Product ID": a.ProductLineId,
|
|
"UUID": a.InstanceUuid,
|
|
}).Debug("Session Environment Info: ")
|
|
return
|
|
}
|
|
|
|
func isNotAuthenticated(err error) bool {
|
|
if soap.IsSoapFault(err) {
|
|
switch soap.ToSoapFault(err).VimFault().(type) {
|
|
case types.NotAuthenticated:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|