Skip to content

Commit ebebc1d

Browse files
authored
Fix BMCUser account creation and password rotation on Dell iDRAC (#946)
- Move BMC account creation out of ensureBMCSecretForUser into the Status.ID == "" branch where it belongs; the function now only manages the Kubernetes BMCSecret and its reference - Include the password in the same PATCH when updating an existing account; Dell iDRAC returns HTTP 400 when RoleId/Enabled are modified without a password in the request body - Set Status.ID and Status.LastRotation atomically in patchUserStatus to avoid a stale ResourceVersion conflict from a second Status patch - Move setBMCUserSecretRef after CreateOrUpdateAccount in both handleRotatingPassword and ensureBMCSecretForUser so that a BMC failure does not leave Spec.BMCSecretRef pointing at an unverified secret, which would cause an infinite rotation loop - Fix mock server to update the authentication store when Password is changed via PATCH, matching the behaviour of the real Redfish PATCH Signed-off-by: Stefan Hipfel <stefan.hipfel@sap.com>
1 parent 50daae3 commit ebebc1d

3 files changed

Lines changed: 24 additions & 30 deletions

File tree

bmc/mock/server/server.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,17 @@ func (s *MockServer) handlePatch(w http.ResponseWriter, r *http.Request) {
493493
}
494494

495495
mergeJSON(base, update)
496-
s.saveResource(filePath, base)
497496

497+
// Keep the authentication store in sync when Password is updated via PATCH.
498+
if newPwd, ok := update["Password"].(string); ok && newPwd != "" {
499+
if username, ok := base["UserName"].(string); ok && username != "" {
500+
s.mu.Lock()
501+
s.accounts[username] = newPwd
502+
s.mu.Unlock()
503+
}
504+
}
505+
506+
s.saveResource(filePath, base)
498507
w.WriteHeader(http.StatusNoContent)
499508
}
500509

bmc/redfish.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -819,20 +819,14 @@ func (r *RedfishBaseBMC) CreateOrUpdateAccount(
819819
if a.UserName == userName {
820820
a.RoleID = role
821821
a.Enabled = enabled
822+
// Always include the password in the same PATCH. Some BMCs (e.g. Dell iDRAC) return
823+
// HTTP 400 when RoleId/Enabled are modified without a password in the request body.
824+
if password != "" {
825+
a.Password = password
826+
}
822827
if err := a.Update(); err != nil {
823828
return fmt.Errorf("failed to update account: %w", err)
824829
}
825-
// Update password using the official Redfish ChangePassword action
826-
if password != "" {
827-
if _, err := a.ChangePassword(password, r.options.Password); err != nil {
828-
// Fallback: Use PATCH to update password
829-
// This is needed for BMCs with buggy Redfish implementations
830-
a.Password = password
831-
if err := a.Update(); err != nil {
832-
return fmt.Errorf("failed to update account password via PATCH: %w", err)
833-
}
834-
}
835-
}
836830
return nil
837831
}
838832
}

