Skip to content

Commit 66996aa

Browse files
Move BMCSettings mock redfish_local method into go mock server (#790)
* Move BMCSettings redfish_local into go mock server * Review comments * amend server fail logic * fix reset to default logic * fix the data count lenght in mocked data * Rename variable * fix redundant check * fix newer tests workflow from main branch
1 parent 9bcdb7e commit 66996aa

13 files changed

Lines changed: 406 additions & 128 deletions
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"@odata.type": "#ManagerSettings.v1_0_0.Settings",
3+
"Id": "Settings",
4+
"Name": "BMC Pending Settings",
5+
"Attributes": {},
6+
"@odata.id": "/redfish/v1/Managers/BMC/Settings",
7+
"@Redfish.Copyright": "Copyright 2014-2023 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
8+
}

bmc/mock/server/data/Managers/BMC/index.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
"@odata.id": "/redfish/v1/Chassis/1U"
8787
}
8888
},
89+
"Attributes": {
90+
"abc": "bar",
91+
"fooreboot": 123,
92+
"composite": "foo-123"
93+
},
8994
"Actions": {
9095
"#Manager.Reset": {
9196
"target": "/redfish/v1/Managers/BMC/Actions/Manager.Reset",
@@ -95,6 +100,11 @@
95100
]
96101
}
97102
},
103+
"@Redfish.Settings": {
104+
"SettingsObject": {
105+
"@odata.id": "/redfish/v1/Managers/BMC/Settings"
106+
}
107+
},
98108
"@odata.id": "/redfish/v1/Managers/BMC",
99109
"@Redfish.Copyright": "Copyright 2014-2023 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
100110
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"@odata.type": "#AttributeRegistry.v1_3_7.AttributeRegistry",
3+
"Id": "BMCAttributeRegistry",
4+
"Name": "BMC Attribute Registry",
5+
"Language": "en",
6+
"OwningEntity": "Metal Operator",
7+
"RegistryVersion": "1.0.0",
8+
"RegistryEntries": {
9+
"Attributes": [
10+
{
11+
"AttributeName": "abc",
12+
"DisplayName": "abc",
13+
"Type": "String",
14+
"ReadOnly": false,
15+
"Immutable": false,
16+
"Hidden": false,
17+
"ResetRequired": false
18+
},
19+
{
20+
"AttributeName": "fooreboot",
21+
"DisplayName": "fooreboot",
22+
"Type": "Integer",
23+
"ReadOnly": false,
24+
"Immutable": false,
25+
"Hidden": false,
26+
"ResetRequired": true
27+
},
28+
{
29+
"AttributeName": "composite",
30+
"DisplayName": "composite",
31+
"Type": "String",
32+
"ReadOnly": false,
33+
"Immutable": false,
34+
"Hidden": false,
35+
"ResetRequired": false
36+
}
37+
]
38+
},
39+
"@odata.id": "/redfish/v1/Registries/BMCAttributeRegistry",
40+
"@Redfish.Copyright": "Copyright 2014-2023 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
41+
}

bmc/mock/server/data/Registries/index.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
"@odata.type": "#MessageRegistryFileCollection.MessageRegistryFileCollection",
33
"Name": "Registry File Collection",
44
"Description": "Registry Repository",
5-
"Members@odata.count": 1,
5+
"Members@odata.count": 3,
66
"Members": [
77
{
88
"@odata.id": "/redfish/v1/Registries/Base.1.5.0"
99
},
1010
{
1111
"@odata.id": "/redfish/v1/Registries/BiosAttributeRegistry.v1_0_0"
12+
},
13+
{
14+
"@odata.id": "/redfish/v1/Registries/BMCAttributeRegistry"
1215
}
1316
],
1417
"@odata.id": "/redfish/v1/Registries",

bmc/mock/server/server.go

Lines changed: 195 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
4992
var (
@@ -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

443491
func (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

463648
func (s *MockServer) applyBiosSettings(urlPath string, update map[string]any) error {

0 commit comments

Comments
 (0)