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 0x00 0x30 // Response: ... (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: ... func huaweiParseStringResponse(hexOut, special string) string { parts := strings.Fields(hexOut) if len(parts) < 2 { return "" } if special == "chassis-type" { // Response: 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 }