diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 78b448f..4c358a6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,5 +18,5 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v3 with: - args: -v --config .golangci.yml --timeout=5m + args: -v --config .golangci.yml --timeout=5m --out-format=colored-line-number version: latest diff --git a/config/asrockrack.go b/config/asrockrack.go index 51c995b..64a7ac9 100644 --- a/config/asrockrack.go +++ b/config/asrockrack.go @@ -105,8 +105,51 @@ func (cm *asrockrackVendorConfig) Marshal() (string, error) { } } +func (cm *asrockrackVendorConfig) Unmarshal(cfgData string) error { + return xml.Unmarshal([]byte(cfgData), cm.ConfigData) +} + +func (cm *asrockrackVendorConfig) StandardConfig() (biosConfig map[string]string, err error) { + return biosConfig, err +} + // Generic config options +func (cm *asrockrackVendorConfig) BootOrder(mode string) error { + // Unimplemented + return nil +} + +func (cm *asrockrackVendorConfig) BootMode(mode string) error { + // Unimplemented + return nil +} + +func (cm *asrockrackVendorConfig) IntelSGX(mode string) error { + // Unimplemented + return nil +} + +func (cm *asrockrackVendorConfig) SecureBoot(enable bool) error { + // Unimplemented + return nil +} + +func (cm *asrockrackVendorConfig) TPM(enable bool) error { + // Unimplemented + return nil +} + +func (cm *asrockrackVendorConfig) SMT(enable bool) error { + // Unimplemented + return nil +} + +func (cm *asrockrackVendorConfig) SRIOV(enable bool) error { + // Unimplemented + return nil +} + func (cm *asrockrackVendorConfig) EnableTPM() { // Unimplemented } diff --git a/config/dell.go b/config/dell.go index 4bfc882..ec0b3e7 100644 --- a/config/dell.go +++ b/config/dell.go @@ -129,8 +129,51 @@ func (cm *dellVendorConfig) Marshal() (string, error) { } } +func (cm *dellVendorConfig) Unmarshal(cfgData string) error { + return xml.Unmarshal([]byte(cfgData), cm.ConfigData) +} + +func (cm *dellVendorConfig) StandardConfig() (biosConfig map[string]string, err error) { + return biosConfig, err +} + // Generic config options +func (cm *dellVendorConfig) BootOrder(mode string) error { + // Unimplemented + return nil +} + +func (cm *dellVendorConfig) BootMode(mode string) error { + // Unimplemented + return nil +} + +func (cm *dellVendorConfig) IntelSGX(mode string) error { + // Unimplemented + return nil +} + +func (cm *dellVendorConfig) SecureBoot(enable bool) error { + // Unimplemented + return nil +} + +func (cm *dellVendorConfig) TPM(enable bool) error { + // Unimplemented + return nil +} + +func (cm *dellVendorConfig) SMT(enable bool) error { + // Unimplemented + return nil +} + +func (cm *dellVendorConfig) SRIOV(enable bool) error { + // Unimplemented + return nil +} + func (cm *dellVendorConfig) EnableTPM() { cm.Raw("EnableTPM", "Enabled", []string{"BIOS.Setup.1-1"}) } diff --git a/config/errors.go b/config/errors.go index 60dd15f..62285b1 100644 --- a/config/errors.go +++ b/config/errors.go @@ -7,11 +7,27 @@ import ( var errUnknownConfigFormat = errors.New("unknown config format") var errUnknownVendor = errors.New("unknown/unsupported vendor") +var errUnknownSettingType = errors.New("unknown setting type") + +var errInvalidBootModeOption = errors.New("invalid BootMode option ") +var errInvalidSGXOption = errors.New("invalid SGX option ") func UnknownConfigFormatError(format string) error { return fmt.Errorf("unknown config format %w : %s", errUnknownConfigFormat, format) } +func UnknownSettingType(t string) error { + return fmt.Errorf("unknown setting type %w : %s", errUnknownSettingType, t) +} + func UnknownVendorError(vendorName string) error { return fmt.Errorf("unknown/unsupported vendor %w : %s", errUnknownVendor, vendorName) } + +func InvalidBootModeOption(mode string) error { + return fmt.Errorf("%w : %s", errInvalidBootModeOption, mode) +} + +func InvalidSGXOption(mode string) error { + return fmt.Errorf("%w : %s", errInvalidSGXOption, mode) +} diff --git a/config/interface.go b/config/interface.go index 9684cce..9865422 100644 --- a/config/interface.go +++ b/config/interface.go @@ -7,11 +7,18 @@ import ( ) type VendorConfigManager interface { - EnableTPM() - EnableSRIOV() - Raw(name, value string, menuPath []string) Marshal() (string, error) + Unmarshal(cfgData string) (err error) + StandardConfig() (biosConfig map[string]string, err error) + + BootMode(mode string) error + BootOrder(mode string) error + IntelSGX(mode string) error + SecureBoot(enable bool) error + TPM(enable bool) error + SMT(enable bool) error + SRIOV(enable bool) error } func NewVendorConfigManager(configFormat, vendorName string, vendorOptions map[string]string) (VendorConfigManager, error) { diff --git a/config/supermicro.go b/config/supermicro.go index 25fd02e..30e7092 100644 --- a/config/supermicro.go +++ b/config/supermicro.go @@ -1,8 +1,18 @@ package config import ( + "bytes" "encoding/xml" + "fmt" "strings" + + "golang.org/x/net/html/charset" +) + +const ( + // enabledValue and disabledValue are utilized for bios setting value normalization + enabledValue = "Enabled" + disabledValue = "Disabled" ) type supermicroVendorConfig struct { @@ -28,10 +38,12 @@ type supermicroBiosCfgMenu struct { type supermicroBiosCfgSetting struct { XMLName xml.Name `xml:"Setting"` - Name string `xml:"Name,attr"` + Name string `xml:"name,attr"` Order string `xml:"order,attr"` SelectedOption string `xml:"selectedOption,attr"` Type string `xml:"type,attr"` + CheckedStatus string `xml:"checkedStatus,attr"` + NumericValue string `xml:"numericValue,attr"` } func NewSupermicroVendorConfigManager(configFormat string, vendorOptions map[string]string) (VendorConfigManager, error) { @@ -51,53 +63,56 @@ func NewSupermicroVendorConfigManager(configFormat string, vendorOptions map[str return supermicro, nil } -// FindMenu locates an existing SupermicroBiosCfgMenu if one exists in the ConfigData, if not -// it creates one and returns a pointer to that. -func (cm *supermicroVendorConfig) FindMenu(menuName string, menuRoot *supermicroBiosCfgMenu) (m *supermicroBiosCfgMenu) { - // root is cm.ConfigData.BiosCfg.Menus - for _, m = range menuRoot.Menus { - if m.Name == menuName { - return - } - } +// Function to find or create a setting by path +func (cm *supermicroVendorConfig) FindOrCreateSetting(path []string, value string) *supermicroBiosCfgSetting { + biosCfg := cm.ConfigData.BiosCfg - m.Name = menuName + var currentMenus = &biosCfg.Menus - menuRoot.Menus = append(menuRoot.Menus, m) + for i, part := range path { + if i == len(path)-1 { + // Last part, create or find the setting + for j := range *currentMenus { + for k := range (*currentMenus)[j].Settings { + if (*currentMenus)[j].Settings[k].Name == part { + return (*currentMenus)[j].Settings[k] + } + } + } - return -} + // If no setting found in any menu, create a new setting in the first menu + newSetting := supermicroBiosCfgSetting{Name: part, SelectedOption: ""} + (*currentMenus)[0].Settings = append((*currentMenus)[0].Settings, &newSetting) -// FindMenuSetting locates an existing SupermicroBiosCfgSetting if one exists in the -// ConfigData, if not it creates one and returns a pointer to that. -func (cm *supermicroVendorConfig) FindMenuSetting(m *supermicroBiosCfgMenu, name string) (s *supermicroBiosCfgSetting) { - for _, s = range m.Settings { - if s.Name == name { - return + return (*currentMenus)[0].Settings[len((*currentMenus)[0].Settings)-1] + } else { + // Intermediate part, find or create the menu + currentMenu := cm.FindOrCreateMenu(currentMenus, part) + currentMenus = ¤tMenu.Menus } } - s.Name = name + return nil +} + +// Function to find or create a menu by name +func (cm *supermicroVendorConfig) FindOrCreateMenu(menus *[]*supermicroBiosCfgMenu, name string) *supermicroBiosCfgMenu { + for i := range *menus { + if (*menus)[i].Name == name { + return (*menus)[i] + } + } - m.Settings = append(m.Settings, s) + newMenu := &supermicroBiosCfgMenu{Name: name} + *menus = append(*menus, newMenu) - return + return (*menus)[len(*menus)-1] } func (cm *supermicroVendorConfig) Raw(name, value string, menuPath []string) { - menus := make([]*supermicroBiosCfgMenu, 0, len(menuPath)) - - for i, name := range menuPath { - var m *supermicroBiosCfgMenu - - if i == 0 { - m = cm.FindMenu(name, cm.ConfigData.BiosCfg.Menus[0]) - } else { - m = cm.FindMenu(name, menus[i-1]) - } + menuPath = append(menuPath, name) - menus = append(menus, m) - } + _ = cm.FindOrCreateSetting(menuPath, value) } func (cm *supermicroVendorConfig) Marshal() (string, error) { @@ -114,13 +129,206 @@ func (cm *supermicroVendorConfig) Marshal() (string, error) { } } +func (cm *supermicroVendorConfig) Unmarshal(cfgData string) (err error) { + // the xml exported by sum is ISO-8859-1 encoded + decoder := xml.NewDecoder(bytes.NewReader([]byte(cfgData))) + // convert characters from non-UTF-8 to UTF-8 + decoder.CharsetReader = charset.NewReaderLabel + + return decoder.Decode(cm.ConfigData.BiosCfg) +} + +func (cm *supermicroVendorConfig) StandardConfig() (biosConfig map[string]string, err error) { + biosConfig = make(map[string]string) + + for _, menu := range cm.ConfigData.BiosCfg.Menus { + for _, s := range menu.Settings { + switch s.Name { + // We want to drop this list of settings + case "NewSetupPassword", "NewSysPassword", "OldSetupPassword", "OldSysPassword": + // All others get normalized + default: + var k, v string + k, v, err = normalizeSetting(s) + + if err != nil { + return + } + + biosConfig[k] = v + } + } + } + + return biosConfig, err +} + +func normalizeSetting(s *supermicroBiosCfgSetting) (k, v string, err error) { + switch s.Type { + case "CheckBox": + k = normalizeName(s.Name) + v = normalizeValue(k, s.CheckedStatus) + case "Option": + k = normalizeName(s.Name) + v = normalizeValue(k, s.SelectedOption) + case "Password": + k = normalizeName(s.Name) + v = "" + case "Numeric": + k = normalizeName(s.Name) + v = normalizeValue(k, s.NumericValue) + default: + err = UnknownSettingType(s.Type) + return + } + + return +} + +func normalizeName(k string) string { + switch k { + case "CpuMinSevAsid": + return "amd_sev" + case "BootMode", "Boot mode select": + return "boot_mode" + case "IntelTxt": + return "intel_txt" + case "Software Guard Extensions (SGX)": + return "intel_sgx" + case "SecureBoot", "Secure Boot": + return "secure_boot" + case "Hyper-Threading", "Hyper-Threading [ALL]", "LogicalProc": + return "smt" + case "SriovGlobalEnable": + return "sr_iov" + case "TpmSecurity", "Security Device Support": + return "tpm" + default: + // When we don't normalize the key prepend "raw:" + return "raw:" + k + } +} + +func normalizeBootMode(v string) string { + switch strings.ToLower(v) { + case "legacy": + return "BIOS" + default: + return strings.ToUpper(v) + } +} + +func normalizeValue(k, v string) string { + if k == "boot_mode" { + return normalizeBootMode(v) + } + + switch strings.ToLower(v) { + case "disable": + return disabledValue + case "disabled": + return disabledValue + case "enable": + return enabledValue + case "enabled": + return enabledValue + case "off": + return disabledValue + case "on": + return enabledValue + default: + return v + } +} + // Generic config options -func (cm *supermicroVendorConfig) EnableTPM() { - cm.Raw(" Security Device Support", "Enable", []string{"Trusted Computing"}) - cm.Raw(" SHA-1 PCR Bank", "Enabled", []string{"Trusted Computing"}) +func (cm *supermicroVendorConfig) BootMode(mode string) error { + switch strings.ToUpper(mode) { + case "LEGACY", "UEFI", "DUAL": + cm.Raw("Boot mode select", strings.ToUpper(mode), []string{"Boot"}) + default: + return InvalidBootModeOption(strings.ToUpper(mode)) + } + + return nil +} + +func (cm *supermicroVendorConfig) BootOrder(mode string) error { + // In a supermicro config there are 8 total legacy boot options and 9 UEFI boot options + // Since we primarily care about the first two boot options we explicitly define them + // and rely on the for loop to populate the remainder as Disabled. + switch strings.ToUpper(mode) { + case "LEGACY": + cm.Raw("Legacy Boot Option #1", "Hard Disk", []string{"Boot"}) + cm.Raw("Legacy Boot Option #2", "Network", []string{"Boot"}) + + for i := 3; i < 8; i++ { + cm.Raw("Legacy Boot Option #"+fmt.Sprint(i), "Disabled", []string{"Boot"}) + } + case "UEFI": + cm.Raw("UEFI Boot Option #1", "UEFI Hard Disk", []string{"Boot"}) + cm.Raw("UEFI Boot Option #2", "UEFI Network", []string{"Boot"}) + + for i := 3; i < 9; i++ { + cm.Raw("UEFI Boot Option #"+fmt.Sprint(i), "Disabled", []string{"Boot"}) + } + case "DUAL": + // TODO(jwb) Is this just both sets? + default: + return InvalidBootModeOption(strings.ToUpper(mode)) + } + + return nil +} + +func (cm *supermicroVendorConfig) IntelSGX(mode string) error { + switch mode { + case "Disabled", "Enabled", "Software Controlled": + // TODO(jwb) Path needs to be determined. + cm.Raw("Software Guard Extensions (SGX)", mode, []string{"Advanced", "PCIe/PCI/PnP Configuration"}) + default: + return InvalidSGXOption(mode) + } + + return nil +} + +func (cm *supermicroVendorConfig) SecureBoot(enable bool) error { + if !enable { + cm.Raw("Secure Boot", "Disabled", []string{"SMC Secure Boot Configuration"}) + } else { + cm.Raw("Secure Boot", "Enabled", []string{"SMC Secure Boot Configuration"}) + } + + return nil +} + +func (cm *supermicroVendorConfig) TPM(enable bool) error { + if enable { + // Note, this is actually 'Enable' not 'Enabled' like everything else. + cm.Raw(" Security Device Support", "Enable", []string{"Trusted Computing"}) + cm.Raw(" SHA-1 PCR Bank", "Enabled", []string{"Trusted Computing"}) + } else { + // Note, this is actually 'Disable' not 'Disabled' like everything else. + cm.Raw(" Security Device Support", "Disable", []string{"Trusted Computing"}) + cm.Raw(" SHA-1 PCR Bank", "Disabled", []string{"Trusted Computing"}) + } + + return nil +} + +func (cm *supermicroVendorConfig) SMT(enable bool) error { + if enable { + cm.Raw("Hyper-Threading", "Enabled", []string{"Advanced", "CPU Configuration"}) + } else { + cm.Raw("Hyper-Threading", "Disabled", []string{"Advanced", "CPU Configuration"}) + } + + return nil } -func (cm *supermicroVendorConfig) EnableSRIOV() { - cm.Raw("SR-IOV Support", "Enabled", []string{"Advanced", "PCIe/PCI/PnP Configuration"}) +func (cm *supermicroVendorConfig) SRIOV(enable bool) error { + // TODO(jwb) Need to figure out how we do this on platforms that support it... + return nil } diff --git a/go.mod b/go.mod index 385b3f2..a488a90 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module github.com/bmc-toolbox/common go 1.17 + +require golang.org/x/net v0.25.0 + +require golang.org/x/text v0.15.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7cb371d --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=