Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions docs/book/vc_shared_sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# vSphere Shared Session capability

One problem that can be found when provisioning a large amount of clusters using
vSphere CSI is vCenter session exhaustion. This happens because every
workload cluster needs to request a new session to vSphere to do proper reconciliation.

vSphere 8.0U3 and up uses a new approach of session management, that allows the
creation and sharing of the sessions among different clusters.

A cluster admin can implement a rest API that, once called, requests a new vCenter
session and shares with CSI. This session will not count on the total generated
sessions of vSphere, and instead will be a child derived session.

This configuration can be applied on vSphere CSI with the usage of
the following CSI configuration:

```shell
[Global]
ca-file = "/etc/ssl/certs/trusted-certificates.crt"
[VirtualCenter "your-vcenter-host"]
datacenters = "datacenter1"
vc-session-manager-url = "https://some-session-manager/session"
vc-session-manager-token = "a-secret-token"
```

The configuration above will make CSI call the shared session rest API and use the
provided token to authenticate against vSphere, instead of using a username/password.

The parameter provider at `vc-session-manager-token` is sent as a `Authorization: Bearer` token
to the session manager, and in case this directive is not configured CSI will send the
Pod Service Account token instead.

Below is an example implementation of a shared session manager rest API. Starting the
program below and calling `http://127.0.0.1:18080/session` should return a JSON that is expected
by CSI using session manager to work:

```shell
$ curl 127.0.0.1:18080/session
{"token":"cst-VCT-52f8d061-aace-4506-f4e6-fca78293a93f-....."}
```

**NOTE**: Below implementation is **NOT PRODUCTION READY** and does not implement
any kind of authentication!

