Add LOGPile BMC diagnostic log analyzer

Features:
- Modular parser architecture for vendor-specific formats
- Inspur/Kaytus parser supporting asset.json, devicefrusdr.log,
  component.log, idl.log, and syslog files
- PCI Vendor/Device ID lookup for hardware identification
- Web interface with tabs: Events, Sensors, Config, Serials, Firmware
- Server specification summary with component grouping
- Export to CSV, JSON, TXT formats
- BMC alarm parsing from IDL logs (memory errors, PSU events, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 04:11:23 +03:00
parent fb800216f1
commit 512957545a
29 changed files with 4086 additions and 1 deletions

160
internal/parser/archive.go Normal file
View File

@@ -0,0 +1,160 @@
package parser
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// ExtractedFile represents a file extracted from archive
type ExtractedFile struct {
Path string
Content []byte
}
// ExtractArchive extracts tar.gz or zip archive and returns file contents
func ExtractArchive(archivePath string) ([]ExtractedFile, error) {
ext := strings.ToLower(filepath.Ext(archivePath))
switch ext {
case ".gz", ".tgz":
return extractTarGz(archivePath)
case ".zip":
return extractZip(archivePath)
default:
return nil, fmt.Errorf("unsupported archive format: %s", ext)
}
}
// ExtractArchiveFromReader extracts archive from reader
func ExtractArchiveFromReader(r io.Reader, filename string) ([]ExtractedFile, error) {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".gz", ".tgz":
return extractTarGzFromReader(r)
default:
return nil, fmt.Errorf("unsupported archive format: %s", ext)
}
}
func extractTarGz(archivePath string) ([]ExtractedFile, error) {
f, err := os.Open(archivePath)
if err != nil {
return nil, fmt.Errorf("open archive: %w", err)
}
defer f.Close()
return extractTarGzFromReader(f)
}
func extractTarGzFromReader(r io.Reader) ([]ExtractedFile, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("gzip reader: %w", err)
}
defer gzr.Close()
tr := tar.NewReader(gzr)
var files []ExtractedFile
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("tar read: %w", err)
}
// Skip directories
if header.Typeflag == tar.TypeDir {
continue
}
// Skip large files (>10MB)
if header.Size > 10*1024*1024 {
continue
}
content, err := io.ReadAll(tr)
if err != nil {
return nil, fmt.Errorf("read file %s: %w", header.Name, err)
}
files = append(files, ExtractedFile{
Path: header.Name,
Content: content,
})
}
return files, nil
}
func extractZip(archivePath string) ([]ExtractedFile, error) {
r, err := zip.OpenReader(archivePath)
if err != nil {
return nil, fmt.Errorf("open zip: %w", err)
}
defer r.Close()
var files []ExtractedFile
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
// Skip large files (>10MB)
if f.FileInfo().Size() > 10*1024*1024 {
continue
}
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("open file %s: %w", f.Name, err)
}
content, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("read file %s: %w", f.Name, err)
}
files = append(files, ExtractedFile{
Path: f.Name,
Content: content,
})
}
return files, nil
}
// FindFileByPattern finds files matching pattern in extracted files
func FindFileByPattern(files []ExtractedFile, patterns ...string) []ExtractedFile {
var result []ExtractedFile
for _, f := range files {
for _, pattern := range patterns {
if strings.Contains(strings.ToLower(f.Path), strings.ToLower(pattern)) {
result = append(result, f)
break
}
}
}
return result
}
// FindFileByName finds file by exact name (case-insensitive)
func FindFileByName(files []ExtractedFile, name string) *ExtractedFile {
for _, f := range files {
if strings.EqualFold(filepath.Base(f.Path), name) {
return &f
}
}
return nil
}

View File

@@ -0,0 +1,30 @@
package parser
import (
"git.mchus.pro/mchus/logpile/internal/models"
)
// VendorParser interface for vendor-specific parsers
type VendorParser interface {
// Name returns human-readable parser name
Name() string
// Vendor returns vendor identifier (e.g., "inspur", "supermicro", "dell")
Vendor() string
// Detect checks if this parser can handle the given files
// Returns confidence score 0-100 (0 = cannot parse, 100 = definitely this format)
Detect(files []ExtractedFile) int
// Parse parses the extracted files and returns analysis result
Parse(files []ExtractedFile) (*models.AnalysisResult, error)
}
// FileParser interface for parsing specific file types within vendor module
type FileParser interface {
// CanParse checks if this parser can handle the file
CanParse(file ExtractedFile) bool
// Parse parses the file content
Parse(content []byte) error
}

86
internal/parser/parser.go Normal file
View File

@@ -0,0 +1,86 @@
package parser
import (
"fmt"
"io"
"git.mchus.pro/mchus/logpile/internal/models"
)
// BMCParser parses BMC diagnostic archives using vendor-specific parsers
type BMCParser struct {
result *models.AnalysisResult
files []ExtractedFile
vendorParser VendorParser
}
// NewBMCParser creates a new BMC parser
func NewBMCParser() *BMCParser {
return &BMCParser{
result: &models.AnalysisResult{
Events: make([]models.Event, 0),
FRU: make([]models.FRUInfo, 0),
Sensors: make([]models.SensorReading, 0),
},
}
}
// ParseArchive parses an archive file
func (p *BMCParser) ParseArchive(archivePath string) error {
files, err := ExtractArchive(archivePath)
if err != nil {
return fmt.Errorf("extract archive: %w", err)
}
p.files = files
return p.parseFiles()
}
// ParseFromReader parses archive from reader
func (p *BMCParser) ParseFromReader(r io.Reader, filename string) error {
files, err := ExtractArchiveFromReader(r, filename)
if err != nil {
return fmt.Errorf("extract archive: %w", err)
}
p.files = files
p.result.Filename = filename
return p.parseFiles()
}
func (p *BMCParser) parseFiles() error {
// Auto-detect format
vendorParser, err := DetectFormat(p.files)
if err != nil {
return fmt.Errorf("detect format: %w", err)
}
p.vendorParser = vendorParser
// Parse using detected vendor parser
result, err := vendorParser.Parse(p.files)
if err != nil {
return fmt.Errorf("parse with %s: %w", vendorParser.Name(), err)
}
// Preserve filename
result.Filename = p.result.Filename
p.result = result
return nil
}
// Result returns the analysis result
func (p *BMCParser) Result() *models.AnalysisResult {
return p.result
}
// GetFiles returns all extracted files
func (p *BMCParser) GetFiles() []ExtractedFile {
return p.files
}
// DetectedVendor returns the detected vendor parser name
func (p *BMCParser) DetectedVendor() string {
if p.vendorParser != nil {
return p.vendorParser.Name()
}
return "unknown"
}

