Skip to content
8 changes: 8 additions & 0 deletions bmc/mock/server/data/Managers/BMC/Settings/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"@odata.type": "#ManagerSettings.v1_0_0.Settings",
"Id": "Settings",
"Name": "BMC Pending Settings",
"Attributes": {},
"@odata.id": "/redfish/v1/Managers/BMC/Settings",
"@Redfish.Copyright": "Copyright 2014-2023 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}
10 changes: 10 additions & 0 deletions bmc/mock/server/data/Managers/BMC/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
"@odata.id": "/redfish/v1/Chassis/1U"
}
},
"Attributes": {
"abc": "bar",
"fooreboot": 123,
"composite": "foo-123"
},
"Actions": {
"#Manager.Reset": {
"target": "/redfish/v1/Managers/BMC/Actions/Manager.Reset",
Expand All @@ -95,6 +100,11 @@
]
}
},
"@Redfish.Settings": {
"SettingsObject": {
"@odata.id": "/redfish/v1/Managers/BMC/Settings"
}
},
"@odata.id": "/redfish/v1/Managers/BMC",
"@Redfish.Copyright": "Copyright 2014-2023 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}
41 changes: 41 additions & 0 deletions bmc/mock/server/data/Registries/BMCAttributeRegistry/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"@odata.type": "#AttributeRegistry.v1_3_7.AttributeRegistry",
"Id": "BMCAttributeRegistry",
"Name": "BMC Attribute Registry",
"Language": "en",
"OwningEntity": "Metal Operator",
"RegistryVersion": "1.0.0",
"RegistryEntries": {
"Attributes": [
{
"AttributeName": "abc",
"DisplayName": "abc",
"Type": "String",
"ReadOnly": false,
"Immutable": false,
"Hidden": false,
"ResetRequired": false
},
{
"AttributeName": "fooreboot",
"DisplayName": "fooreboot",
"Type": "Integer",
"ReadOnly": false,
"Immutable": false,
"Hidden": false,
"ResetRequired": true
},
{
"AttributeName": "composite",
"DisplayName": "composite",
"Type": "String",
"ReadOnly": false,
"Immutable": false,
"Hidden": false,
"ResetRequired": false
}
]
},
"@odata.id": "/redfish/v1/Registries/BMCAttributeRegistry",
"@Redfish.Copyright": "Copyright 2014-2023 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}
5 changes: 4 additions & 1 deletion bmc/mock/server/data/Registries/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
"@odata.type": "#MessageRegistryFileCollection.MessageRegistryFileCollection",
"Name": "Registry File Collection",
"Description": "Registry Repository",
"Members@odata.count": 1,
"Members@odata.count": 3,
"Members": [
{
"@odata.id": "/redfish/v1/Registries/Base.1.5.0"
},
{
"@odata.id": "/redfish/v1/Registries/BiosAttributeRegistry.v1_0_0"
},
{
"@odata.id": "/redfish/v1/Registries/BMCAttributeRegistry"
}
],
"@odata.id": "/redfish/v1/Registries",
Expand Down
205 changes: 195 additions & 10 deletions bmc/mock/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,53 @@ const (

biosSettingsPathSuffix = "Bios/Settings"
attributesKey = "Attributes"

// Fixed file paths for BMC manager resources.
bmcFilePath = "data/Managers/BMC/index.json"
bmcSettingsFilePath = "data/Managers/BMC/Settings/index.json"
)

// noRebootSettings and noRebootBMCSettings are populated at init time by
// scanning the embedded attribute registries for entries where ResetRequired == false.
var (
noRebootSettings []string
noRebootBMCSettings []string
)

// BIOS settings that can be applied without reboot.
var noRebootSettings = []string{"AdminPhone"}
func init() {
noRebootSettings = mustLoadNoRebootAttrs("data/Registries/BiosAttributeRegistry.v1_0_0.json")
noRebootBMCSettings = mustLoadNoRebootAttrs("data/Registries/BMCAttributeRegistry/index.json")
}

// mustLoadNoRebootAttrs reads an attribute registry JSON from the embedded FS and
// returns the names of all attributes whose ResetRequired field is false.
// It panics if the file cannot be read or parsed so that a renamed or malformed
// embedded registry is caught immediately at startup rather than silently
// flipping all settings onto the slow (reboot-required) path.
func mustLoadNoRebootAttrs(registryPath string) []string {
data, err := dataFS.ReadFile(registryPath)
if err != nil {
panic(fmt.Sprintf("read %s: %v", registryPath, err))
}
var registry struct {
RegistryEntries struct {
Attributes []struct {
AttributeName string `json:"AttributeName"`
ResetRequired bool `json:"ResetRequired"`
} `json:"Attributes"`
} `json:"RegistryEntries"`
}
if err := json.Unmarshal(data, &registry); err != nil {
panic(fmt.Sprintf("parse %s: %v", registryPath, err))
}
var result []string
for _, attr := range registry.RegistryEntries.Attributes {
if !attr.ResetRequired {
result = append(result, attr.AttributeName)
}
}
return result
Comment thread
nagadeesh-nagaraja marked this conversation as resolved.
}