```go
package main

import (
"context"
"encoding/json"
"log"
"net/http"
"net/url"

"github.com/vmware/govmomi"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/soap"
)

const (
vcURL = "https://my-vc.tld"
vcUsername = "Administrator@vsphere.local"
vcPassword = "somepassword"
)

var (
userPassword = url.UserPassword(vcUsername, vcPassword)
)

// SharedSessionResponse is the expected response of CPI when using Shared session manager
type SharedSessionResponse struct {
Token string `json:"token"`
}

func main() {
ctx := context.Background()
vcURL, err := soap.ParseURL(vcURL)
if err != nil {
panic(err)
}
soapClient := soap.NewClient(vcURL, false)
c, err := vim25.NewClient(ctx, soapClient)
if err != nil {
panic(err)
}
client := &govmomi.Client{
Client: c,
SessionManager: session.NewManager(c),
}
if err := client.SessionManager.Login(ctx, userPassword); err != nil {
panic(err)
}

vcsession := func(w http.ResponseWriter, r *http.Request) {
clonedtoken, err := client.SessionManager.AcquireCloneTicket(ctx)
if err != nil {
w.WriteHeader(http.StatusForbidden)
return
}
token := &SharedSessionResponse{Token: clonedtoken}
jsonT, err := json.Marshal(token)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(jsonT)
}

http.HandleFunc("/session", vcsession)
log.Printf("starting webserver on port 18080")
http.ListenAndServe(":18080", nil)
}
```
4 changes: 3 additions & 1 deletion hack/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ function build_driver_images_linux() {

function build_syncer_image_linux() {
echo "building ${SYNCER_IMAGE_NAME}:${VERSION} for linux"
docker buildx build --platform "linux/$ARCH"\
docker buildx build \
--platform "linux/$ARCH" \
--output "${LINUX_IMAGE_OUTPUT}" \
-f images/syncer/Dockerfile \
-t "${SYNCER_IMAGE_NAME}":"${VERSION}" \
--build-arg "VERSION=${VERSION}" \
Expand Down
29 changes: 29 additions & 0 deletions pkg/common/cns-lib/vsphere/tagmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package vsphere

import (
"context"
"fmt"

"github.com/vmware/govmomi/vapi/tags"
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/logger"
)

// GetTagManager returns tagManager connected to given VirtualCenter.
func (vc *VirtualCenter) GetTagManager(ctx context.Context) (*tags.Manager, error) {
log := logger.GetLogger(ctx)
// Validate input.
if vc == nil || vc.Client == nil || vc.Client.Client == nil {
return nil, fmt.Errorf("vCenter not initialized")
}

if err := vc.Connect(ctx); err != nil {
return nil, fmt.Errorf("error connecting to VC: %w", err)
}

vc.tagManager = tags.NewManager(vc.RestClient)
if vc.tagManager == nil {
return nil, fmt.Errorf("failed to create a tagManager")
}
log.Infof("New tag manager with useragent '%s'", vc.tagManager.UserAgent)
return vc.tagManager, nil
}
71 changes: 8 additions & 63 deletions pkg/common/cns-lib/vsphere/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,15 @@ package vsphere

import (
"context"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"net/url"
"reflect"
"strconv"
"strings"

"github.com/davecgh/go-spew/spew"
"github.com/vmware/govmomi/cns"
cnstypes "github.com/vmware/govmomi/cns/types"
"github.com/vmware/govmomi/sts"
"github.com/vmware/govmomi/vapi/rest"
"github.com/vmware/govmomi/vapi/tags"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/config"
Expand Down Expand Up @@ -201,6 +194,12 @@ func GetVirtualCenterConfig(ctx context.Context, cfg *config.Config) (*VirtualCe
ListVolumeThreshold: cfg.Global.ListVolumeThreshold,
MigrationDataStoreURL: cfg.VirtualCenter[host].MigrationDataStoreURL,
FileVolumeActivated: cfg.VirtualCenter[host].FileVolumeActivated,
VCSessionManagerURL: cfg.VirtualCenter[host].VCSessionManagerURL,
VCSessionManagerToken: cfg.VirtualCenter[host].VCSessionManagerToken,
}

if vcConfig.VCSessionManagerURL != "" {
log.Infof("Using Shared Session Manager: %s", vcConfig.VCSessionManagerURL)
}

log.Debugf("Setting the queryLimit = %v, ListVolumeThreshold = %v", vcConfig.QueryLimit, vcConfig.ListVolumeThreshold)
Expand Down Expand Up @@ -247,6 +246,8 @@ func GetVirtualCenterConfigs(ctx context.Context, cfg *config.Config) ([]*Virtua
QueryLimit: cfg.Global.QueryLimit,
ListVolumeThreshold: cfg.Global.ListVolumeThreshold,
FileVolumeActivated: cfg.VirtualCenter[vCenterIP].FileVolumeActivated,
VCSessionManagerURL: cfg.VirtualCenter[vCenterIP].VCSessionManagerURL,
VCSessionManagerToken: cfg.VirtualCenter[vCenterIP].VCSessionManagerToken,
}
if vcConfig.CAFile == "" {
vcConfig.CAFile = cfg.Global.CAFile
Expand Down Expand Up @@ -307,62 +308,6 @@ func CompareKubernetesMetadata(ctx context.Context, k8sMetaData *cnstypes.CnsKub
return labelsMatch
}

// Signer decodes the certificate and private key and returns SAML token needed
// for authentication.
func signer(ctx context.Context, client *vim25.Client, username string, password string) (*sts.Signer, error) {
pemBlock, _ := pem.Decode([]byte(username))
if pemBlock == nil {
return nil, nil
}
certificate, err := tls.X509KeyPair([]byte(username), []byte(password))
if err != nil {
return nil, fmt.Errorf("failed to load X509 key pair. Error: %+v", err)
}
tokens, err := sts.NewClient(ctx, client)
if err != nil {
return nil, fmt.Errorf("failed to create STS client. err: %+v", err)
}
req := sts.TokenRequest{
Certificate: &certificate,
Delegatable: true,
}
signer, err := tokens.Issue(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to issue SAML token. err: %+v", err)
}
return signer, nil
}

// GetTagManager returns tagManager connected to given VirtualCenter.
func GetTagManager(ctx context.Context, vc *VirtualCenter) (*tags.Manager, error) {
log := logger.GetLogger(ctx)
// Validate input.
if vc == nil || vc.Client == nil || vc.Client.Client == nil {
return nil, fmt.Errorf("vCenter not initialized")
}

restClient := rest.NewClient(vc.Client.Client)
signer, err := signer(ctx, vc.Client.Client, vc.Config.Username, vc.Config.Password)
if err != nil {
return nil, fmt.Errorf("failed to create the Signer. Error: %v", err)
}
if signer == nil {
user := url.UserPassword(vc.Config.Username, vc.Config.Password)
err = restClient.Login(ctx, user)
} else {
err = restClient.LoginByToken(restClient.WithSigner(ctx, signer))
}
if err != nil {
return nil, fmt.Errorf("failed to login for the rest client. Error: %v", err)
}
tagManager := tags.NewManager(restClient)
if tagManager == nil {
return nil, fmt.Errorf("failed to create a tagManager")
}
log.Infof("New tag manager with useragent '%s'", tagManager.UserAgent)
return tagManager, nil
}

// GetCandidateDatastoresInClusters gets the shared datastores and vSAN-direct
// managed datastores of given VC clusters from GetCandidateDatastoresInCluster and
// returns a map of clusterID -> array of datastores
Expand Down
104 changes: 104 additions & 0 deletions pkg/common/cns-lib/vsphere/vc_session_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package vsphere

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)

const (
saFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
)

// SharedSessionResponse is the expected structure for a session manager valid
// token response
type SharedSessionResponse struct {
Token string `json:"token"`
}

// SharedTokenOptions represents the options that can be used when calling vc session manager
type SharedTokenOptions struct {
// URL is the session manager URL. Eg.: https://my-session-manager/session)
URL string
// Token is the authorization token that should be passed to session manager
Token string
// TrustedCertificates contains the certpool of certificates trusted by the client
TrustedCertificates *x509.CertPool
// InsecureSkipVerify defines if bad certificates requests should be ignored
InsecureSkipVerify bool
// Timeout defines the client timeout. Defaults to 5 seconds
Timeout time.Duration
// TokenFile defines a file with token content. Defaults to Kubernetes Service Account file
TokenFile string
}

// GetSharedToken executes an http request on session manager and gets the session manager
// token that can be reused on govmomi sessions
func GetSharedToken(ctx context.Context, options SharedTokenOptions) (string, error) {
if options.URL == "" {
return "", fmt.Errorf("URL of session manager cannot be empty")
}

if options.TokenFile == "" {
options.TokenFile = saFile
}

// If the token is empty, we should use service account from the Pod instead
if options.Token == "" {
saValue, err := os.ReadFile(options.TokenFile)
if err != nil {
return "", fmt.Errorf("failed reading token from service account: %w", err)
}
options.Token = string(saValue)
}

timeout := 5 * time.Second
if options.Timeout != 0 {
timeout = options.Timeout
}

transport := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: options.TrustedCertificates,
InsecureSkipVerify: options.InsecureSkipVerify,
},
}

client := &http.Client{
Timeout: timeout,
Transport: transport,
}

request, err := http.NewRequest(http.MethodGet, options.URL, nil)
if err != nil {
return "", fmt.Errorf("failed creating new http client: %w", err)
}
authToken := fmt.Sprintf("Bearer %s", options.Token)
request.Header.Add("Authorization", authToken)

resp, err := client.Do(request)
if err != nil {
return "", fmt.Errorf("failed calling vc session manager: %w", err)
}

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("invalid vc session manager response: %s", resp.Status)
}

token := &SharedSessionResponse{}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(token); err != nil {
return "", fmt.Errorf("failed decoding vc session manager response: %w", err)
}

if token.Token == "" {
return "", fmt.Errorf("returned vc session token is empty")
}
return token.Token, nil
}
Loading