feat(parser): lenovo xcc vroc volume parsing - v1.2

Parse inventory_volume.log: Intel VROC (VMD) RAID volumes including
RAID level, capacity (GiB/TiB support added), status and member drives.
Add Drives []string to StorageVolume model.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 16:58:50 +03:00
parent 835df2676c
commit aba7a54990
11 changed files with 1147 additions and 2236 deletions
+150 -1
View File
@@ -9,6 +9,7 @@ package lenovo_xcc
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"time"
@@ -17,7 +18,7 @@ import (
"git.mchus.pro/mchus/logpile/internal/parser"
)
const parserVersion = "1.1"
const parserVersion = "1.2"
func init() {
parser.Register(&Parser{})
@@ -88,6 +89,9 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
if f := findByPath(files, "tmp/inventory_disk.log"); f != nil {
result.Hardware.Storage = parseDisks(f.Content)
}
if f := findByPath(files, "tmp/inventory_volume.log"); f != nil {
result.Hardware.Volumes = parseVolumes(f.Content)
}
if f := findByPath(files, "tmp/inventory_card.log"); f != nil {
result.Hardware.PCIeDevices = parseCards(f.Content)
}
@@ -103,6 +107,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
for _, f := range findEventFiles(files) {
result.Events = append(result.Events, parseEvents(f.Content)...)
}
applyDIMMWarningsFromEvents(result)
result.Protocol = "ipmi"
result.SourceType = models.SourceTypeArchive
@@ -297,6 +302,25 @@ type xccEventDoc struct {
Items []xccEvent `json:"items"`
}
type xccVolumeDoc struct {
Items []xccVolumeItem `json:"items"`
}
type xccVolumeItem struct {
Volumes []xccVolume `json:"volumes"`
TotalCapacityStr string `json:"totalCapacityStr"`
}
type xccVolume struct {
ID int `json:"id"`
Name string `json:"name"`
Drives string `json:"drives"` // e.g. "M.2 Drive 0, M.2 Drive 1"
RDLvlStr string `json:"rdlvlstr"` // e.g. "RAID 1"
CapacityStr string `json:"capacityStr"` // e.g. "893.750 GiB"
Status int `json:"status"`
StatusStr string `json:"statusStr"` // e.g. "Optimal"
}
type xccEvent struct {
Severity string `json:"severity"` // "I", "W", "E", "C"
Source string `json:"source"`
@@ -462,6 +486,37 @@ func parseDisks(content []byte) []models.Storage {
return out
}
func parseVolumes(content []byte) []models.StorageVolume {
var doc xccVolumeDoc
if err := json.Unmarshal(content, &doc); err != nil || len(doc.Items) == 0 {
return nil
}
var out []models.StorageVolume
for _, item := range doc.Items {
for _, v := range item.Volumes {
vol := models.StorageVolume{
ID: fmt.Sprintf("%d", v.ID),
Name: strings.TrimSpace(v.Name),
RAIDLevel: strings.TrimSpace(v.RDLvlStr),
SizeGB: parseCapacityToGB(v.CapacityStr),
Status: strings.TrimSpace(v.StatusStr),
}
drives := strings.TrimSpace(v.Drives)
if drives != "" {
for _, d := range strings.Split(drives, ",") {
vol.Drives = append(vol.Drives, strings.TrimSpace(d))
}
// M.2 NVMe volumes are managed by Intel VROC (VMD)
if strings.Contains(strings.ToLower(drives), "m.2") {
vol.Controller = "Intel VROC"
}
}
out = append(out, vol)
}
}
return out
}
func parseCards(content []byte) []models.PCIeDevice {
var doc xccCardDoc
if err := json.Unmarshal(content, &doc); err != nil {
@@ -613,6 +668,96 @@ func isUnqualifiedDIMM(value string) bool {
return strings.Contains(strings.ToLower(strings.TrimSpace(value)), "unqualified dimm")
}
var (
unqualifiedDIMMSlotRE = regexp.MustCompile(`(?i)\bunqualified dimm\s+(\d+)\b`)
unqualifiedDIMMSerialRE = regexp.MustCompile(`(?i)\bserial number is\s+([A-Z0-9-]+)`)
)
func applyDIMMWarningsFromEvents(result *models.AnalysisResult) {
if result == nil || result.Hardware == nil || len(result.Hardware.Memory) == 0 || len(result.Events) == 0 {
return
}
for _, ev := range result.Events {
if !isUnqualifiedDIMM(ev.Description) {
continue
}
idx := findDIMMIndexForUnqualifiedEvent(result.Hardware.Memory, ev.Description)
if idx < 0 {
continue
}
dimm := &result.Hardware.Memory[idx]
dimm.Status = "Warning"
dimm.ErrorDescription = ev.Description
if !ev.Timestamp.IsZero() {
ts := ev.Timestamp.UTC()
dimm.StatusChangedAt = &ts
dimm.StatusCheckedAt = &ts
}
appendDIMMStatusHistory(dimm, ev)
}
}
func findDIMMIndexForUnqualifiedEvent(memory []models.MemoryDIMM, description string) int {
slot := extractUnqualifiedDIMMSlot(description)
serial := normalizeUnqualifiedDIMMSerial(extractUnqualifiedDIMMSerial(description))
for i := range memory {
if slot != "" && strings.EqualFold(strings.TrimSpace(memory[i].Slot), slot) {
return i
}
}
for i := range memory {
if serial != "" && normalizeUnqualifiedDIMMSerial(memory[i].SerialNumber) == serial {
return i
}
}
return -1
}
func extractUnqualifiedDIMMSlot(description string) string {
m := unqualifiedDIMMSlotRE.FindStringSubmatch(description)
if len(m) < 2 {
return ""
}
return "DIMM " + strings.TrimSpace(m[1])
}
func extractUnqualifiedDIMMSerial(description string) string {
m := unqualifiedDIMMSerialRE.FindStringSubmatch(description)
if len(m) < 2 {
return ""
}
return strings.TrimSpace(m[1])
}
func normalizeUnqualifiedDIMMSerial(serial string) string {
serial = strings.ToUpper(strings.TrimSpace(serial))
if idx := strings.Index(serial, "-"); idx >= 0 {
serial = serial[:idx]
}
return serial
}
func appendDIMMStatusHistory(dimm *models.MemoryDIMM, ev models.Event) {
if dimm == nil || ev.Timestamp.IsZero() {
return
}
for _, item := range dimm.StatusHistory {
if strings.EqualFold(strings.TrimSpace(item.Status), "Warning") &&
item.ChangedAt.Equal(ev.Timestamp.UTC()) &&
strings.TrimSpace(item.Details) == strings.TrimSpace(ev.Description) {
return
}
}
dimm.StatusHistory = append(dimm.StatusHistory, models.StatusHistoryEntry{
Status: "Warning",
ChangedAt: ev.Timestamp.UTC(),
Details: ev.Description,
})
}
func parseXCCTime(s string) (time.Time, error) {
s = strings.TrimSpace(s)
formats := []string{
@@ -674,8 +819,12 @@ func parseCapacityToGB(s string) int {
switch strings.ToUpper(parts[1]) {
case "TB":
return int(v * 1000)
case "TIB":
return int(v * 1099.511627776) // 1 TiB = 1099.511... GB
case "GB":
return int(v)
case "GIB":
return int(v * 1.073741824) // 1 GiB = 1.073741824 GB
case "MB":
return int(v / 1024)
}