Skip to content

Commit 0ec9586

Browse files
authored
Implement session manager client on CSI (#3419)
* Implement session manager client on CSI * Fix broken unittest
1 parent 29b9619 commit 0ec9586

File tree

12 files changed

+538
-15
lines changed

12 files changed

+538
-15
lines changed

docs/book/vc_shared_sessions.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# vSphere Shared Session capability
2+
3+
One problem that can be found when provisioning a large amount of clusters using
4+
vSphere CSI is vCenter session exhaustion. This happens because every
5+
workload cluster needs to request a new session to vSphere to do proper reconciliation.
6+
7+
vSphere 8.0U3 and up uses a new approach of session management, that allows the
8+
creation and sharing of the sessions among different clusters.
9+
10+
A cluster admin can implement a rest API that, once called, requests a new vCenter
11+
session and shares with CSI. This session will not count on the total generated
12+
sessions of vSphere, and instead will be a child derived session.
13+
14+
This configuration can be applied on vSphere CSI with the usage of
15+
the following CSI configuration:
16+
17+
```shell
18+
[Global]
19+
ca-file = "/etc/ssl/certs/trusted-certificates.crt"
20+
[VirtualCenter "your-vcenter-host"]
21+
datacenters = "datacenter1"
22+
vc-session-manager-url = "https://some-session-manager/session"
23+
vc-session-manager-token = "a-secret-token"
24+
```
25+
26+
The configuration above will make CSI call the shared session rest API and use the
27+
provided token to authenticate against vSphere, instead of using a username/password.
28+
29+
The parameter provider at `vc-session-manager-token` is sent as a `Authorization: Bearer` token
30+
to the session manager, and in case this directive is not configured CSI will send the
31+
Pod Service Account token instead.
32+
33+
Below is an example implementation of a shared session manager rest API. Starting the
34+
program below and calling `http://127.0.0.1:18080/session` should return a JSON that is expected
35+
by CSI using session manager to work:
36+
37+
```shell
38+
$ curl 127.0.0.1:18080/session
39+
{"token":"cst-VCT-52f8d061-aace-4506-f4e6-fca78293a93f-....."}
40+
```
41+
42+
**NOTE**: Below implementation is **NOT PRODUCTION READY** and does not implement
43+
any kind of authentication!
44+
45+
```go
46+
package main
47+
48+
import (
49+
"context"
50+
"encoding/json"
51+
"log"
52+
"net/http"
53+
"net/url"
54+
55+
"github.com/vmware/govmomi"
56+
"github.com/vmware/govmomi/session"
57+
"github.com/vmware/govmomi/vim25"
58+
"github.com/vmware/govmomi/vim25/soap"
59+
)
60+
61+
const (
62+
vcURL = "https://my-vc.tld"
63+
vcUsername = "[email protected]"
64+
vcPassword = "somepassword"
65+
)
66+
67+
var (
68+
userPassword = url.UserPassword(vcUsername, vcPassword)
69+
)
70+
71+
// SharedSessionResponse is the expected response of CPI when using Shared session manager
72+
type SharedSessionResponse struct {
73+
Token string `json:"token"`
74+
}
75+
76+
func main() {
77+
ctx := context.Background()
78+
vcURL, err := soap.ParseURL(vcURL)
79+
if err != nil {
80+
panic(err)
81+
}
82+
soapClient := soap.NewClient(vcURL, false)
83+
c, err := vim25.NewClient(ctx, soapClient)
84+
if err != nil {
85+
panic(err)
86+
}
87+
client := &govmomi.Client{
88+
Client: c,
89+
SessionManager: session.NewManager(c),
90+
}
91+
if err := client.SessionManager.Login(ctx, userPassword); err != nil {
92+
panic(err)
93+
}
94+
95+
vcsession := func(w http.ResponseWriter, r *http.Request) {
96+
clonedtoken, err := client.SessionManager.AcquireCloneTicket(ctx)
97+
if err != nil {
98+
w.WriteHeader(http.StatusForbidden)
99+
return
100+
}
101+
token := &SharedSessionResponse{Token: clonedtoken}
102+
jsonT, err := json.Marshal(token)
103+
if err != nil {
104+
w.WriteHeader(http.StatusInternalServerError)
105+
return
106+
}
107+
w.WriteHeader(http.StatusOK)
108+
w.Write(jsonT)
109+
}
110+
111+
http.HandleFunc("/session", vcsession)
112+
log.Printf("starting webserver on port 18080")
113+
http.ListenAndServe(":18080", nil)
114+
}
115+
```

pkg/common/cns-lib/vsphere/utils.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ func GetVirtualCenterConfig(ctx context.Context, cfg *config.Config) (*VirtualCe
194194
ListVolumeThreshold: cfg.Global.ListVolumeThreshold,
195195
MigrationDataStoreURL: cfg.VirtualCenter[host].MigrationDataStoreURL,
196196
FileVolumeActivated: cfg.VirtualCenter[host].FileVolumeActivated,
197+
VCSessionManagerURL: cfg.VirtualCenter[host].VCSessionManagerURL,
198+
VCSessionManagerToken: cfg.VirtualCenter[host].VCSessionManagerToken,
199+
}
200+
201+
if vcConfig.VCSessionManagerURL != "" {
202+
log.Infof("Using Shared Session Manager: %s", vcConfig.VCSessionManagerURL)
197203
}
198204

199205
log.Debugf("Setting the queryLimit = %v, ListVolumeThreshold = %v", vcConfig.QueryLimit, vcConfig.ListVolumeThreshold)
@@ -240,6 +246,8 @@ func GetVirtualCenterConfigs(ctx context.Context, cfg *config.Config) ([]*Virtua
240246
QueryLimit: cfg.Global.QueryLimit,
241247
ListVolumeThreshold: cfg.Global.ListVolumeThreshold,
242248
FileVolumeActivated: cfg.VirtualCenter[vCenterIP].FileVolumeActivated,
249+
VCSessionManagerURL: cfg.VirtualCenter[vCenterIP].VCSessionManagerURL,
250+
VCSessionManagerToken: cfg.VirtualCenter[vCenterIP].VCSessionManagerToken,
243251
}
244252
if vcConfig.CAFile == "" {
245253
vcConfig.CAFile = cfg.Global.CAFile
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package vsphere
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"os"
11+
"time"
12+
)
13+
14+
const (
15+
saFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
16+
)
17+
18+
// SharedSessionResponse is the expected structure for a session manager valid
19+
// token response
20+
type SharedSessionResponse struct {
21+
Token string `json:"token"`
22+
}
23+
24+
// SharedTokenOptions represents the options that can be used when calling vc session manager
25+
type SharedTokenOptions struct {
26+
// URL is the session manager URL. Eg.: https://my-session-manager/session)
27+
URL string
28+
// Token is the authorization token that should be passed to session manager
29+
Token string
30+
// TrustedCertificates contains the certpool of certificates trusted by the client
31+
TrustedCertificates *x509.CertPool
32+
// InsecureSkipVerify defines if bad certificates requests should be ignored
33+
InsecureSkipVerify bool
34+
// Timeout defines the client timeout. Defaults to 5 seconds
35+
Timeout time.Duration
36+
// TokenFile defines a file with token content. Defaults to Kubernetes Service Account file
37+
TokenFile string
38+
}
39+
40+
// GetSharedToken executes an http request on session manager and gets the session manager
41+
// token that can be reused on govmomi sessions
42+
func GetSharedToken(ctx context.Context, options SharedTokenOptions) (string, error) {
43+
if options.URL == "" {
44+
return "", fmt.Errorf("URL of session manager cannot be empty")
45+
}
46+
47+
if options.TokenFile == "" {
48+
options.TokenFile = saFile
49+
}
50+
51+
// If the token is empty, we should use service account from the Pod instead
52+
if options.Token == "" {
53+
saValue, err := os.ReadFile(options.TokenFile)
54+
if err != nil {
55+
return "", fmt.Errorf("failed reading token from service account: %w", err)
56+
}
57+
options.Token = string(saValue)
58+
}
59+
60+
timeout := 5 * time.Second
61+
if options.Timeout != 0 {
62+
timeout = options.Timeout
63+
}
64+
65+
transport := &http.Transport{
66+
TLSClientConfig: &tls.Config{
67+
RootCAs: options.TrustedCertificates,
68+
InsecureSkipVerify: options.InsecureSkipVerify,
69+
},
70+
}
71+
72+
client := &http.Client{
73+
Timeout: timeout,
74+
Transport: transport,
75+
}
76+
77+
request, err := http.NewRequest(http.MethodGet, options.URL, nil)
78+
if err != nil {
79+
return "", fmt.Errorf("failed creating new http client: %w", err)
80+
}
81+
authToken := fmt.Sprintf("Bearer %s", options.Token)
82+
request.Header.Add("Authorization", authToken)
83+
84+
resp, err := client.Do(request)
85+
if err != nil {
86+
return "", fmt.Errorf("failed calling vc session manager: %w", err)
87+
}
88+
89+
if resp.StatusCode != http.StatusOK {
90+
return "", fmt.Errorf("invalid vc session manager response: %s", resp.Status)
91+
}
92+
93+
token := &SharedSessionResponse{}
94+
defer resp.Body.Close()
95+
decoder := json.NewDecoder(resp.Body)
96+
if err := decoder.Decode(token); err != nil {
97+
return "", fmt.Errorf("failed decoding vc session manager response: %w", err)
98+
}
99+
100+
if token.Token == "" {
101+
return "", fmt.Errorf("returned vc session token is empty")
102+
}
103+
return token.Token, nil
104+
}

0 commit comments

Comments
 (0)