106
internal/parser/registry.go Normal file
View File

@@ -0,0 +1,106 @@
package parser
import (
"fmt"
"sort"
"sync"
)
var (
registry = make(map[string]VendorParser)
registryLock sync.RWMutex
)
// Register adds a vendor parser to the registry
// Called from vendor module init() functions
func Register(p VendorParser) {
registryLock.Lock()
defer registryLock.Unlock()
vendor := p.Vendor()
if _, exists := registry[vendor]; exists {
panic(fmt.Sprintf("parser already registered for vendor: %s", vendor))
}
registry[vendor] = p
}
// GetParser returns parser for specific vendor
func GetParser(vendor string) (VendorParser, bool) {
registryLock.RLock()
defer registryLock.RUnlock()
p, ok := registry[vendor]
return p, ok
}
// ListParsers returns list of all registered vendor names
func ListParsers() []string {
registryLock.RLock()
defer registryLock.RUnlock()
vendors := make([]string, 0, len(registry))
for v := range registry {
vendors = append(vendors, v)
}
sort.Strings(vendors)
return vendors
}
// DetectResult holds detection result for a parser
type DetectResult struct {
Parser VendorParser
Confidence int
}
// DetectFormat tries to detect archive format and returns best matching parser
func DetectFormat(files []ExtractedFile) (VendorParser, error) {
registryLock.RLock()
defer registryLock.RUnlock()
var results []DetectResult
for _, p := range registry {
confidence := p.Detect(files)
if confidence > 0 {
results = append(results, DetectResult{
Parser: p,
Confidence: confidence,
})
}
}
if len(results) == 0 {
return nil, fmt.Errorf("no parser found for this archive format")
}
// Sort by confidence descending
sort.Slice(results, func(i, j int) bool {
return results[i].Confidence > results[j].Confidence
})
return results[0].Parser, nil
}
// DetectAllFormats returns all parsers that can handle the files with their confidence
func DetectAllFormats(files []ExtractedFile) []DetectResult {
registryLock.RLock()
defer registryLock.RUnlock()
var results []DetectResult
for _, p := range registry {
confidence := p.Detect(files)
if confidence > 0 {
results = append(results, DetectResult{
Parser: p,
Confidence: confidence,
})
}
}
sort.Slice(results, func(i, j int) bool {
return results[i].Confidence > results[j].Confidence
})
return results
}

96
internal/parser/vendors/README.md vendored Normal file
View File

@@ -0,0 +1,96 @@
# Vendor Parser Modules
Каждый производитель серверов имеет свой формат диагностических архивов BMC.
Эта директория содержит модули парсеров для разных производителей.
## Структура модуля
```
vendors/
├── vendors.go # Импорты всех модулей (добавьте сюда новый)
├── README.md # Эта документация
├── template/ # Шаблон для нового модуля
│ └── parser.go.template
├── inspur/ # Модуль Inspur/Kaytus
│ ├── parser.go # Основной парсер + регистрация
│ ├── sdr.go # Парсинг SDR (сенсоры)
│ ├── fru.go # Парсинг FRU (серийники)
│ ├── asset.go # Парсинг asset.json
│ └── syslog.go # Парсинг syslog
├── supermicro/ # Будущий модуль Supermicro
├── dell/ # Будущий модуль Dell iDRAC
└── hpe/ # Будущий модуль HPE iLO
```
## Как добавить новый модуль
### 1. Создайте директорию модуля
```bash
mkdir -p internal/parser/vendors/VENDORNAME
```
### 2. Скопируйте шаблон
```bash
cp internal/parser/vendors/template/parser.go.template \
internal/parser/vendors/VENDORNAME/parser.go
```
### 3. Отредактируйте parser.go
- Замените `VENDORNAME` на идентификатор вендора (например, `supermicro`)
- Замените `VENDOR_DESCRIPTION` на описание (например, `Supermicro`)
- Реализуйте метод `Detect()` для определения формата
- Реализуйте метод `Parse()` для парсинга данных
### 4. Зарегистрируйте модуль
Добавьте импорт в `vendors/vendors.go`:
```go
import (
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/inspur"
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/VENDORNAME" // Новый модуль
)
```
### 5. Готово!
Модуль автоматически зарегистрируется при старте приложения через `init()`.
## Интерфейс VendorParser
```go
type VendorParser interface {
// Name возвращает человекочитаемое имя парсера
Name() string
// Vendor возвращает идентификатор вендора
Vendor() string
// Detect проверяет, подходит ли этот парсер для файлов
// Возвращает уверенность 0-100 (0 = не подходит, 100 = точно этот формат)
Detect(files []ExtractedFile) int
// Parse парсит извлеченные файлы
Parse(files []ExtractedFile) (*models.AnalysisResult, error)
}
```
## Советы по реализации Detect()
- Ищите уникальные файлы/директории для данного вендора
- Проверяйте содержимое файлов на характерные маркеры
- Возвращайте высокий confidence (70+) только при уверенном совпадении
- Несколько парсеров могут вернуть >0, выбирается с максимальным confidence
## Поддерживаемые вендоры
| Вендор | Идентификатор | Статус | Протестировано на |
|--------|---------------|--------|-------------------|
| Inspur/Kaytus | `inspur` | ✅ Готов | KR4268X2 (onekeylog) |
| Supermicro | `supermicro` | ⏳ Планируется | - |
| Dell iDRAC | `dell` | ⏳ Планируется | - |
| HPE iLO | `hpe` | ⏳ Планируется | - |
| Lenovo XCC | `lenovo` | ⏳ Планируется | - |