// Power state categories for system reset actions.
var (
Expand Down Expand Up @@ -242,6 +285,11 @@ func (s *MockServer) handlePatch(w http.ResponseWriter, r *http.Request) {
return
}

if err := s.applyBMCSettings(r.URL.Path, update); err != nil {
s.handleError(w, r, err)
return
}

mergeJSON(base, update)
s.saveResource(filePath, base)

Expand Down Expand Up @@ -441,23 +489,160 @@ func (s *MockServer) doPowerReset(systemPath, basePath string) {
}

func (s *MockServer) doBMCReset(bmcPath string) {
// Simulate the BMC being offline during reset.
// The lock set by handleBMCReset prevents new POST operations during this window.
time.Sleep(150 * time.Millisecond)

s.mu.Lock()
if base, ok := s.overrides[bmcPath].(map[string]any); ok {
base["PowerState"] = PowerOffState
s.log.Info("Powered off the BMC")
s.setLocked(base, false)
s.log.Info("BMC reset complete")
}
s.mu.Unlock()

time.Sleep(150 * time.Millisecond)
if err := s.applyPendingBMCSettings(); err != nil {
s.log.Error(err, "Failed to apply pending BMC settings")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func (s *MockServer) applyBMCSettings(urlPath string, update map[string]any) error {
if !strings.Contains(urlPath, "Managers/BMC/Settings") {
return nil
}

attrs, ok := update[attributesKey].(map[string]any)
if !ok || len(attrs) == 0 {
return nil
}

// Attrs that can be applied immediately without a BMC reset.
immediate := make(map[string]any)
for key, val := range attrs {
if slices.Contains(noRebootBMCSettings, key) {
immediate[key] = val
}
}

if len(immediate) == 0 {
return nil
}

// Apply to the current BMC manager resource.
s.mu.Lock()
if base, ok := s.overrides[bmcPath].(map[string]any); ok {
base["PowerState"] = PowerOnState
s.setLocked(base, false)
s.log.Info("Powered on the BMC")
defer s.mu.Unlock()

bmcBase, err := s.loadResourceLocked(bmcFilePath)
if err != nil {
return err
}
s.mu.Unlock()

// If the BMC is mid-reset (locked), leave the immediate settings in attrs
// so they are written to the pending Settings resource and picked up by
// applyPendingBMCSettings once the reset completes.
if s.isLocked(bmcBase) {
return nil
}

s.log.Info("Applying BMC settings without reset", "settings", immediate)

// Remove immediate settings from the pending update so they are not
// written to the Settings (pending) resource.
for key := range immediate {
delete(attrs, key)
}

if bmcAttrs, ok := bmcBase[attributesKey].(map[string]any); ok {
maps.Copy(bmcAttrs, immediate)
}
s.overrides[bmcFilePath] = bmcBase

return nil
}

func (s *MockServer) applyPendingBMCSettings() error {
s.mu.Lock()
defer s.mu.Unlock()

pending, err := s.loadResourceLocked(bmcSettingsFilePath)
if err != nil {
return err
}

pendingAttrs, ok := pending[attributesKey].(map[string]any)
if !ok || len(pendingAttrs) == 0 {
return nil
}

current, err := s.loadResourceLocked(bmcFilePath)
if err != nil {
return err
}

currentAttrs, ok := current[attributesKey].(map[string]any)
if !ok {
return nil
}

maps.Copy(currentAttrs, pendingAttrs)
pending[attributesKey] = map[string]any{}

s.overrides[bmcFilePath] = current
s.overrides[bmcSettingsFilePath] = pending
s.log.Info("Applied pending BMC settings")

return nil
}

// GetBMCSettingAttr returns the current BMC Attributes map for the given managerID
// (e.g. "BMC"). Returns nil if the resource cannot be loaded.
func (s *MockServer) GetBMCSettingAttr(managerID string) map[string]any {
filePath := fmt.Sprintf("data/Managers/%s/index.json", managerID)
resource, err := s.loadResource(filePath)
if err != nil {
return nil
}
attrs, _ := resource[attributesKey].(map[string]any)
return attrs
}

// ResetBMCSettings resets the BMC attribute state on the server to defaults,
// clearing both current and pending attributes. managerID is the folder name under data/Managers/ (e.g. "BMC").
func (s *MockServer) ResetBMCSettings(managerID string) {
filePath := fmt.Sprintf("data/Managers/%s/index.json", managerID)
settingsFilePath := fmt.Sprintf("data/Managers/%s/Settings/index.json", managerID)
s.mu.Lock()
defer s.mu.Unlock()
s.resetResourceFromEmbeddedLocked(filePath)
s.resetResourceFromEmbeddedLocked(settingsFilePath)
}

// ResetBIOSSettings resets the BIOS attribute state on the server to defaults,
// clearing both current and pending attributes. systemID is the folder name under data/Systems/ (e.g. "437XR1138R2").
func (s *MockServer) ResetBIOSSettings(systemID string) {
filePath := fmt.Sprintf("data/Systems/%s/Bios/index.json", systemID)
settingsFilePath := fmt.Sprintf("data/Systems/%s/Bios/Settings/index.json", systemID)
s.mu.Lock()
defer s.mu.Unlock()
s.resetResourceFromEmbeddedLocked(filePath)
s.resetResourceFromEmbeddedLocked(settingsFilePath)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// resetResourceFromEmbeddedLocked replaces the override for filePath with the
// full contents of the embedded file, clearing all mutated fields including
// resourceLock, Attributes, and any other state accumulated during a test.
// The caller must hold s.mu.
func (s *MockServer) resetResourceFromEmbeddedLocked(filePath string) {
raw, err := dataFS.ReadFile(filePath)
if err != nil {
s.log.Error(err, "Failed to read embedded default", "path", filePath)
return
}
var defaults map[string]any
if err := json.Unmarshal(raw, &defaults); err != nil {
s.log.Error(err, "Failed to parse embedded default", "path", filePath)
return
}
s.overrides[filePath] = defaults
}

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