@@ -40,10 +40,53 @@ const (
4040
4141 biosSettingsPathSuffix = "Bios/Settings"
4242 attributesKey = "Attributes"
43+
44+ // Fixed file paths for BMC manager resources.
45+ bmcFilePath = "data/Managers/BMC/index.json"
46+ bmcSettingsFilePath = "data/Managers/BMC/Settings/index.json"
47+ )
48+
49+ // noRebootSettings and noRebootBMCSettings are populated at init time by
50+ // scanning the embedded attribute registries for entries where ResetRequired == false.
51+ var (
52+ noRebootSettings []string
53+ noRebootBMCSettings []string
4354)
4455
45- // BIOS settings that can be applied without reboot.
46- var noRebootSettings = []string {"AdminPhone" }
56+ func init () {
57+ noRebootSettings = mustLoadNoRebootAttrs ("data/Registries/BiosAttributeRegistry.v1_0_0.json" )
58+ noRebootBMCSettings = mustLoadNoRebootAttrs ("data/Registries/BMCAttributeRegistry/index.json" )
59+ }
60+
61+ // mustLoadNoRebootAttrs reads an attribute registry JSON from the embedded FS and
62+ // returns the names of all attributes whose ResetRequired field is false.
63+ // It panics if the file cannot be read or parsed so that a renamed or malformed
64+ // embedded registry is caught immediately at startup rather than silently
65+ // flipping all settings onto the slow (reboot-required) path.
66+ func mustLoadNoRebootAttrs (registryPath string ) []string {
67+ data , err := dataFS .ReadFile (registryPath )
68+ if err != nil {
69+ panic (fmt .Sprintf ("read %s: %v" , registryPath , err ))
70+ }
71+ var registry struct {
72+ RegistryEntries struct {
73+ Attributes []struct {
74+ AttributeName string `json:"AttributeName"`
75+ ResetRequired bool `json:"ResetRequired"`
76+ } `json:"Attributes"`
77+ } `json:"RegistryEntries"`
78+ }
79+ if err := json .Unmarshal (data , & registry ); err != nil {
80+ panic (fmt .Sprintf ("parse %s: %v" , registryPath , err ))
81+ }
82+ var result []string
83+ for _ , attr := range registry .RegistryEntries .Attributes {
84+ if ! attr .ResetRequired {
85+ result = append (result , attr .AttributeName )
86+ }
87+ }
88+ return result
89+ }
4790
4891// Power state categories for system reset actions.
4992var (
@@ -242,6 +285,11 @@ func (s *MockServer) handlePatch(w http.ResponseWriter, r *http.Request) {
242285 return
243286 }
244287
288+ if err := s .applyBMCSettings (r .URL .Path , update ); err != nil {
289+ s .handleError (w , r , err )
290+ return
291+ }
292+
245293 mergeJSON (base , update )
246294 s .saveResource (filePath , base )
247295
@@ -441,23 +489,160 @@ func (s *MockServer) doPowerReset(systemPath, basePath string) {
441489}
442490
443491func (s * MockServer ) doBMCReset (bmcPath string ) {
492+ // Simulate the BMC being offline during reset.
493+ // The lock set by handleBMCReset prevents new POST operations during this window.
444494 time .Sleep (150 * time .Millisecond )
495+
445496 s .mu .Lock ()
446497 if base , ok := s .overrides [bmcPath ].(map [string ]any ); ok {
447- base [ "PowerState" ] = PowerOffState
448- s .log .Info ("Powered off the BMC " )
498+ s . setLocked ( base , false )
499+ s .log .Info ("BMC reset complete " )
449500 }
450501 s .mu .Unlock ()
451502
452- time .Sleep (150 * time .Millisecond )
503+ if err := s .applyPendingBMCSettings (); err != nil {
504+ s .log .Error (err , "Failed to apply pending BMC settings" )
505+ }
506+ }
453507
508+ func (s * MockServer ) applyBMCSettings (urlPath string , update map [string ]any ) error {
509+ if ! strings .Contains (urlPath , "Managers/BMC/Settings" ) {
510+ return nil
511+ }
512+
513+ attrs , ok := update [attributesKey ].(map [string ]any )
514+ if ! ok || len (attrs ) == 0 {
515+ return nil
516+ }
517+
518+ // Attrs that can be applied immediately without a BMC reset.
519+ immediate := make (map [string ]any )
520+ for key , val := range attrs {
521+ if slices .Contains (noRebootBMCSettings , key ) {
522+ immediate [key ] = val
523+ }
524+ }
525+
526+ if len (immediate ) == 0 {
527+ return nil
528+ }
529+
530+ // Apply to the current BMC manager resource.
454531 s .mu .Lock ()
455- if base , ok := s .overrides [bmcPath ].(map [string ]any ); ok {
456- base ["PowerState" ] = PowerOnState
457- s .setLocked (base , false )
458- s .log .Info ("Powered on the BMC" )
532+ defer s .mu .Unlock ()
533+
534+ bmcBase , err := s .loadResourceLocked (bmcFilePath )
535+ if err != nil {
536+ return err
459537 }
460- s .mu .Unlock ()
538+
539+ // If the BMC is mid-reset (locked), leave the immediate settings in attrs
540+ // so they are written to the pending Settings resource and picked up by
541+ // applyPendingBMCSettings once the reset completes.
542+ if s .isLocked (bmcBase ) {
543+ return nil
544+ }
545+
546+ s .log .Info ("Applying BMC settings without reset" , "settings" , immediate )
547+
548+ // Remove immediate settings from the pending update so they are not
549+ // written to the Settings (pending) resource.
550+ for key := range immediate {
551+ delete (attrs , key )
552+ }
553+
554+ if bmcAttrs , ok := bmcBase [attributesKey ].(map [string ]any ); ok {
555+ maps .Copy (bmcAttrs , immediate )
556+ }
557+ s .overrides [bmcFilePath ] = bmcBase
558+
559+ return nil
560+ }
561+
562+ func (s * MockServer ) applyPendingBMCSettings () error {
563+ s .mu .Lock ()
564+ defer s .mu .Unlock ()
565+
566+ pending , err := s .loadResourceLocked (bmcSettingsFilePath )
567+ if err != nil {
568+ return err
569+ }
570+
571+ pendingAttrs , ok := pending [attributesKey ].(map [string ]any )
572+ if ! ok || len (pendingAttrs ) == 0 {
573+ return nil
574+ }
575+
576+ current , err := s .loadResourceLocked (bmcFilePath )
577+ if err != nil {
578+ return err
579+ }
580+
581+ currentAttrs , ok := current [attributesKey ].(map [string ]any )
582+ if ! ok {
583+ return nil
584+ }
585+
586+ maps .Copy (currentAttrs , pendingAttrs )
587+ pending [attributesKey ] = map [string ]any {}
588+
589+ s .overrides [bmcFilePath ] = current
590+ s .overrides [bmcSettingsFilePath ] = pending
591+ s .log .Info ("Applied pending BMC settings" )
592+
593+ return nil
594+ }
595+
596+ // GetBMCSettingAttr returns the current BMC Attributes map for the given managerID
597+ // (e.g. "BMC"). Returns nil if the resource cannot be loaded.
598+ func (s * MockServer ) GetBMCSettingAttr (managerID string ) map [string ]any {
599+ filePath := fmt .Sprintf ("data/Managers/%s/index.json" , managerID )
600+ resource , err := s .loadResource (filePath )
601+ if err != nil {
602+ return nil
603+ }
604+ attrs , _ := resource [attributesKey ].(map [string ]any )
605+ return attrs
606+ }
607+
608+ // ResetBMCSettings resets the BMC attribute state on the server to defaults,
609+ // clearing both current and pending attributes. managerID is the folder name under data/Managers/ (e.g. "BMC").
610+ func (s * MockServer ) ResetBMCSettings (managerID string ) {
611+ filePath := fmt .Sprintf ("data/Managers/%s/index.json" , managerID )
612+ settingsFilePath := fmt .Sprintf ("data/Managers/%s/Settings/index.json" , managerID )
613+ s .mu .Lock ()
614+ defer s .mu .Unlock ()
615+ s .resetResourceFromEmbeddedLocked (filePath )
616+ s .resetResourceFromEmbeddedLocked (settingsFilePath )
617+ }
618+
619+ // ResetBIOSSettings resets the BIOS attribute state on the server to defaults,
620+ // clearing both current and pending attributes. systemID is the folder name under data/Systems/ (e.g. "437XR1138R2").
621+ func (s * MockServer ) ResetBIOSSettings (systemID string ) {
622+ filePath := fmt .Sprintf ("data/Systems/%s/Bios/index.json" , systemID )
623+ settingsFilePath := fmt .Sprintf ("data/Systems/%s/Bios/Settings/index.json" , systemID )
624+ s .mu .Lock ()
625+ defer s .mu .Unlock ()
626+ s .resetResourceFromEmbeddedLocked (filePath )
627+ s .resetResourceFromEmbeddedLocked (settingsFilePath )
628+ }
629+
630+ // resetResourceFromEmbeddedLocked replaces the override for filePath with the
631+ // full contents of the embedded file, clearing all mutated fields including
632+ // resourceLock, Attributes, and any other state accumulated during a test.
633+ // The caller must hold s.mu.
634+ func (s * MockServer ) resetResourceFromEmbeddedLocked (filePath string ) {
635+ raw , err := dataFS .ReadFile (filePath )
636+ if err != nil {
637+ s .log .Error (err , "Failed to read embedded default" , "path" , filePath )
638+ return
639+ }
640+ var defaults map [string ]any
641+ if err := json .Unmarshal (raw , & defaults ); err != nil {
642+ s .log .Error (err , "Failed to parse embedded default" , "path" , filePath )
643+ return
644+ }
645+ s .overrides [filePath ] = defaults
461646}
462647
463648func (s * MockServer ) applyBiosSettings (urlPath string , update map [string ]any ) error {
0 commit comments