Replaces separate IPMI FRU and SAA DMI cards with a single FRU / Elabel card that reads all available sources in parallel and shows each field with a color-coded source chip (IPMI FRU / Huawei iBMC / SAA DMI). Huawei elabel fields are read/written via OEM IPMI raw commands (NetFn 0x30, cmd 0x90) with 19-byte chunking protocol, matching the FusionServer ElabelTool V511 wire format. Covers DeviceName, DeviceSerialNumber, ProductName, ProductSerialNumber, ProductAssetTag, ProductManufacturer, MainboardManufacturer, BoardProductName, ChassisPartnumber, ChassisType (read-only), IOChassisSerial, IOChassisAssetTag, and GUID (read-only via standard 0x06 0x08). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
281 lines
8.3 KiB
Go
281 lines
8.3 KiB
Go
package webui
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type huaweiField struct {
|
|
Name string `json:"name"`
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
ReadOnly bool `json:"read_only,omitempty"`
|
|
}
|
|
|
|
type huaweiChange struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type huaweiFieldDef struct {
|
|
Name string
|
|
Key string
|
|
FruID byte
|
|
TypeID byte
|
|
FieldID byte
|
|
Special string // "chassis-type" | "guid"
|
|
}
|
|
|
|
var huaweiElabelDefs = []huaweiFieldDef{
|
|
{"Device Name", "DeviceName", 0x00, 0x06, 0x01, ""},
|
|
{"Device Serial Number", "DeviceSerialNumber", 0x00, 0x06, 0x03, ""},
|
|
{"Product Name", "ProductName", 0x00, 0x03, 0x01, ""},
|
|
{"Product Serial Number", "ProductSerialNumber", 0x00, 0x03, 0x04, ""},
|
|
{"Product Asset Tag", "ProductAssetTag", 0x00, 0x03, 0x05, ""},
|
|
{"Product Manufacturer", "ProductManufacturer", 0x00, 0x03, 0x00, ""},
|
|
{"Mainboard Manufacturer", "MainboardManufacturer", 0x00, 0x02, 0x01, ""},
|
|
{"Board Product Name", "BoardProductName", 0x00, 0x02, 0x02, ""},
|
|
{"Chassis Part Number", "ChassisPartnumber", 0x00, 0x01, 0x01, ""},
|
|
{"Chassis Type", "ChassisType", 0x00, 0x01, 0x00, "chassis-type"},
|
|
{"IO Chassis Serial", "IOChassisSerialNumber", 0x01, 0x03, 0x04, ""},
|
|
{"IO Chassis Asset Tag", "IOChassisAssetTag", 0x01, 0x03, 0x05, ""},
|
|
{"GUID", "GUID", 0x00, 0x00, 0x00, "guid"},
|
|
}
|
|
|
|
// huaweiGetRaw reads a string elabel field via OEM IPMI raw command.
|
|
// Protocol: ipmitool raw 0x30 0x90 0x05 <fru_id> <type_id> <field_id> 0x00 0x30
|
|
// Response: <length_byte> <ascii_byte1> ... (null-terminated)
|
|
func huaweiGetRaw(ctx context.Context, def huaweiFieldDef) (string, error) {
|
|
if def.Special == "guid" {
|
|
return huaweiGetGUID(ctx)
|
|
}
|
|
args := []string{
|
|
"0x30", "0x90", "0x05",
|
|
fmt.Sprintf("0x%02x", def.FruID),
|
|
fmt.Sprintf("0x%02x", def.TypeID),
|
|
fmt.Sprintf("0x%02x", def.FieldID),
|
|
"0x00", "0x30",
|
|
}
|
|
out, err := exec.CommandContext(ctx, "ipmitool", append([]string{"raw"}, args...)...).CombinedOutput()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return huaweiParseStringResponse(strings.TrimSpace(string(out)), def.Special), nil
|
|
}
|
|
|
|
// huaweiParseStringResponse decodes the OEM IPMI response bytes to a string.
|
|
// Format: <length_byte> <byte1> <byte2> ...
|
|
func huaweiParseStringResponse(hexOut, special string) string {
|
|
parts := strings.Fields(hexOut)
|
|
if len(parts) < 2 {
|
|
return ""
|
|
}
|
|
if special == "chassis-type" {
|
|
// Response: <length=1> <type_byte>
|
|
if len(parts) >= 2 {
|
|
n, err := strconv.ParseUint(parts[1], 16, 8)
|
|
if err == nil {
|
|
return fmt.Sprintf("0x%02x", n)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
var sb strings.Builder
|
|
for _, p := range parts[1:] {
|
|
b, err := strconv.ParseUint(p, 16, 8)
|
|
if err != nil || b == 0 {
|
|
break
|
|
}
|
|
sb.WriteByte(byte(b))
|
|
}
|
|
return strings.TrimRight(sb.String(), "\x00")
|
|
}
|
|
|
|
// huaweiGetGUID reads the system GUID via standard IPMI Get System GUID (0x06 0x08).
|
|
func huaweiGetGUID(ctx context.Context) (string, error) {
|
|
out, err := exec.CommandContext(ctx, "ipmitool", "raw", "0x06", "0x08").CombinedOutput()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parts := strings.Fields(strings.TrimSpace(string(out)))
|
|
if len(parts) != 16 {
|
|
return "", nil
|
|
}
|
|
// Format as UUID: 4-2-2-2-6 byte groups
|
|
// iBMC returns bytes in reversed order; re-reverse to get canonical UUID.
|
|
var bytes [16]string
|
|
for i, p := range parts {
|
|
bytes[15-i] = p
|
|
}
|
|
return fmt.Sprintf("%s%s%s%s-%s%s-%s%s-%s%s-%s%s%s%s%s%s",
|
|
bytes[0], bytes[1], bytes[2], bytes[3],
|
|
bytes[4], bytes[5],
|
|
bytes[6], bytes[7],
|
|
bytes[8], bytes[9],
|
|
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
|
|
), nil
|
|
}
|
|
|
|
// huaweiChunks splits a value into 19-byte chunks for the OEM IPMI SET protocol.
|
|
// Key byte: bit7=1 means more chunks follow; bits 0-6 = offset into string.
|
|
func huaweiChunks(value string) [][]string {
|
|
if len(value) == 0 {
|
|
return [][]string{{"0x00", "0x01", "0x00"}}
|
|
}
|
|
const maxLen = 63
|
|
if len(value) > maxLen {
|
|
value = value[:maxLen]
|
|
}
|
|
const chunkSize = 19
|
|
var chunks [][]string
|
|
for offset := 0; offset < len(value); {
|
|
end := offset + chunkSize
|
|
if end > len(value) {
|
|
end = len(value)
|
|
}
|
|
isLast := end >= len(value)
|
|
key := byte(offset)
|
|
if !isLast {
|
|
key |= 0x80
|
|
}
|
|
args := []string{
|
|
fmt.Sprintf("0x%02x", key),
|
|
fmt.Sprintf("0x%02x", end-offset),
|
|
}
|
|
for _, b := range []byte(value[offset:end]) {
|
|
args = append(args, fmt.Sprintf("0x%02x", b))
|
|
}
|
|
chunks = append(chunks, args)
|
|
offset = end
|
|
}
|
|
return chunks
|
|
}
|
|
|
|
func (h *handler) handleAPIHuaweiElabelRead(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
var fields []huaweiField
|
|
for _, def := range huaweiElabelDefs {
|
|
val, err := huaweiGetRaw(ctx, def)
|
|
if err != nil {
|
|
// First field failure likely means no Huawei BMC — abort with error.
|
|
if len(fields) == 0 {
|
|
msg := strings.TrimSpace(err.Error())
|
|
writeError(w, http.StatusInternalServerError, "huawei elabel not available: "+msg)
|
|
return
|
|
}
|
|
val = ""
|
|
}
|
|
fields = append(fields, huaweiField{
|
|
Name: def.Name,
|
|
Key: def.Key,
|
|
Value: val,
|
|
ReadOnly: def.Special == "guid" || def.Special == "chassis-type",
|
|
})
|
|
}
|
|
writeJSON(w, fields)
|
|
}
|
|
|
|
func (h *handler) handleAPIHuaweiElabelWrite(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Changes []huaweiChange `json:"changes"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid JSON")
|
|
return
|
|
}
|
|
if len(req.Changes) == 0 {
|
|
writeError(w, http.StatusUnprocessableEntity, "no changes provided")
|
|
return
|
|
}
|
|
|
|
defByKey := make(map[string]huaweiFieldDef, len(huaweiElabelDefs))
|
|
for _, d := range huaweiElabelDefs {
|
|
defByKey[d.Key] = d
|
|
}
|
|
|
|
for _, c := range req.Changes {
|
|
def, ok := defByKey[c.Key]
|
|
if !ok {
|
|
writeError(w, http.StatusUnprocessableEntity, "unknown field key: "+c.Key)
|
|
return
|
|
}
|
|
if def.Special == "guid" || def.Special == "chassis-type" {
|
|
writeError(w, http.StatusUnprocessableEntity, "field is read-only: "+c.Key)
|
|
return
|
|
}
|
|
if len(c.Value) > 63 {
|
|
writeError(w, http.StatusUnprocessableEntity, "value too long (max 63 chars): "+c.Key)
|
|
return
|
|
}
|
|
for _, ch := range c.Value {
|
|
if ch < 0x20 || ch > 0x7E {
|
|
writeError(w, http.StatusUnprocessableEntity, "non-printable character in value for: "+c.Key)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
t := &Task{
|
|
ID: newJobID("huawei-elabel-write"),
|
|
Name: fmt.Sprintf("Huawei Elabel Write (%d field(s))", len(req.Changes)),
|
|
Target: "huawei-elabel-write",
|
|
Priority: defaultTaskPriority("huawei-elabel-write", taskParams{}),
|
|
Status: TaskPending,
|
|
CreatedAt: time.Now(),
|
|
params: taskParams{HuaweiElabelChanges: req.Changes},
|
|
}
|
|
globalQueue.enqueue(t)
|
|
writeJSON(w, map[string]string{"task_id": t.ID})
|
|
}
|
|
|
|
func runHuaweiElabelWriteTask(ctx context.Context, j *jobState, p taskParams) error {
|
|
defByKey := make(map[string]huaweiFieldDef, len(huaweiElabelDefs))
|
|
for _, d := range huaweiElabelDefs {
|
|
defByKey[d.Key] = d
|
|
}
|
|
|
|
// Enable device name effective flag before writing.
|
|
enableCmd := exec.CommandContext(ctx, "ipmitool", "raw", "0x30", "0x90", "0x21", "0x04", "0x01")
|
|
if out, err := enableCmd.CombinedOutput(); err != nil {
|
|
j.append("Warning: enable flag: " + strings.TrimSpace(string(out)))
|
|
}
|
|
|
|
for _, c := range p.HuaweiElabelChanges {
|
|
def := defByKey[c.Key]
|
|
setPrefix := []string{
|
|
"0x30", "0x90", "0x04",
|
|
fmt.Sprintf("0x%02x", def.FruID),
|
|
fmt.Sprintf("0x%02x", def.TypeID),
|
|
fmt.Sprintf("0x%02x", def.FieldID),
|
|
}
|
|
|
|
chunks := huaweiChunks(c.Value)
|
|
j.append(fmt.Sprintf("Setting %s = %q (%d chunk(s))", c.Key, c.Value, len(chunks)))
|
|
|
|
for _, chunk := range chunks {
|
|
args := append([]string{"raw"}, setPrefix...)
|
|
args = append(args, chunk...)
|
|
cmd := exec.CommandContext(ctx, "ipmitool", args...)
|
|
if err := streamCmdJob(j, cmd); err != nil {
|
|
return fmt.Errorf("set %s: %w", c.Key, err)
|
|
}
|
|
}
|
|
|
|
// Commit after each field.
|
|
commitCmd := exec.CommandContext(ctx, "ipmitool", "raw", "0x30", "0x90", "0x06", "0x00", "0xAA")
|
|
if out, err := commitCmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("commit after %s: %w (output: %s)", c.Key, err, strings.TrimSpace(string(out)))
|
|
}
|
|
j.append("Committed " + c.Key)
|
|
}
|
|
return nil
|
|
}
|