Repository navigation
Add optional Redfish session token caching to reduce BMC audit log spam #1146
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
256e365
4b834e0
c7d1d66
257e401
0902e46
fee4044
e2fe28a
f8ceb2a
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,103 @@ | ||||||||||||||||||||||||||||||||||
| // SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors | ||||||||||||||||||||||||||||||||||
| // SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| package bmc | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||
| "sync/atomic" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| "github.com/stmcginnis/gofish" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Dialer creates BMC connections. | ||||||||||||||||||||||||||||||||||
| type Dialer interface { | ||||||||||||||||||||||||||||||||||
| Dial(ctx context.Context, o Options) (BMC, error) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // DirectDialer creates a fresh basic-auth BMC connection on every Dial call. | ||||||||||||||||||||||||||||||||||
| type DirectDialer struct{} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func (DirectDialer) Dial(ctx context.Context, o Options) (BMC, error) { | ||||||||||||||||||||||||||||||||||
| return NewRedfishBMCClient(ctx, o) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // SessionDialer dials using a shared SessionCache, reusing tokens across calls. | ||||||||||||||||||||||||||||||||||
| // On 401/403 it invalidates the cached session and retries once. | ||||||||||||||||||||||||||||||||||
| type SessionDialer struct { | ||||||||||||||||||||||||||||||||||
| cache *SessionCache | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func NewSessionDialer(cache *SessionCache) *SessionDialer { | ||||||||||||||||||||||||||||||||||
| return &SessionDialer{cache: cache} | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| type redfishClientHolder interface { | ||||||||||||||||||||||||||||||||||
| Client() *gofish.APIClient | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func (p *SessionDialer) Dial(ctx context.Context, o Options) (BMC, error) { | ||||||||||||||||||||||||||||||||||
| o.SessionCache = p.cache | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| bmcClient, err := NewRedfishBMCClient(ctx, o) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| if IsSessionExpiredError(err) { | ||||||||||||||||||||||||||||||||||
| p.cache.Invalidate(SessionCacheKey{Endpoint: o.Endpoint, Username: o.Username}) | ||||||||||||||||||||||||||||||||||
| bmcClient, err = NewRedfishBMCClient(ctx, o) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if holder, ok := bmcClient.(redfishClientHolder); ok { | ||||||||||||||||||||||||||||||||||
| httpClient := holder.Client().HTTPClient | ||||||||||||||||||||||||||||||||||
| base := httpClient.Transport | ||||||||||||||||||||||||||||||||||
| if base == nil { | ||||||||||||||||||||||||||||||||||
| base = http.DefaultTransport | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| httpClient.Transport = &retryRoundTripper{ | ||||||||||||||||||||||||||||||||||
| base: base, | ||||||||||||||||||||||||||||||||||
| cache: p.cache, | ||||||||||||||||||||||||||||||||||
| key: SessionCacheKey{Endpoint: o.Endpoint, Username: o.Username}, | ||||||||||||||||||||||||||||||||||
| opts: o, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return bmcClient, nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // retryRoundTripper recovers from BMC-side session eviction (e.g. BMC restart) | ||||||||||||||||||||||||||||||||||
| // without surfacing a 401/403 to the caller. | ||||||||||||||||||||||||||||||||||
| type retryRoundTripper struct { | ||||||||||||||||||||||||||||||||||
| base http.RoundTripper | ||||||||||||||||||||||||||||||||||
| cache *SessionCache | ||||||||||||||||||||||||||||||||||
| key SessionCacheKey | ||||||||||||||||||||||||||||||||||
| opts Options | ||||||||||||||||||||||||||||||||||
| retried atomic.Bool | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func (r *retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { | ||||||||||||||||||||||||||||||||||
| resp, err := r.base.RoundTrip(req) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return resp, err | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden { | ||||||||||||||||||||||||||||||||||
| return resp, nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if !r.retried.CompareAndSwap(false, true) { | ||||||||||||||||||||||||||||||||||
| return resp, nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| _ = resp.Body.Close() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| r.cache.Invalidate(r.key) | ||||||||||||||||||||||||||||||||||
| session, sessionErr := r.cache.GetOrCreate(req.Context(), r.opts) | ||||||||||||||||||||||||||||||||||
| if sessionErr != nil { | ||||||||||||||||||||||||||||||||||
| return nil, sessionErr | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| retryReq := req.Clone(req.Context()) | ||||||||||||||||||||||||||||||||||
| retryReq.Header.Set("X-Auth-Token", session.Token) | ||||||||||||||||||||||||||||||||||
| return r.base.RoundTrip(retryReq) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+100
to
+102
Contributor
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Rewind the request body before the retry.
🐛 Proposed fix retryReq := req.Clone(req.Context())
+ if req.Body != nil && req.Body != http.NoBody {
+ if req.GetBody == nil {
+ return nil, fmt.Errorf("bmc: cannot retry request with non-rewindable body")
+ }
+ body, err := req.GetBody()
+ if err != nil {
+ return nil, err
+ }
+ retryReq.Body = body
+ }
retryReq.Header.Set("X-Auth-Token", session.Token)
return r.base.RoundTrip(retryReq)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
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.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 16228
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 35053
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 32036
🏁 Script executed:
Repository: ironcore-dev/metal-operator
Length of output: 14221
Apply the refreshed token to subsequent requests.
After
RoundTripretries a 401/403, it updates onlyretryReq. In a BMC reconciliation,GetManagerruns beforeGetSystems. If the first call recovers, the next request can still use the stale token. Sinceretriedis already set, that 401/403 is returned and the current reconciliation fails. The replacement session is cached, so a new client in a later reconciliation can use it.🐛 Suggested fix
type retryRoundTripper struct { base http.RoundTripper cache *SessionCache key SessionCacheKey opts Options retried atomic.Bool + currentToken atomic.Value } func (r *retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if token, ok := r.currentToken.Load().(string); ok { + req = req.Clone(req.Context()) + req.Header.Set("X-Auth-Token", token) + } resp, err := r.base.RoundTrip(req) if err != nil { return resp, err @@ if sessionErr != nil { return nil, sessionErr } + r.currentToken.Store(session.Token) retryReq := req.Clone(req.Context()) retryReq.Header.Set("X-Auth-Token", session.Token) return r.base.RoundTrip(retryReq) }🤖 Prompt for AI Agents