internal/controller/bmcuser_controller.go

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (r *BMCUserReconciler) reconcile(ctx context.Context, user *metalv1alpha1.B
7979
return ctrl.Result{}, fmt.Errorf("failed to get BMC client: %w", err)
8080
}
8181
defer bmcClient.Logout()
82-
if err = r.patchUserStatus(ctx, user, bmcClient); err != nil {
82+
if err = r.patchUserStatus(ctx, user, bmcClient, metav1.Time{}); err != nil {
8383
return ctrl.Result{}, fmt.Errorf("failed to update User status: %w", err)
8484
}
8585

@@ -103,15 +103,9 @@ func (r *BMCUserReconciler) reconcile(ctx context.Context, user *metalv1alpha1.B
103103
if err = bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, password, true); err != nil {
104104
return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
105105
}
106-
if err = r.patchUserStatus(ctx, user, bmcClient); err != nil {
106+
if err = r.patchUserStatus(ctx, user, bmcClient, metav1.Now()); err != nil {
107107
return ctrl.Result{}, fmt.Errorf("failed to update User status after creating BMC account: %w", err)
108108
}
109-
// Set initial rotation timestamp to prevent immediate rotation after account creation
110-
userBase := user.DeepCopy()
111-
user.Status.LastRotation = &metav1.Time{Time: metav1.Now().Time}
112-
if err := r.Status().Patch(ctx, user, client.MergeFrom(userBase)); err != nil {
113-
return ctrl.Result{}, fmt.Errorf("failed to set initial rotation time: %w", err)
114-
}
115109
}
116110
if user.Status.EffectiveBMCSecretRef != nil &&
117111
user.Spec.BMCSecretRef.Name != user.Status.EffectiveBMCSecretRef.Name {
@@ -123,7 +117,7 @@ func (r *BMCUserReconciler) reconcile(ctx context.Context, user *metalv1alpha1.B
123117
return r.handleRotatingPassword(ctx, user, bmcObj, bmcClient)
124118
}
125119

126-
func (r *BMCUserReconciler) patchUserStatus(ctx context.Context, user *metalv1alpha1.BMCUser, bmcClient bmc.BMC) error {
120+
func (r *BMCUserReconciler) patchUserStatus(ctx context.Context, user *metalv1alpha1.BMCUser, bmcClient bmc.BMC, lastRotation metav1.Time) error {
127121
log := ctrl.LoggerFrom(ctx)
128122
accounts, err := bmcClient.GetAccounts()
129123
if err != nil {
@@ -134,6 +128,9 @@ func (r *BMCUserReconciler) patchUserStatus(ctx context.Context, user *metalv1al
134128
log.V(1).Info("BMC account already exists", "ID", account.ID)
135129
userBase := user.DeepCopy()
136130
user.Status.ID = account.ID
131+
if !lastRotation.IsZero() {
132+
user.Status.LastRotation = &lastRotation
133+
}
137134
// Only parse password expiration if it's set (empty string is valid)
138135
if account.PasswordExpiration != "" {
139136
exp, err := time.Parse(time.RFC3339, account.PasswordExpiration)
@@ -148,7 +145,6 @@ func (r *BMCUserReconciler) patchUserStatus(ctx context.Context, user *metalv1al
148145
}
149146
log.V(1).Info("Updated User status with BMC account ID", "AccountID", account.ID)
150147
return nil
151-
152148
}
153149
}
154150
return nil
@@ -197,12 +193,12 @@ func (r *BMCUserReconciler) handleRotatingPassword(ctx context.Context, user *me
197193
if err != nil {
198194
return ctrl.Result{}, fmt.Errorf("failed to create BMCSecret: %w", err)
199195
}
200-
if err := r.setBMCUserSecretRef(ctx, user, secret); err != nil {
201-
return ctrl.Result{}, fmt.Errorf("failed to set BMCSecret reference for User: %w", err)
202-
}
203196
if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, true); err != nil {
204197
return ctrl.Result{}, fmt.Errorf("failed to create or update BMC account with new password: %w", err)
205198
}
199+
if err := r.setBMCUserSecretRef(ctx, user, secret); err != nil {
200+
return ctrl.Result{}, fmt.Errorf("failed to set BMCSecret reference for User: %w", err)
201+
}
206202

207203
// Update the last rotation time
208204
userBase := user.DeepCopy()
@@ -235,11 +231,6 @@ func (r *BMCUserReconciler) ensureBMCSecretForUser(ctx context.Context, bmcClien
235231
if err := r.setBMCUserSecretRef(ctx, user, secret); err != nil {
236232
return fmt.Errorf("failed to set BMCSecret reference for User: %w", err)
237233
}
238-
log.V(1).Info("Creating BMC account with new password", "Account", user.Name)
239-
if err := bmcClient.CreateOrUpdateAccount(ctx, user.Spec.UserName, user.Spec.RoleID, newPassword, true); err != nil {
240-
return fmt.Errorf("failed to create or update BMC account with new password: %w", err)
241-
}
242-
log.V(1).Info("BMC account created with new password", "Account", user.Name)
243234
return nil
244235
}
245236

0 commit comments

Comments
 (0)