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:
160
internal/parser/archive.go
Normal file
160
internal/parser/archive.go
Normal 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
|
||||
}
|
||||
30
internal/parser/interface.go
Normal file
30
internal/parser/interface.go
Normal 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
86
internal/parser/parser.go
Normal 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
106
internal/parser/registry.go
Normal 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
96
internal/parser/vendors/README.md
vendored
Normal 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
352
internal/parser/vendors/inspur/asset.go
vendored
Normal 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 ""
|
||||
}
|
||||
147
internal/parser/vendors/inspur/component.go
vendored
Normal file
147
internal/parser/vendors/inspur/component.go
vendored
Normal 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
97
internal/parser/vendors/inspur/fru.go
vendored
Normal 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
123
internal/parser/vendors/inspur/idl.go
vendored
Normal 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
143
internal/parser/vendors/inspur/parser.go
vendored
Normal 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
89
internal/parser/vendors/inspur/sdr.go
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
97
internal/parser/vendors/inspur/syslog.go
vendored
Normal file
97
internal/parser/vendors/inspur/syslog.go
vendored
Normal 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
177
internal/parser/vendors/pciids/pciids.go
vendored
Normal 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",
|
||||
}
|
||||
70
internal/parser/vendors/template/parser.go.template
vendored
Normal file
70
internal/parser/vendors/template/parser.go.template
vendored
Normal 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
14
internal/parser/vendors/vendors.go
vendored
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user