-
Notifications
You must be signed in to change notification settings - Fork 1.5k
do not merge - vsphere client mgmt #10581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,271 @@ | ||
| package vsphere | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/sha256" | ||
| "fmt" | ||
| "net/url" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/pkg/errors" | ||
| "github.com/sirupsen/logrus" | ||
| "github.com/vmware/govmomi" | ||
| "github.com/vmware/govmomi/cns" | ||
| "github.com/vmware/govmomi/find" | ||
| "github.com/vmware/govmomi/pbm" | ||
| "github.com/vmware/govmomi/vapi/rest" | ||
| "github.com/vmware/govmomi/vim25" | ||
| "github.com/vmware/govmomi/vim25/soap" | ||
| ) | ||
|
|
||
| // Session manages a vCenter connection lifecycle including SOAP, REST, and | ||
| // auxiliary clients. It provides a single entry point for creating and | ||
| // reusing vSphere client sessions with proper caching and cleanup. | ||
| type Session struct { | ||
| server string | ||
| govmomi *govmomi.Client | ||
| vim25 *vim25.Client | ||
| rest *rest.Client | ||
| cns *cns.Client | ||
| pbm *pbm.Client | ||
| finder *find.Finder | ||
| logger logrus.FieldLogger | ||
| once sync.Once | ||
| closeErr error | ||
| } | ||
|
|
||
| // SessionOptions configures a vSphere session. | ||
| type SessionOptions struct { | ||
| // Timeout for the initial connection. Defaults to 60s if zero. | ||
| Timeout time.Duration | ||
| // Insecure skips TLS certificate verification. Defaults to false. | ||
| Insecure bool | ||
| // Logger for structured logging. If nil, uses logrus.StandardLogger(). | ||
| Logger logrus.FieldLogger | ||
| } | ||
|
|
||
| // SessionOption configures a session via functional options. | ||
| type SessionOption func(*SessionOptions) | ||
|
|
||
| // WithTimeout sets the connection timeout. | ||
| func WithTimeout(timeout time.Duration) SessionOption { | ||
| return func(o *SessionOptions) { o.Timeout = timeout } | ||
| } | ||
|
|
||
| // WithInsecure skips TLS certificate verification. | ||
| func WithInsecure(insecure bool) SessionOption { | ||
| return func(o *SessionOptions) { o.Insecure = insecure } | ||
| } | ||
|
|
||
| // WithLogger sets the logger for structured logging. | ||
| func WithLogger(logger logrus.FieldLogger) SessionOption { | ||
| return func(o *SessionOptions) { o.Logger = logger } | ||
| } | ||
|
|
||
| // applyDefaults sets reasonable defaults for unset options. | ||
| func (o *SessionOptions) applyDefaults() { | ||
| if o.Timeout == 0 { | ||
| o.Timeout = 60 * time.Second | ||
| } | ||
| if o.Logger == nil { | ||
| o.Logger = logrus.StandardLogger() | ||
| } | ||
| } | ||
|
|
||
| // SessionCacheKey returns a deterministic key for session caching based on | ||
| // server, username, and password. | ||
| func SessionCacheKey(server, username, password string) string { | ||
| hash := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%s", server, username, password))) | ||
| return fmt.Sprintf("%x", hash[:16]) | ||
| } | ||
|
|
||
| var ( | ||
| sessionsMu sync.Mutex | ||
| sessions = make(map[string]*Session) | ||
| ) | ||
|
|
||
| // redactHostname returns a short deterministic hash of the hostname, suitable | ||
| // for logging. The plain hostname is never exposed in logs. | ||
| func redactHostname(hostname string) string { | ||
| h := sha256.Sum256([]byte(hostname)) | ||
| return fmt.Sprintf("%x", h[:16]) | ||
| } | ||
|
|
||
| // NewSession creates or retrieves a cached vSphere session for the given | ||
| // server, username, and password. Returns the session and a cleanup function. | ||
| // | ||
| // The cleanup function must be called when the session is no longer needed. | ||
| // It is idempotent and safe to call multiple times. | ||
| // | ||
| // Options allow configuring timeout, TLS, and other settings. | ||
| func NewSession(ctx context.Context, server, username, password string, opts ...SessionOption) (*Session, func(), error) { | ||
| options := makeSessionOptions(opts...) | ||
| options.applyDefaults() | ||
|
|
||
| if server == "" || username == "" || password == "" { | ||
| return nil, nil, errors.New("vSphere server, username, and password are required") | ||
| } | ||
|
|
||
| key := SessionCacheKey(server, username, password) | ||
|
|
||
| sessionsMu.Lock() | ||
| if sess, ok := sessions[key]; ok { | ||
| if sess.Error() != nil { | ||
| // Cached session is closed — evict it so a fresh one is created. | ||
| delete(sessions, key) | ||
| sessionsMu.Unlock() | ||
| // Fall through to create a new session. | ||
| } else { | ||
| sessionsMu.Unlock() | ||
| return sess, func() { sess.Close() }, nil | ||
| } | ||
|
Comment on lines
+121
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shared cached sessions need reference-counted cleanup. Each caller gets a cleanup function that always calls Also applies to: 142-147 🤖 Prompt for AI Agents |
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| sessionsMu.Unlock() | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| ctx, cancel := context.WithTimeout(ctx, options.Timeout) | ||
| defer cancel() | ||
|
|
||
| sess, err := newSessionFromConn(ctx, server, username, password, options) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
|
|
||
| sessionsMu.Lock() | ||
| // Double check after acquiring the lock (another goroutine may have created | ||
| // the session in the meantime). | ||
| if s, ok := sessions[key]; ok { | ||
| // A cached session arrived in the meantime — close the one we just | ||
| // created to avoid leaking an authenticated client. | ||
| sess.Close() | ||
| sessionsMu.Unlock() | ||
| return s, func() { s.Close() }, nil | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| sessions[key] = sess | ||
| sessionsMu.Unlock() | ||
|
|
||
| return sess, func() { sess.Close() }, nil | ||
| } | ||
|
|
||
| func makeSessionOptions(opts ...SessionOption) *SessionOptions { | ||
| o := &SessionOptions{} | ||
| for _, opt := range opts { | ||
| opt(o) | ||
| } | ||
| return o | ||
| } | ||
|
|
||
| // Close releases all resources held by the session. It is safe to call | ||
| // multiple times. If any cleanup operation fails, the first error is | ||
| // stored and returned via Error(). | ||
| func (s *Session) Close() { | ||
| s.once.Do(func() { | ||
| var firstErr error | ||
|
|
||
| s.logger.WithField("vcenter", redactHostname(s.server)).Debug("Closing vSphere session") | ||
|
|
||
| if s.rest != nil { | ||
| if err := s.rest.Logout(context.Background()); err != nil { | ||
| s.logger.WithError(err).Warn("Failed to logout REST client") | ||
| if firstErr == nil { | ||
| firstErr = err | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if s.govmomi != nil { | ||
| if err := s.govmomi.Logout(context.Background()); err != nil { | ||
|
Comment on lines
+167
to
+177
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and show the relevant code in session.go
FILE="pkg/asset/installconfig/vsphere/session.go"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
nl -ba "$FILE" | sed -n '130,220p'
# Search for Close() and any existing timeout/context usage in the vsphere session code
rg -n "func \\(.*\\) Close\\(|Logout\\(context|WithTimeout\\(|Background\\(\\)" "$FILE"
# Show definitions of any session structs fields related to timeouts or contexts
rg -n "type .*Session|struct .*Session|timeout|ctx|context\\.Context" "$FILE"Repository: openshift/installer Length of output: 107 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="pkg/asset/installconfig/vsphere/session.go"
[ -f "$FILE" ] || { echo "Missing file: $FILE"; exit 1; }
# Show the relevant region with line numbers
cat -n "$FILE" | sed -n '130,220p'
# Find Close() and Logout usage plus any existing timeouts/context usage nearby
rg -n "func \\(.*\\) Close\\(|Logout\\(context\\.|Logout\\(|WithTimeout\\(|context\\.Background\\(" "$FILE"
# Inspect the Logout method signatures/types referenced in this file (if any)
rg -n "Logout\\(" "$FILE"
# Check whether session struct already carries a timeout or context
rg -n "type .*Session|struct .*Session|timeout|ctx\\b|context\\.Context" "$FILE"Repository: openshift/installer Length of output: 4817 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="pkg/asset/installconfig/vsphere/session.go"
# Print the top portion: Session/SessionOptions structs and timeout option definitions
cat -n "$FILE" | sed -n '1,140p'
# Print the rest around NewSession and any other context usage
cat -n "$FILE" | sed -n '140,280p'Repository: openshift/installer Length of output: 9840 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Search for Logout signatures in vendor (govmomi/vim25 and vapi/rest)
rg -n "func \\(.*\\) Logout\\(ctx context\\.Context\\)" vendor/github.com/vmware/govmomi -S || true
rg -n "Logout\\(ctx context\\.Context\\)" vendor/github.com/vmware/govmomi -S | head -n 50
# Also check for rest client Logout signature
rg -n "Logout\\(ctx context\\.Context\\)" vendor/github.com/vmware/govmomi/vapi/rest -S || true
# Show session.go Session struct fields to see if timeout is stored
rg -n "type Session struct \\{|type SessionOptions struct \\{|Timeout" pkg/asset/installconfig/vsphere/session.goRepository: openshift/installer Length of output: 1249 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show relevant parts around Logout implementations in vendored govmomi
for f in \
vendor/github.com/vmware/govmomi/client.go \
vendor/github.com/vmware/govmomi/vapi/rest/client.go \
vendor/github.com/vmware/govmomi/session/manager.go
do
echo "==== $f ===="
# print around the first Logout occurrence
start=$(rg -n "func \\(.*\\) Logout\\(ctx context\\.Context\\)" "$f" | head -n 1 | cut -d: -f1)
if [ -z "${start:-}" ]; then
echo "No Logout(ctx) found in $f"
continue
fi
# show ~80 lines around it
from=$((start-20))
to=$((start+120))
if [ $from -lt 1 ]; then from=1; fi
sed -n "${from},${to}p" "$f" | nl -ba | sed 's/^ *[0-9]*\t//'
doneRepository: openshift/installer Length of output: 178 🏁 Script executed: #!/bin/bash
set -euo pipefail
for f in \
vendor/github.com/vmware/govmomi/client.go \
vendor/github.com/vmware/govmomi/vapi/rest/client.go \
vendor/github.com/vmware/govmomi/session/manager.go
do
echo "==== $f ===="
# Find the first Logout(ctx ...) in the file
line=$(rg -n "func \\(.*\\) Logout\\(ctx context\\.Context\\)" "$f" | head -n 1 | cut -d: -f1 || true)
if [ -z "${line:-}" ]; then
echo "No Logout(ctx) found"
continue
fi
from=$((line-20))
to=$((line+160))
if [ $from -lt 1 ]; then from=1; fi
cat -n "$f" | sed -n "${from},${to}p"
echo
doneRepository: openshift/installer Length of output: 8535 Bound vCenter logout RPCs with a timeout in 🤖 Prompt for AI Agents |
||
| s.logger.WithError(err).Warn("Failed to logout SOAP client") | ||
| if firstErr == nil { | ||
| firstErr = err | ||
| } | ||
| } | ||
| } | ||
|
|
||
| s.closeErr = firstErr | ||
| s.logger.WithField("vcenter", redactHostname(s.server)).Debug("vSphere session closed") | ||
| }) | ||
| } | ||
|
|
||
| // Error returns any error that occurred during Close. | ||
| func (s *Session) Error() error { | ||
| return s.closeErr | ||
| } | ||
|
|
||
| func newSessionFromConn(ctx context.Context, server, username, password string, opts *SessionOptions) (*Session, error) { | ||
| s := &Session{ | ||
| server: server, | ||
| logger: opts.Logger, | ||
| } | ||
|
|
||
| u, err := soap.ParseURL(server) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "parse vCenter URL") | ||
| } | ||
| u.User = url.UserPassword(username, password) | ||
|
|
||
| c, err := govmomi.NewClient(ctx, u, opts.Insecure) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "create SOAP client") | ||
| } | ||
| s.govmomi = c | ||
| s.vim25 = c.Client | ||
| s.finder = find.NewFinder(s.vim25, true) | ||
|
|
||
| restClient := rest.NewClient(c.Client) | ||
| if err := restClient.Login(ctx, u.User); err != nil { | ||
| // Log out SOAP client before returning error — this replaces the | ||
| // buggy pattern in CreateVSphereClients where the logout error | ||
| // was swallowed and could mask the original error. | ||
| if logoutErr := c.Logout(context.Background()); logoutErr != nil { | ||
| opts.Logger.WithError(logoutErr).Warn("Failed to logout SOAP client after REST login failure") | ||
| } | ||
| return nil, errors.Wrap(err, "REST client login") | ||
| } | ||
| s.rest = restClient | ||
|
|
||
| if cnsClient, err := cns.NewClient(context.Background(), s.vim25); err == nil { | ||
| s.cns = cnsClient | ||
| } else { | ||
| opts.Logger.WithError(err).Debug("CNS client creation failed (optional)") | ||
| } | ||
|
|
||
| if pbmClient, err := pbm.NewClient(context.Background(), s.vim25); err == nil { | ||
| s.pbm = pbmClient | ||
| } else { | ||
| opts.Logger.WithError(err).Debug("PBM client creation failed (optional)") | ||
| } | ||
|
|
||
| s.logger.WithField("vcenter", redactHostname(server)).Info("vSphere session created") | ||
| return s, nil | ||
| } | ||
|
|
||
| // Vim25Client returns the underlying SOAP client for general vSphere API calls. | ||
| func (s *Session) Vim25Client() *vim25.Client { | ||
| return s.vim25 | ||
| } | ||
|
|
||
| // RestClient returns the REST client for vSphere tag and policy operations. | ||
| func (s *Session) RestClient() *rest.Client { | ||
| return s.rest | ||
| } | ||
|
|
||
| // CNSClient returns the CNS (vSAN) client for storage volume operations. | ||
| func (s *Session) CNSClient() *cns.Client { | ||
| return s.cns | ||
| } | ||
|
|
||
| // PBMClient returns the PBM (storage policy) client. | ||
| func (s *Session) PBMClient() *pbm.Client { | ||
| return s.pbm | ||
| } | ||
|
|
||
| // Finder returns a finder for locating vSphere inventory objects. | ||
| func (s *Session) Finder() *find.Finder { | ||
| return s.finder | ||
| } | ||
|
|
||
| // Server returns the vCenter server URL. | ||
| func (s *Session) Server() string { | ||
| return s.server | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encode the cache-key inputs unambiguously.
fmt.Sprintf("%s|%s|%s", ...)is not injective: values containing|can produce the same preimage string for different(server, username, password)tuples. That can make unrelated credentials reuse the same cached session. Hash length-prefixed fields instead of a delimiter-joined string.🤖 Prompt for AI Agents