352
internal/parser/vendors/inspur/asset.go vendored Normal file
View File

@@ -0,0 +1,352 @@
package inspur
import (
"encoding/json"
"fmt"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
)
// AssetJSON represents the structure of Inspur asset.json file
type AssetJSON struct {
VersionInfo []struct {
DeviceID int `json:"DeviceId"`
DeviceName string `json:"DeviceName"`
DeviceRevision string `json:"DeviceRevision"`
BuildTime string `json:"BuildTime"`
} `json:"VersionInfo"`
CpuInfo []struct {
ProcessorName string `json:"ProcessorName"`
ProcessorID string `json:"ProcessorId"`
MicroCodeVer string `json:"MicroCodeVer"`
CurrentSpeed int `json:"CurrentSpeed"`
Core int `json:"Core"`
ThreadCount int `json:"ThreadCount"`
L1Cache int `json:"L1Cache"`
L2Cache int `json:"L2Cache"`
L3Cache int `json:"L3Cache"`
CpuTdp int `json:"CpuTdp"`
PPIN string `json:"PPIN"`
TurboEnableMaxSpeed int `json:"TurboEnableMaxSpeed"`
TurboCloseMaxSpeed int `json:"TurboCloseMaxSpeed"`
UPIBandwidth string `json:"UPIBandwidth"`
} `json:"CpuInfo"`
MemInfo struct {
MemCommonInfo []struct {
Manufacturer string `json:"Manufacturer"`
MaxSpeed int `json:"MaxSpeed"`
CurrentSpeed int `json:"CurrentSpeed"`
MemoryType int `json:"MemoryType"`
Rank int `json:"Rank"`
DataWidth int `json:"DataWidth"`
ConfiguredVoltage int `json:"ConfiguredVoltage"`
PhysicalSize int `json:"PhysicalSize"`
} `json:"MemCommonInfo"`
DimmInfo []struct {
SerialNumber string `json:"SerialNumber"`
PartNumber string `json:"PartNumber"`
AssetTag string `json:"AssetTag"`
} `json:"DimmInfo"`
} `json:"MemInfo"`
HddInfo []struct {
SerialNumber string `json:"SerialNumber"`
Manufacturer string `json:"Manufacturer"`
ModelName string `json:"ModelName"`
FirmwareVersion string `json:"FirmwareVersion"`
Capacity int `json:"Capacity"`
Location int `json:"Location"`
DiskInterfaceType int `json:"DiskInterfaceType"`
MediaType int `json:"MediaType"`
LocationString string `json:"LocationString"`
BlockSizeBytes int `json:"BlockSizeBytes"`
CapableSpeedGbs string `json:"CapableSpeedGbs"`
NegotiatedSpeedGbs string `json:"NegotiatedSpeedGbs"`
PcieSlot int `json:"PcieSlot"`
} `json:"HddInfo"`
PcieInfo []struct {
VendorId int `json:"VendorId"`
DeviceId int `json:"DeviceId"`
BusNumber int `json:"BusNumber"`
DeviceNumber int `json:"DeviceNumber"`
FunctionNumber int `json:"FunctionNumber"`
MaxLinkWidth int `json:"MaxLinkWidth"`
MaxLinkSpeed int `json:"MaxLinkSpeed"`
NegotiatedLinkWidth int `json:"NegotiatedLinkWidth"`
CurrentLinkSpeed int `json:"CurrentLinkSpeed"`
ClassCode int `json:"ClassCode"`
SubClassCode int `json:"SubClassCode"`
PcieSlot int `json:"PcieSlot"`
LocString string `json:"LocString"`
PartNumber *string `json:"PartNumber"`
SerialNumber *string `json:"SerialNumber"`
Mac []string `json:"Mac"`
} `json:"PcieInfo"`
}
// ParseAssetJSON parses Inspur asset.json content
func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
var asset AssetJSON
if err := json.Unmarshal(content, &asset); err != nil {
return nil, err
}
config := &models.HardwareConfig{}
// Parse version info
for _, v := range asset.VersionInfo {
config.Firmware = append(config.Firmware, models.FirmwareInfo{
DeviceName: v.DeviceName,
Version: v.DeviceRevision,
BuildTime: v.BuildTime,
})
}
// Parse CPU info
for i, cpu := range asset.CpuInfo {
config.CPUs = append(config.CPUs, models.CPU{
Socket: i,
Model: strings.TrimSpace(cpu.ProcessorName),
Cores: cpu.Core,
Threads: cpu.ThreadCount,
FrequencyMHz: cpu.CurrentSpeed,
MaxFreqMHz: cpu.TurboEnableMaxSpeed,
L1CacheKB: cpu.L1Cache,
L2CacheKB: cpu.L2Cache,
L3CacheKB: cpu.L3Cache,
TDP: cpu.CpuTdp,
PPIN: cpu.PPIN,
})
}
// Parse memory info
if len(asset.MemInfo.MemCommonInfo) > 0 {
common := asset.MemInfo.MemCommonInfo[0]
for i, dimm := range asset.MemInfo.DimmInfo {
config.Memory = append(config.Memory, models.MemoryDIMM{
Slot: i,
SizeMB: common.PhysicalSize * 1024,
Type: memoryTypeToString(common.MemoryType),
SpeedMHz: common.CurrentSpeed,
Manufacturer: common.Manufacturer,
SerialNumber: dimm.SerialNumber,
PartNumber: strings.TrimSpace(dimm.PartNumber),
})
}
}
// Parse storage info
for _, hdd := range asset.HddInfo {
storageType := "HDD"
if hdd.DiskInterfaceType == 5 {
storageType = "NVMe"
} else if hdd.MediaType == 1 {
storageType = "SSD"
}
// Resolve manufacturer: try vendor ID first, then model name extraction
modelName := strings.TrimSpace(hdd.ModelName)
manufacturer := resolveManufacturer(hdd.Manufacturer, modelName)
config.Storage = append(config.Storage, models.Storage{
Slot: hdd.LocationString,
Type: storageType,
Model: modelName,
SizeGB: hdd.Capacity,
SerialNumber: hdd.SerialNumber,
Manufacturer: manufacturer,
Firmware: hdd.FirmwareVersion,
Interface: diskInterfaceToString(hdd.DiskInterfaceType),
})
}
// Parse PCIe info
for _, pcie := range asset.PcieInfo {
vendor, deviceName := pciids.DeviceInfo(pcie.VendorId, pcie.DeviceId)
device := models.PCIeDevice{
Slot: pcie.LocString,
VendorID: pcie.VendorId,
DeviceID: pcie.DeviceId,
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
LinkWidth: pcie.NegotiatedLinkWidth,
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
MaxLinkWidth: pcie.MaxLinkWidth,
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
Manufacturer: vendor,
}
if pcie.PartNumber != nil {
device.PartNumber = strings.TrimSpace(*pcie.PartNumber)
}
if pcie.SerialNumber != nil {
device.SerialNumber = strings.TrimSpace(*pcie.SerialNumber)
}
if len(pcie.Mac) > 0 {
device.MACAddresses = pcie.Mac
}
// Use device name from PCI IDs database if available
if deviceName != "" {
device.DeviceClass = deviceName
}
config.PCIeDevices = append(config.PCIeDevices, device)
}
return config, nil
}
func memoryTypeToString(memType int) string {
switch memType {
case 26:
return "DDR4"
case 34:
return "DDR5"
default:
return "Unknown"
}
}
func diskInterfaceToString(ifType int) string {
switch ifType {
case 4:
return "SATA"
case 5:
return "NVMe"
case 6:
return "SAS"
default:
return "Unknown"
}
}
func pcieLinkSpeedToString(speed int) string {
switch speed {
case 1:
return "2.5 GT/s"
case 2:
return "5.0 GT/s"
case 3:
return "8.0 GT/s"
case 4:
return "16.0 GT/s"
case 5:
return "32.0 GT/s"
default:
return "Unknown"
}
}
func pcieClassToString(classCode, subClass int) string {
switch classCode {
case 1:
switch subClass {
case 0:
return "SCSI"
case 1:
return "IDE"
case 4:
return "RAID"
case 6:
return "SATA"
case 7:
return "SAS"
case 8:
return "NVMe"
default:
return "Storage"
}
case 2:
return "Network"
case 3:
switch subClass {
case 0:
return "VGA"
case 2:
return "3D Controller"
default:
return "Display"
}
case 4:
return "Multimedia"
case 6:
return "Bridge"
case 12:
return "Serial Bus"
default:
return "Other"
}
}
func formatBDF(bus, dev, fun int) string {
return fmt.Sprintf("%02x:%02x.%x", bus, dev, fun)
}
// resolveManufacturer resolves manufacturer name from various sources
func resolveManufacturer(rawManufacturer, modelName string) string {
raw := strings.TrimSpace(rawManufacturer)
// If it looks like a vendor ID (hex), try to resolve it
if raw != "" {
if name := pciids.VendorNameFromString(raw); name != "" {
return name
}
// If not a vendor ID but looks like a real name (has letters), use it
hasLetter := false
for _, c := range raw {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
hasLetter = true
break
}
}
if hasLetter && len(raw) > 2 {
return raw
}
}
// Try to extract from model name
return extractStorageManufacturer(modelName)
}
// extractStorageManufacturer tries to extract manufacturer from model name
func extractStorageManufacturer(model string) string {
modelUpper := strings.ToUpper(model)
knownVendors := []struct {
prefix string
name string
}{
{"SAMSUNG", "Samsung"},
{"KIOXIA", "KIOXIA"},
{"TOSHIBA", "Toshiba"},
{"WDC", "Western Digital"},
{"WD", "Western Digital"},
{"SEAGATE", "Seagate"},
{"HGST", "HGST"},
{"INTEL", "Intel"},
{"MICRON", "Micron"},
{"KINGSTON", "Kingston"},
{"CRUCIAL", "Crucial"},
{"SK HYNIX", "SK Hynix"},
{"SKHYNIX", "SK Hynix"},
{"SANDISK", "SanDisk"},
{"LITEON", "Lite-On"},
{"PLEXTOR", "Plextor"},
{"ADATA", "ADATA"},
{"TRANSCEND", "Transcend"},
{"CORSAIR", "Corsair"},
{"SOLIDIGM", "Solidigm"},
}
for _, v := range knownVendors {
if strings.HasPrefix(modelUpper, v.prefix) {
return v.name
}
}
return ""
}

