Skip to content

Commit 4f82e27

Browse files
authored
Persist boot overrides during maintenance (#939)
Signed-off-by: Andreas Fritzler <andreas.fritzler@sap.com>
1 parent 80ddd83 commit 4f82e27

6 files changed

Lines changed: 111 additions & 35 deletions

File tree

bmc/bmc.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,16 @@ type BMC interface {
4646
// Reset performs a reset on the system.
4747
Reset(ctx context.Context, systemURI string, resetType schemas.ResetType) error
4848

49-
// SetPXEBootOnce sets the boot device for the next system boot.
50-
SetPXEBootOnce(ctx context.Context, systemURI string) error
49+
// SetBootOverride configures the system to network-boot on its next
50+
// power-on, bypassing the persistent boot order. If persistent is false
51+
// the override applies to a single boot only; if true it applies to every
52+
// subsequent boot until ClearBootOverride is called.
53+
SetBootOverride(ctx context.Context, systemURI string, persistent bool) error
54+
55+
// ClearBootOverride removes any active boot override so the system uses
56+
// its persistent boot order on the next power-on. No-op when no override
57+
// is currently set.
58+
ClearBootOverride(ctx context.Context, systemURI string) error
5159

5260
// GetSystemInfo retrieves information about the system.
5361
GetSystemInfo(ctx context.Context, systemURI string) (SystemInfo, error)

bmc/redfish.go

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,9 @@ type RedfishBaseBMC struct {
6565
manufacturer string
6666
}
6767

68-
var pxeBootWithSettingUEFIBootMode = schemas.Boot{
69-
BootSourceOverrideEnabled: schemas.OnceBootSourceOverrideEnabled,
70-
BootSourceOverrideMode: schemas.UEFIBootSourceOverrideMode,
71-
BootSourceOverrideTarget: schemas.PxeBootSource,
72-
}
73-
var pxeBootWithoutSettingUEFIBootMode = schemas.Boot{
74-
BootSourceOverrideEnabled: schemas.OnceBootSourceOverrideEnabled,
75-
BootSourceOverrideTarget: schemas.PxeBootSource,
68+
var clearedBootOverride = schemas.Boot{
69+
BootSourceOverrideEnabled: schemas.DisabledBootSourceOverrideEnabled,
70+
BootSourceOverrideTarget: schemas.NoneBootSource,
7671
}
7772

7873
type InvalidBIOSSettingsError struct {
@@ -231,26 +226,65 @@ func (r *RedfishBaseBMC) GetSystems(ctx context.Context) ([]Server, error) {
231226
return servers, nil
232227
}
233228

234-
// SetPXEBootOnce sets the boot device for the next system boot using Redfish.
235-
func (r *RedfishBaseBMC) SetPXEBootOnce(ctx context.Context, systemURI string) error {
229+
// SetBootOverride sets a Redfish boot source override targeting network boot.
230+
// With persistent=false it sets BootSourceOverrideEnabled=Once; with
231+
// persistent=true it sets Continuous, which is preserved across power cycles.
232+
// When the requested persistent override already matches the current state
233+
// the call is a no-op.
234+
func (r *RedfishBaseBMC) SetBootOverride(ctx context.Context, systemURI string, persistent bool) error {
236235
system, err := r.getSystemFromUri(ctx, systemURI)
237236
if err != nil {
238237
return fmt.Errorf("failed to get systems: %w", err)
239238
}
240-
var setBoot schemas.Boot
239+
wantEnabled := schemas.OnceBootSourceOverrideEnabled
240+
if persistent {
241+
wantEnabled = schemas.ContinuousBootSourceOverrideEnabled
242+
if system.Boot.BootSourceOverrideEnabled == schemas.ContinuousBootSourceOverrideEnabled &&
243+
system.Boot.BootSourceOverrideTarget == schemas.PxeBootSource &&
244+
(system.Boot.BootSourceOverrideMode == "" ||
245+
system.Boot.BootSourceOverrideMode == schemas.UEFIBootSourceOverrideMode) {
246+
return nil
247+
}
248+
}
249+
250+
setBoot := schemas.Boot{
251+
BootSourceOverrideEnabled: wantEnabled,
252+
BootSourceOverrideTarget: schemas.PxeBootSource,
253+
}
241254
// TODO: cover setting BootSourceOverrideMode with BIOS settings profile
242-
// Only skip setting BootSourceOverrideMode for older BMCs that don't report it
255+
// Only set BootSourceOverrideMode when the BMC reports it; older BMCs that
256+
// don't expose it will reject the field.
243257
if system.Boot.BootSourceOverrideMode != "" && system.Boot.BootSourceOverrideMode != schemas.UEFIBootSourceOverrideMode {
244-
setBoot = pxeBootWithSettingUEFIBootMode
245-
} else {
246-
setBoot = pxeBootWithoutSettingUEFIBootMode
258+
setBoot.BootSourceOverrideMode = schemas.UEFIBootSourceOverrideMode
247259
}
248260

249261
// TODO: pass logging context from caller
250262
log := ctrl.LoggerFrom(ctx)
251-
log.V(2).Info("Setting PXE boot once", "SystemURI", systemURI, "Boot settings", setBoot)
263+
log.V(2).Info("Setting boot override", "SystemURI", systemURI, "Boot settings", setBoot)
264+
if err := system.SetBoot(&setBoot); err != nil {
265+
return fmt.Errorf("failed to set boot override: %w", err)
266+
}
267+
return nil
268+
}
269+
270+
// ClearBootOverride disables any active Redfish boot source override so the
271+
// system uses its persistent boot order on the next power-on. No SetBoot call
272+
// is issued when the override is already disabled.
273+
func (r *RedfishBaseBMC) ClearBootOverride(ctx context.Context, systemURI string) error {
274+
system, err := r.getSystemFromUri(ctx, systemURI)
275+
if err != nil {
276+
return fmt.Errorf("failed to get systems: %w", err)
277+
}
278+
if system.Boot.BootSourceOverrideEnabled == "" ||
279+
system.Boot.BootSourceOverrideEnabled == schemas.DisabledBootSourceOverrideEnabled {
280+
return nil
281+
}
282+
283+
setBoot := clearedBootOverride
284+
log := ctrl.LoggerFrom(ctx)
285+
log.V(2).Info("Clearing boot override", "SystemURI", systemURI, "Boot settings", setBoot)
252286
if err := system.SetBoot(&setBoot); err != nil {
253-
return fmt.Errorf("failed to set the boot order: %w", err)
287+
return fmt.Errorf("failed to clear boot override: %w", err)
254288
}
255289
return nil
256290
}

bmc/redfish_local.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ const (
2727
type RedfishLocalBMC struct {
2828
*RedfishBaseBMC
2929
// registryURL is an optional base URL (e.g. http://host:port). When set,
30-
// SetPXEBootOnce posts dummy registration data to simulate a probe boot.
30+
// SetBootOverride posts dummy registration data to simulate a probe boot.
3131
registryURL string
3232
}
3333

3434
// NewRedfishLocalBMCClientWithRegistry creates a RedfishLocalBMC that, after
35-
// SetPXEBootOnce, simulates probe registration by posting dummy network data to
35+
// SetBootOverride, simulates probe registration by posting dummy network data to
3636
// registryBaseURL/register. Use this in place of RedfishWithRegistryPatch for dev/tilt
3737
// environments where a real probe will not run.
3838
func NewRedfishLocalBMCClientWithRegistry(ctx context.Context, options Options, registryBaseURL string) (BMC, error) {
@@ -260,14 +260,16 @@ func (r *RedfishLocalBMC) CheckBMCPendingComponentUpgrade(_ context.Context, _ C
260260
return false, nil
261261
}
262262

263-
// SetPXEBootOnce sets the boot device for the next system boot using Redfish.
264-
// When a registryURL is configured, it additionally simulates probe registration
265-
// by posting dummy network data to the registry in a background goroutine.
266-
func (r *RedfishLocalBMC) SetPXEBootOnce(ctx context.Context, systemURI string) error {
267-
if err := r.RedfishBaseBMC.SetPXEBootOnce(ctx, systemURI); err != nil {
263+
// SetBootOverride sets the network-boot override and, for one-shot overrides
264+
// only, simulates probe registration by posting dummy network data to the
265+
// configured registryURL in a background goroutine. The persistent override
266+
// path skips the registration simulation since maintenance flows do not go
267+
// through discovery.
268+
func (r *RedfishLocalBMC) SetBootOverride(ctx context.Context, systemURI string, persistent bool) error {
269+
if err := r.RedfishBaseBMC.SetBootOverride(ctx, systemURI, persistent); err != nil {
268270
return err
269271
}
270-
if r.registryURL == "" {
272+
if persistent || r.registryURL == "" {
271273
return nil
272274
}
273275
go func() {

bmc/redfish_supermicro.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,34 @@ type SupermicroRedfishBMC struct {
1616
*RedfishBaseBMC
1717
}
1818

19-
// SetPXEBootOnce sets the boot device for the next system boot.
20-
// Supermicro requires explicitly setting BootSourceOverrideMode to UEFI.
21-
func (r *SupermicroRedfishBMC) SetPXEBootOnce(ctx context.Context, systemURI string) error {
19+
// SetBootOverride sets a network-boot override on Supermicro hardware, which
20+
// requires explicitly setting BootSourceOverrideMode to UEFI for the override
21+
// to take effect.
22+
func (r *SupermicroRedfishBMC) SetBootOverride(ctx context.Context, systemURI string, persistent bool) error {
2223
system, err := r.getSystemFromUri(ctx, systemURI)
2324
if err != nil {
2425
return fmt.Errorf("failed to get systems: %w", err)
2526
}
27+
wantEnabled := schemas.OnceBootSourceOverrideEnabled
28+
if persistent {
29+
wantEnabled = schemas.ContinuousBootSourceOverrideEnabled
30+
if system.Boot.BootSourceOverrideEnabled == schemas.ContinuousBootSourceOverrideEnabled &&
31+
system.Boot.BootSourceOverrideTarget == schemas.PxeBootSource &&
32+
system.Boot.BootSourceOverrideMode == schemas.UEFIBootSourceOverrideMode {
33+
return nil
34+
}
35+
}
2636

2737
setBoot := &schemas.Boot{
28-
BootSourceOverrideEnabled: schemas.OnceBootSourceOverrideEnabled,
38+
BootSourceOverrideEnabled: wantEnabled,
2939
BootSourceOverrideMode: schemas.UEFIBootSourceOverrideMode,
3040
BootSourceOverrideTarget: schemas.PxeBootSource,
3141
}
3242

3343
log := ctrl.LoggerFrom(ctx)
34-
log.V(2).Info("Setting PXE boot once (Supermicro)", "SystemURI", systemURI, "Boot settings", setBoot)
44+
log.V(2).Info("Setting boot override (Supermicro)", "SystemURI", systemURI, "Boot settings", setBoot)
3545
if err := system.SetBoot(setBoot); err != nil {
36-
return fmt.Errorf("failed to set the boot order: %w", err)
46+
return fmt.Errorf("failed to set boot override: %w", err)
3747
}
3848
return nil
3949
}

internal/bmcutils/bmcutils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ type createBMCClientConfig struct {
4545
}
4646

4747
// WithRegistryURL configures the BMC client to POST dummy registration data to
48-
// the given base URL after SetPXEBootOnce. Used with ProtocolRedfishWithRegistryPatch to
48+
// the given base URL after SetBootOverride. Used with ProtocolRedfishWithRegistryPatch to
4949
// simulate probe boot registration without a real K8s Job.
5050
func WithRegistryURL(url string) CreateBMCClientOption {
5151
return func(c *createBMCClientConfig) {

internal/controller/server_controller.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,12 @@ func (r *ServerReconciler) handleDiscoveryState(ctx context.Context, bmcClient b
424424

425425
func (r *ServerReconciler) handleAvailableState(ctx context.Context, bmcClient bmc.BMC, server *metalv1alpha1.Server) (bool, error) {
426426
log := ctrl.LoggerFrom(ctx)
427+
// Self-heal: drop any boot override that may have been left set by a
428+
// previous Maintenance cycle that ended unexpectedly. ClearBootOverride
429+
// is a no-op when no override is active.
430+
if err := bmcClient.ClearBootOverride(ctx, server.Spec.SystemURI); err != nil {
431+
return false, fmt.Errorf("failed to clear boot override: %w", err)
432+
}
427433
serverBase := server.DeepCopy()
428434
if server.Status.PowerState != metalv1alpha1.ServerOffPowerState {
429435
server.Spec.Power = metalv1alpha1.PowerOff
@@ -469,6 +475,12 @@ func (r *ServerReconciler) handleAvailableState(ctx context.Context, bmcClient b
469475

470476
func (r *ServerReconciler) handleReservedState(ctx context.Context, bmcClient bmc.BMC, server *metalv1alpha1.Server) (bool, error) {
471477
log := ctrl.LoggerFrom(ctx)
478+
// Self-heal: drop any boot override that may have been left set by a
479+
// previous Maintenance cycle that ended unexpectedly. ClearBootOverride
480+
// is a no-op when no override is active.
481+
if err := bmcClient.ClearBootOverride(ctx, server.Spec.SystemURI); err != nil {
482+
return false, fmt.Errorf("failed to clear boot override: %w", err)
483+
}
472484
serverClaimRef := server.Spec.ServerClaimRef
473485
if serverClaimRef == nil {
474486
if modified, err := r.patchServerState(ctx, server, metalv1alpha1.ServerStateAvailable); err != nil || modified {
@@ -576,11 +588,21 @@ func (r *ServerReconciler) handleMaintenanceState(ctx context.Context, bmcClient
576588
if err := r.updateServerStatusFromSystemInfo(ctx, bmcClient, server); err != nil {
577589
return false, fmt.Errorf("failed to update server status system info: %w", err)
578590
}
591+
if err := bmcClient.ClearBootOverride(ctx, server.Spec.SystemURI); err != nil {
592+
return false, fmt.Errorf("failed to clear boot override on maintenance exit: %w", err)
593+
}
579594
if server.Spec.ServerClaimRef == nil {
580595
return r.patchServerState(ctx, server, metalv1alpha1.ServerStateInitial)
581596
}
582597
return r.patchServerState(ctx, server, metalv1alpha1.ServerStateReserved)
583598
}
599+
// Re-assert the persistent network-boot override on every reconcile while in Maintenance.
600+
// This protects against reboots not driven by metal-operator (e.g. a vendor BIOS
601+
// upgrade task rebooting the system itself) falling through to disk and starting
602+
// the production OS while the host is still being worked on.
603+
if err := bmcClient.SetBootOverride(ctx, server.Spec.SystemURI, true); err != nil {
604+
return false, fmt.Errorf("failed to set persistent network boot for maintenance: %w", err)
605+
}
584606
if err := r.ensureServerPowerState(ctx, bmcClient, server); err != nil {
585607
return false, fmt.Errorf("failed to ensure server power state: %w", err)
586608
}
@@ -914,7 +936,7 @@ func (r *ServerReconciler) pxeBootServer(ctx context.Context, bmcClient bmc.BMC,
914936
return fmt.Errorf("can only PXE boot server with valid BMC ref or inline BMC configuration")
915937
}
916938

917-
if err := bmcClient.SetPXEBootOnce(ctx, server.Spec.SystemURI); err != nil {
939+
if err := bmcClient.SetBootOverride(ctx, server.Spec.SystemURI, false); err != nil {
918940
return fmt.Errorf("failed to set PXE boot one for server: %w", err)
919941
}
920942
return nil

0 commit comments

Comments
 (0)