View File

@@ -0,0 +1,147 @@
package inspur
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
// ParseComponentLog parses component.log file and extracts PSU and other info
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
if hw == nil {
return
}
text := string(content)
// Parse RESTful PSU info
parsePSUInfo(text, hw)
}
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
func ParseComponentLogEvents(content []byte) []models.Event {
var events []models.Event
text := string(content)
// Parse RESTful Memory info for Warning/Error status
memEvents := parseMemoryEvents(text)
events = append(events, memEvents...)
return events
}
// PSUInfo represents the RESTful PSU info structure
type PSUInfo struct {
PowerSupplies []struct {
ID int `json:"id"`
Present int `json:"present"`
VendorID string `json:"vendor_id"`
Model string `json:"model"`
SerialNum string `json:"serial_num"`
FwVer string `json:"fw_ver"`
RatedPower int `json:"rated_power"`
Status string `json:"status"`
} `json:"power_supplies"`
}
func parsePSUInfo(text string, hw *models.HardwareConfig) {
// Find RESTful PSU info section
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*(?:RESTful|BMC|$)`)
match := re.FindStringSubmatch(text)
if match == nil {
return
}
jsonStr := match[1]
// Clean up the JSON (it might have newlines)
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var psuInfo PSUInfo
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
return
}
// Clear existing PSU data and populate with RESTful data
hw.PowerSupply = nil
for _, psu := range psuInfo.PowerSupplies {
if psu.Present != 1 {
continue
}
hw.PowerSupply = append(hw.PowerSupply, models.PSU{
Slot: formatPSUSlot(psu.ID),
Model: psu.Model,
WattageW: psu.RatedPower,
SerialNumber: psu.SerialNum,
Status: psu.Status,
})
}
}
func formatPSUSlot(id int) string {
return fmt.Sprintf("PSU%d", id)
}
// MemoryInfo represents the RESTful Memory info structure
type MemoryInfo struct {
MemModules []struct {
MemModID int `json:"mem_mod_id"`
MemModSlot string `json:"mem_mod_slot"`
MemModSize int `json:"mem_mod_size"`
MemModVendor string `json:"mem_mod_vendor"`
MemModPartNum string `json:"mem_mod_part_num"`
MemModSerial string `json:"mem_mod_serial_num"`
Status string `json:"status"`
} `json:"mem_modules"`
}
func parseMemoryEvents(text string) []models.Event {
var events []models.Event
// Find RESTful Memory info section
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
match := re.FindStringSubmatch(text)
if match == nil {
return events
}
jsonStr := match[1]
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
var memInfo MemoryInfo
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
return events
}
// Generate events for memory modules with Warning or Error status
for _, mem := range memInfo.MemModules {
if mem.Status == "Warning" || mem.Status == "Error" || mem.Status == "Critical" {
severity := models.SeverityWarning
if mem.Status == "Error" || mem.Status == "Critical" {
severity = models.SeverityCritical
}
description := fmt.Sprintf("Memory module %s: %s", mem.MemModSlot, mem.Status)
if mem.MemModSize == 0 {
description = fmt.Sprintf("Memory module %s not detected (capacity 0GB)", mem.MemModSlot)
}
events = append(events, models.Event{
ID: fmt.Sprintf("mem_%d", mem.MemModID),
Timestamp: time.Now(), // No timestamp in source, use current
Source: "Memory",
SensorType: "memory",
SensorName: mem.MemModSlot,
EventType: "Memory Status",
Severity: severity,
Description: description,
RawData: fmt.Sprintf("Slot: %s, Vendor: %s, P/N: %s, S/N: %s", mem.MemModSlot, mem.MemModVendor, mem.MemModPartNum, mem.MemModSerial),
})
}
}
return events
}

97
internal/parser/vendors/inspur/fru.go vendored Normal file
View File

@@ -0,0 +1,97 @@
package inspur
import (
"bufio"
"regexp"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
)
var (
fruDeviceRegex = regexp.MustCompile(`^FRU Device Description\s*:\s*(.+)$`)
fruFieldRegex = regexp.MustCompile(`^\s+(.+?)\s*:\s*(.*)$`)
)
// ParseFRU parses BMC FRU (Field Replaceable Unit) output
func ParseFRU(content []byte) []models.FRUInfo {
var fruList []models.FRUInfo
var current *models.FRUInfo
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := scanner.Text()
// Check for new FRU device
if matches := fruDeviceRegex.FindStringSubmatch(line); matches != nil {
if current != nil && current.Description != "" {
fruList = append(fruList, *current)
}
current = &models.FRUInfo{
Description: strings.TrimSpace(matches[1]),
}
continue
}
// Skip if no current FRU device
if current == nil {
continue
}
// Skip "Device not present" entries
if strings.Contains(line, "Device not present") {
current = nil
continue
}
// Parse FRU fields
if matches := fruFieldRegex.FindStringSubmatch(line); matches != nil {
fieldName := strings.TrimSpace(matches[1])
fieldValue := strings.TrimSpace(matches[2])
switch fieldName {
case "Chassis Type":
current.ChassisType = fieldValue
case "Chassis Part Number":
if fieldValue != "0" {
current.PartNumber = fieldValue
}
case "Chassis Serial":
if fieldValue != "0" {
current.SerialNumber = fieldValue
}
case "Board Mfg Date":
current.MfgDate = fieldValue
case "Board Mfg", "Product Manufacturer":
if fieldValue != "NULL" {
current.Manufacturer = fieldValue
}
case "Board Product", "Product Name":
if fieldValue != "NULL" {
current.ProductName = fieldValue
}
case "Board Serial", "Product Serial":
current.SerialNumber = fieldValue
case "Board Part Number", "Product Part Number":
if fieldValue != "0" {
current.PartNumber = fieldValue
}
case "Product Version":
if fieldValue != "0" {
current.Version = fieldValue
}
case "Product Asset Tag":
if fieldValue != "NULL" {
current.AssetTag = fieldValue
}
}
}
}
// Don't forget the last one
if current != nil && current.Description != "" {
fruList = append(fruList, *current)
}
return fruList
}

123
internal/parser/vendors/inspur/idl.go vendored Normal file
View File

@@ -0,0 +1,123 @@
package inspur
import (
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
// ParseIDLLog parses the IDL (Inspur Diagnostic Log) file for BMC alarms
// Format: |timestamp|component|type|severity|eventID|description|
func ParseIDLLog(content []byte) []models.Event {
var events []models.Event
// Pattern to match CommerDiagnose log entries
// Example: |2025-12-02T17:54:27+08:00|MEMORY|Assert|Warning|0C180401|CPU1_C4D0 Memory Device Disabled...|
re := regexp.MustCompile(`\|(\d{4}-\d{2}-\d{2}T[\d:]+[+-]\d{2}:\d{2})\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|`)
lines := strings.Split(string(content), "\n")
seenEvents := make(map[string]bool) // Deduplicate events
for _, line := range lines {
if !strings.Contains(line, "CommerDiagnose") {
continue
}
matches := re.FindStringSubmatch(line)
if matches == nil {
continue
}
timestamp := matches[1]
component := matches[2]
eventType := matches[3]
severityStr := matches[4]
eventID := matches[5]
description := matches[6]
// Parse timestamp
ts, err := time.Parse("2006-01-02T15:04:05-07:00", timestamp)
if err != nil {
ts = time.Now()
}
// Map severity
severity := mapIDLSeverity(severityStr)
// Clean up description
description = cleanDescription(description)
// Create unique key for deduplication
eventKey := eventID + "|" + description
if seenEvents[eventKey] {
continue
}
seenEvents[eventKey] = true
// Extract sensor name from description if available
sensorName := extractSensorName(description, component)
events = append(events, models.Event{
ID: eventID,
Timestamp: ts,
Source: component,
SensorType: strings.ToLower(component),
SensorName: sensorName,
EventType: eventType,
Severity: severity,
Description: description,
})
}
return events
}
func mapIDLSeverity(s string) models.Severity {
switch strings.ToLower(s) {
case "critical", "error":
return models.SeverityCritical
case "warning":
return models.SeverityWarning
default:
return models.SeverityInfo
}
}
func cleanDescription(desc string) string {
// Remove trailing " - Assert" or similar
desc = strings.TrimSuffix(desc, " - Assert")
desc = strings.TrimSuffix(desc, " - Deassert")
desc = strings.TrimSpace(desc)
return desc
}
func extractSensorName(desc, component string) string {
// Try to extract sensor/device name from description
// For memory: CPU1_C4D0, CPU1_C4D1, etc.
if component == "MEMORY" {
re := regexp.MustCompile(`(CPU\d+_C\d+D\d+)`)
if matches := re.FindStringSubmatch(desc); matches != nil {
return matches[1]
}
}
// For PSU: PSU0, PSU1, etc.
if component == "PSU" || component == "POWER" {
re := regexp.MustCompile(`(PSU\d+)`)
if matches := re.FindStringSubmatch(desc); matches != nil {
return matches[1]
}
}
// For temperature sensors
if component == "TEMPERATURE" || component == "THERMAL" {
re := regexp.MustCompile(`(\w+_Temp|\w+_DTS)`)
if matches := re.FindStringSubmatch(desc); matches != nil {
return matches[1]
}
}
return component
}

143
internal/parser/vendors/inspur/parser.go vendored Normal file
View File

@@ -0,0 +1,143 @@
// Package inspur provides parser for Inspur/Kaytus BMC diagnostic archives
// Tested with: Kaytus KR4268X2 (onekeylog format)
package inspur
import (
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
)
func init() {
parser.Register(&Parser{})
}
// Parser implements VendorParser for Inspur/Kaytus servers
type Parser struct{}
// Name returns human-readable parser name
func (p *Parser) Name() string {
return "Inspur/Kaytus BMC Parser"
}
// Vendor returns vendor identifier
func (p *Parser) Vendor() string {
return "inspur"
}
// Detect checks if archive matches Inspur/Kaytus format
// Returns confidence 0-100
func (p *Parser) Detect(files []parser.ExtractedFile) int {
confidence := 0
for _, f := range files {
path := strings.ToLower(f.Path)
// Strong indicators for Inspur/Kaytus onekeylog format
if strings.Contains(path, "onekeylog/") {
confidence += 30
}
if strings.Contains(path, "devicefrusdr.log") {
confidence += 25
}
if strings.Contains(path, "component/component.log") {
confidence += 15
}
// Check for asset.json with Inspur-specific structure
if strings.HasSuffix(path, "asset.json") {
if containsInspurMarkers(f.Content) {
confidence += 20
}
}
// Cap at 100
if confidence >= 100 {
return 100
}
}
return confidence
}
// containsInspurMarkers checks if content has Inspur-specific markers
func containsInspurMarkers(content []byte) bool {
s := string(content)
// Check for typical Inspur asset.json structure
return strings.Contains(s, "VersionInfo") &&
strings.Contains(s, "CpuInfo") &&
strings.Contains(s, "MemInfo")
}
// Parse parses Inspur/Kaytus archive
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
result := &models.AnalysisResult{
Events: make([]models.Event, 0),
FRU: make([]models.FRUInfo, 0),
Sensors: make([]models.SensorReading, 0),
}
// Parse devicefrusdr.log (contains SDR and FRU data)
if f := parser.FindFileByName(files, "devicefrusdr.log"); f != nil {
p.parseDeviceFruSDR(f.Content, result)
}
// Parse asset.json
if f := parser.FindFileByName(files, "asset.json"); f != nil {
if hw, err := ParseAssetJSON(f.Content); err == nil {
result.Hardware = hw
}
}
// Parse component.log for additional data (PSU, etc.)
if f := parser.FindFileByName(files, "component.log"); f != nil {
if result.Hardware == nil {
result.Hardware = &models.HardwareConfig{}
}
ParseComponentLog(f.Content, result.Hardware)
// Extract events from component.log (memory errors, etc.)
componentEvents := ParseComponentLogEvents(f.Content)
result.Events = append(result.Events, componentEvents...)
}
// Parse IDL log (BMC alarms/diagnose events)
if f := parser.FindFileByName(files, "idl.log"); f != nil {
idlEvents := ParseIDLLog(f.Content)
result.Events = append(result.Events, idlEvents...)
}
// Parse syslog files
syslogFiles := parser.FindFileByPattern(files, "syslog/alert", "syslog/warning", "syslog/notice", "syslog/info")
for _, f := range syslogFiles {
events := ParseSyslog(f.Content, f.Path)
result.Events = append(result.Events, events...)
}
return result, nil
}
func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult) {
lines := string(content)
// Find SDR section
sdrStart := strings.Index(lines, "BMC sdr Info:")
fruStart := strings.Index(lines, "BMC fru Info:")
if sdrStart != -1 {
var sdrContent string
if fruStart != -1 && fruStart > sdrStart {
sdrContent = lines[sdrStart:fruStart]
} else {
sdrContent = lines[sdrStart:]
}
result.Sensors = ParseSDR([]byte(sdrContent))
}
// Find FRU section
if fruStart != -1 {
fruContent := lines[fruStart:]
result.FRU = ParseFRU([]byte(fruContent))
}
}

89
internal/parser/vendors/inspur/sdr.go vendored Normal file
View File

@@ -0,0 +1,89 @@
package inspur
import (
"bufio"
"regexp"
"strconv"
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
)
// SDR sensor reading patterns
var (
sdrLineRegex = regexp.MustCompile(`^(\S+)\s+\|\s+(.+?)\s+\|\s+(\w+)$`)
valueRegex = regexp.MustCompile(`^([\d.]+)\s+(.+)$`)
)
// ParseSDR parses BMC SDR (Sensor Data Record) output
func ParseSDR(content []byte) []models.SensorReading {
var readings []models.SensorReading
scanner := bufio.NewScanner(strings.NewReader(string(content)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "BMC sdr Info:") {
continue
}
matches := sdrLineRegex.FindStringSubmatch(line)
if matches == nil {
continue
}
name := strings.TrimSpace(matches[1])
valueStr := strings.TrimSpace(matches[2])
status := strings.TrimSpace(matches[3])
reading := models.SensorReading{
Name: name,
Status: status,
}
// Parse value and unit
if valueStr != "disabled" && valueStr != "no reading" && !strings.HasPrefix(valueStr, "0x") {
if vm := valueRegex.FindStringSubmatch(valueStr); vm != nil {
if v, err := strconv.ParseFloat(vm[1], 64); err == nil {
reading.Value = v
reading.Unit = strings.TrimSpace(vm[2])
}
}
} else if strings.HasPrefix(valueStr, "0x") {
reading.RawValue = valueStr
}
// Determine sensor type
reading.Type = determineSensorType(name)
readings = append(readings, reading)
}
return readings
}
func determineSensorType(name string) string {
nameLower := strings.ToLower(name)
switch {
case strings.Contains(nameLower, "temp"):
return "temperature"
case strings.Contains(nameLower, "fan") && strings.Contains(nameLower, "speed"):
return "fan_speed"
case strings.Contains(nameLower, "fan") && strings.Contains(nameLower, "status"):
return "fan_status"
case strings.HasSuffix(nameLower, "_vin") || strings.HasSuffix(nameLower, "_vout") ||
strings.HasSuffix(nameLower, "_v") || strings.Contains(nameLower, "volt"):
return "voltage"
case strings.Contains(nameLower, "power") || strings.HasSuffix(nameLower, "_pin") ||
strings.HasSuffix(nameLower, "_pout") || strings.HasSuffix(nameLower, "_pwr"):
return "power"
case strings.Contains(nameLower, "psu") && strings.Contains(nameLower, "status"):
return "psu_status"
case strings.Contains(nameLower, "cpu") && strings.Contains(nameLower, "status"):
return "cpu_status"
case strings.Contains(nameLower, "hdd") || strings.Contains(nameLower, "nvme"):
return "storage_status"
default:
return "other"
}
}

View File

@@ -0,0 +1,97 @@
package inspur
import (
"bufio"
"regexp"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
)
var (
// Syslog format: <priority> timestamp hostname process: message
syslogRegex = regexp.MustCompile(`^<(\d+)>\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*)\s+(\S+)\s+(\S+):\s*(.*)$`)
)
// ParseSyslog parses syslog format logs
func ParseSyslog(content []byte, sourcePath string) []models.Event {
var events []models.Event
// Determine severity from file path
severity := determineSeverityFromPath(sourcePath)
scanner := bufio.NewScanner(strings.NewReader(string(content)))
lineNum := 0
for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
matches := syslogRegex.FindStringSubmatch(line)
if matches == nil {
continue
}
timestamp, err := time.Parse(time.RFC3339, matches[2])
if err != nil {
// Try alternative format
timestamp, err = time.Parse("2006-01-02T15:04:05.000000-07:00", matches[2])
if err != nil {
continue
}
}
event := models.Event{
ID: generateEventID(sourcePath, lineNum),
Timestamp: timestamp,
Source: matches[4],
SensorType: "syslog",
SensorName: matches[3],
Description: matches[5],
Severity: severity,
RawData: line,
}
events = append(events, event)
}
return events
}
func determineSeverityFromPath(path string) models.Severity {
pathLower := strings.ToLower(path)
switch {
case strings.Contains(pathLower, "emerg") || strings.Contains(pathLower, "alert") ||
strings.Contains(pathLower, "crit"):
return models.SeverityCritical
case strings.Contains(pathLower, "warn") || strings.Contains(pathLower, "error"):
return models.SeverityWarning
default:
return models.SeverityInfo
}
}
func generateEventID(source string, lineNum int) string {
parts := strings.Split(source, "/")
filename := parts[len(parts)-1]
return strings.TrimSuffix(filename, ".log") + "_" + itoa(lineNum)
}
func itoa(i int) string {
if i == 0 {
return "0"
}
var b [20]byte
pos := len(b)
for i > 0 {
pos--
b[pos] = byte('0' + i%10)
i /= 10
}
return string(b[pos:])
}

177
internal/parser/vendors/pciids/pciids.go vendored Normal file
View File

@@ -0,0 +1,177 @@
package pciids
import (
"fmt"
"strings"
)
// VendorName returns vendor name by PCI Vendor ID
func VendorName(vendorID int) string {
if name, ok := vendors[vendorID]; ok {
return name
}
return ""
}
// DeviceName returns device name by Vendor ID and Device ID
func DeviceName(vendorID, deviceID int) string {
key := fmt.Sprintf("%04x:%04x", vendorID, deviceID)
if name, ok := devices[key]; ok {
return name
}
return ""
}
// DeviceInfo returns both vendor and device name
func DeviceInfo(vendorID, deviceID int) (vendor, device string) {
vendor = VendorName(vendorID)
device = DeviceName(vendorID, deviceID)
return
}
// VendorNameFromString tries to parse vendor ID from string (hex) and return name
func VendorNameFromString(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
// Try to parse as hex (with or without 0x prefix)
s = strings.TrimPrefix(strings.ToLower(s), "0x")
var id int
for _, c := range s {
if c >= '0' && c <= '9' {
id = id*16 + int(c-'0')
} else if c >= 'a' && c <= 'f' {
id = id*16 + int(c-'a'+10)
} else {
// Not a valid hex string, return original
return ""
}
}
return VendorName(id)
}
// Common PCI Vendor IDs
// Source: https://pci-ids.ucw.cz/
var vendors = map[int]string{
// Storage controllers and SSDs
0x1E0F: "KIOXIA",
0x144D: "Samsung Electronics",
0x1C5C: "SK Hynix",
0x15B7: "SanDisk (Western Digital)",
0x1179: "Toshiba",
0x8086: "Intel",
0x1344: "Micron Technology",
0x126F: "Silicon Motion",
0x1987: "Phison Electronics",
0x1CC1: "ADATA Technology",
0x2646: "Kingston Technology",
0x1E95: "Solid State Storage Technology",
0x025E: "Solidigm",
0x1D97: "Shenzhen Longsys Electronics",
0x1E4B: "MAXIO Technology",
// Network adapters
0x15B3: "Mellanox Technologies",
0x14E4: "Broadcom",
0x10EC: "Realtek Semiconductor",
0x1077: "QLogic",
0x19A2: "Emulex",
0x1137: "Cisco Systems",
0x1924: "Solarflare Communications",
0x177D: "Cavium",
0x1D6A: "Aquantia",
0x1FC9: "Tehuti Networks",
0x18D4: "Chelsio Communications",
// GPU / Graphics
0x10DE: "NVIDIA",
0x1002: "AMD/ATI",
0x102B: "Matrox Electronics",
0x1A03: "ASPEED Technology",
// Storage controllers (RAID/HBA)
0x1000: "LSI Logic / Broadcom",
0x9005: "Adaptec / Microsemi",
0x1028: "Dell",
0x103C: "Hewlett-Packard",
0x17D3: "Areca Technology",
0x1CC4: "Union Memory",
// Server vendors
0x1014: "IBM",
0x15D9: "Supermicro",
0x8088: "Inspur",
// Other common
0x1022: "AMD",
0x1106: "VIA Technologies",
0x10B5: "PLX Technology",
0x1B21: "ASMedia Technology",
0x1B4B: "Marvell Technology",
0x197B: "JMicron Technology",
}
// Device IDs (vendor:device -> name)
var devices = map[string]string{
// NVIDIA GPUs (0x10DE)
"10de:26b9": "L40S 48GB",
"10de:26b1": "L40 48GB",
"10de:2684": "RTX 4090",
"10de:2704": "RTX 4080",
"10de:2782": "RTX 4070 Ti",
"10de:2786": "RTX 4070",
"10de:27b8": "RTX 4060 Ti",
"10de:2882": "RTX 4060",
"10de:2204": "RTX 3090",
"10de:2208": "RTX 3080 Ti",
"10de:2206": "RTX 3080",
"10de:2484": "RTX 3070",
"10de:2503": "RTX 3060",
"10de:20b0": "A100 80GB",
"10de:20b2": "A100 40GB",
"10de:20f1": "A10",
"10de:2236": "A10G",
"10de:25b6": "A16",
"10de:20b5": "A30",
"10de:20b7": "A30X",
"10de:1db4": "V100 32GB",
"10de:1db1": "V100 16GB",
"10de:1e04": "RTX 2080 Ti",
"10de:1e07": "RTX 2080",
"10de:1f02": "RTX 2070",
"10de:26ba": "L40S-PCIE-48G",
"10de:2330": "H100 80GB PCIe",
"10de:2331": "H100 80GB SXM5",
"10de:2322": "H100 NVL",
"10de:2324": "H200",
// AMD GPUs (0x1002)
"1002:744c": "Instinct MI250X",
"1002:7408": "Instinct MI100",
"1002:73a5": "RX 6950 XT",
"1002:73bf": "RX 6900 XT",
"1002:73df": "RX 6700 XT",
"1002:7480": "RX 7900 XTX",
"1002:7483": "RX 7900 XT",
// ASPEED (0x1A03) - BMC VGA
"1a03:2000": "AST2500 VGA",
"1a03:1150": "AST2600 VGA",
// Intel GPUs
"8086:56c0": "Data Center GPU Flex 170",
"8086:56c1": "Data Center GPU Flex 140",
// Mellanox/NVIDIA NICs (0x15B3)
"15b3:1017": "ConnectX-5 100GbE",
"15b3:1019": "ConnectX-5 Ex",
"15b3:101b": "ConnectX-6",
"15b3:101d": "ConnectX-6 Dx",
"15b3:101f": "ConnectX-6 Lx",
"15b3:1021": "ConnectX-7",
"15b3:a2d6": "ConnectX-4 Lx",
}

View File

@@ -0,0 +1,70 @@
// Package VENDORNAME provides parser for VENDOR_DESCRIPTION BMC diagnostic archives
// Copy this template to create a new vendor parser module
package VENDORNAME
import (
"strings"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
)
func init() {
parser.Register(&Parser{})
}
// Parser implements VendorParser for VENDOR_DESCRIPTION servers
type Parser struct{}
// Name returns human-readable parser name
func (p *Parser) Name() string {
return "VENDOR_DESCRIPTION BMC Parser"
}
// Vendor returns vendor identifier
func (p *Parser) Vendor() string {
return "VENDORNAME"
}
// Detect checks if archive matches this vendor's format
// Returns confidence 0-100
func (p *Parser) Detect(files []parser.ExtractedFile) int {
confidence := 0
for _, f := range files {
path := strings.ToLower(f.Path)
// Add detection logic here
// Example:
// if strings.Contains(path, "unique_vendor_file.log") {
// confidence += 50
// }
_ = path
}
// Cap at 100
if confidence > 100 {
return 100
}
return confidence
}
// Parse parses the archive using vendor-specific logic
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
result := &models.AnalysisResult{
Events: make([]models.Event, 0),
FRU: make([]models.FRUInfo, 0),
Sensors: make([]models.SensorReading, 0),
}
// Add parsing logic here
// Example:
// if f := parser.FindFileByName(files, "sensor_data.log"); f != nil {
// result.Sensors = parseSensorLog(f.Content)
// }
return result, nil
}
// Add helper functions for parsing specific file formats below

14
internal/parser/vendors/vendors.go vendored Normal file
View File

@@ -0,0 +1,14 @@
// Package vendors imports all vendor parser modules
// Add new vendor imports here to register them
package vendors
import (
// Import vendor modules to trigger their init() registration
_ "git.mchus.pro/mchus/logpile/internal/parser/vendors/inspur"
// Future vendors:
// _ "git.mchus.pro/mchus/logpile/internal/parser/vendors/supermicro"
// _ "git.mchus.pro/mchus/logpile/internal/parser/vendors/dell"
// _ "git.mchus.pro/mchus/logpile/internal/parser/vendors/hpe"
// _ "git.mchus.pro/mchus/logpile/internal/parser/vendors/lenovo"
)