v1.3.0: Add multiple vendor parsers and enhanced hardware detection
New parsers: - NVIDIA Field Diagnostics parser with dmidecode output support - NVIDIA Bug Report parser with comprehensive hardware extraction - Supermicro crashdump (CDump.txt) parser - Generic fallback parser for unrecognized text files Enhanced GPU parsing (nvidia-bug-report): - Model and manufacturer detection (NVIDIA H100 80GB HBM3) - UUID, Video BIOS version, IRQ information - Bus location (BDF), DMA size/mask, device minor - PCIe bus type details New hardware detection (nvidia-bug-report): - System Information: server S/N, UUID, manufacturer, product name - CPU: model, S/N, cores, threads, frequencies from dmidecode - Memory: P/N, S/N, manufacturer, speed for all DIMMs - Power Supplies: manufacturer, model, S/N, wattage, status - Network Adapters: Ethernet/InfiniBand controllers with VPD data - Model, P/N, S/N from lspci Vital Product Data - Port count/type detection (QSFP56, OSFP, etc.) - Support for ConnectX-6/7 adapters Archive handling improvements: - Plain .gz file support (not just tar.gz) - Increased size limit for plain gzip files (50MB) - Better error handling for mixed archive formats Web interface enhancements: - Display parser name and filename badges - Improved file info section with visual indicators Co-Authored-By: Claude (qwen3-coder:480b) <noreply@anthropic.com>
This commit is contained in:
275
internal/parser/vendors/nvidia_bug_report/README.md
vendored
Normal file
275
internal/parser/vendors/nvidia_bug_report/README.md
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
# NVIDIA Bug Report Parser
|
||||
|
||||
Парсер для файлов nvidia-bug-report, генерируемых скриптом `nvidia-bug-report.sh`.
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот парсер обрабатывает диагностические логи NVIDIA драйверов и извлекает:
|
||||
- Информацию о модулях памяти (из dmidecode)
|
||||
- Информацию о GPU устройствах
|
||||
- Версию NVIDIA драйвера
|
||||
|
||||
## Формат файла
|
||||
|
||||
- Имя файла: `nvidia-bug-report-*.log.gz`
|
||||
- Формат: Gzip-сжатый текстовый файл
|
||||
- Генерируется: `nvidia-bug-report.sh` скриптом
|
||||
|
||||
## Confidence Score
|
||||
|
||||
**85** - высокий приоритет для файлов nvidia-bug-report
|
||||
|
||||
## Извлекаемые данные
|
||||
|
||||
### 1. System Information (из dmidecode)
|
||||
|
||||
Информация о сервере:
|
||||
- **Serial Number**: Серийный номер сервера (например, 2KD501412)
|
||||
- **UUID**: Уникальный идентификатор системы (например, 2e4054bc-1dd2-11b2-0284-6b0a21737950)
|
||||
- **Manufacturer**: Производитель сервера
|
||||
- **Product Name**: Модель сервера
|
||||
- **Version**: Версия системы
|
||||
|
||||
### 2. CPU Information (из dmidecode)
|
||||
|
||||
Для каждого процессора извлекается:
|
||||
- **Model**: Модель процессора (например, Intel(R) Xeon(R) Platinum 8480+)
|
||||
- **Serial Number**: Серийный номер (например, 5DB0D6C0DD30ABD8)
|
||||
- **Core Count**: Количество ядер (например, 56)
|
||||
- **Thread Count**: Количество потоков (например, 112)
|
||||
- **Max Speed**: Максимальная частота (например, 3800 MHz)
|
||||
- **Current Speed**: Текущая частота (например, 2000 MHz)
|
||||
|
||||
Пример:
|
||||
```
|
||||
Socket 0: Intel(R) Xeon(R) Platinum 8480+
|
||||
Serial Number: 5DB0D6C0DD30ABD8
|
||||
Cores: 56, Threads: 112
|
||||
Frequency: 2000 MHz (Max: 3800 MHz)
|
||||
```
|
||||
|
||||
### 3. Memory Modules (из dmidecode)
|
||||
|
||||
Для каждого модуля памяти извлекается:
|
||||
- **Slot/Location**: Например, CPU0_C0D0
|
||||
- **Size**: Размер в GB (например, 64 GB)
|
||||
- **Type**: Тип памяти (DDR5, DDR4, etc.)
|
||||
- **Manufacturer**: Производитель (Hynix, Samsung, Micron, etc.)
|
||||
- **Part Number**: P/N модуля (например, HMCG94AGBRA179N)
|
||||
- **Serial Number**: S/N модуля (например, 80AD0224322B3834E6)
|
||||
- **Speed**: Max/Current скорость (например, 5600/4400 MHz)
|
||||
- **Ranks**: Количество рангов
|
||||
|
||||
Пример:
|
||||
```
|
||||
Slot: CPU0_C0D0
|
||||
Size: 64 GB
|
||||
Type: DDR5
|
||||
Manufacturer: Hynix
|
||||
Part Number: HMCG94AGBRA179N
|
||||
Serial Number: 80AD0224322B3834E6
|
||||
Speed: 5600 MT/s (configured: 4400 MT/s)
|
||||
Ranks: 2
|
||||
```
|
||||
|
||||
### 4. Power Supplies (из dmidecode)
|
||||
|
||||
Для каждого блока питания извлекается:
|
||||
- **Location**: Позиция (например, PSU0, PSU1)
|
||||
- **Manufacturer**: Производитель (например, DELTA, Great Wall)
|
||||
- **Model Part Number**: Модель БП (например, V0310DT000000000)
|
||||
- **Serial Number**: Серийный номер (например, DGPLV251500LZ)
|
||||
- **Max Power Capacity**: Максимальная мощность (например, 2700 W)
|
||||
- **Revision**: Версия прошивки (например, 00.01.04)
|
||||
- **Status**: Статус (например, Present, OK)
|
||||
|
||||
Пример:
|
||||
```
|
||||
PSU0: V0310DT000000000 (DELTA)
|
||||
Serial Number: DGPLV251500LZ
|
||||
Power: 2700 W, Revision: 00.01.04
|
||||
Status: Present, OK
|
||||
```
|
||||
|
||||
### 5. Network Adapters (из lspci)
|
||||
|
||||
Для каждого сетевого адаптера (Ethernet, Network, InfiniBand) извлекается:
|
||||
- **Model**: Полное название модели из VPD (например, "NVIDIA ConnectX-7 HHHL Adapter card, 400GbE / NDR IB (default mode), Single-port OSFP, PCIe 5.0 x16")
|
||||
- **Location**: PCI BDF адрес (например, 0000:0e:00.0)
|
||||
- **Slot**: Физический слот (например, 108)
|
||||
- **Part Number**: P/N адаптера (например, MCX75310AAS-NEAT)
|
||||
- **Serial Number**: S/N адаптера (например, MT2430600249)
|
||||
- **Vendor**: Производитель (Mellanox, NVIDIA)
|
||||
- **Vendor ID / Device ID**: PCI идентификаторы (например, 15b3:1021)
|
||||
- **Port Count**: Количество портов (определяется из модели: Dual-port = 2, Single-port = 1)
|
||||
- **Port Type**: Тип портов (QSFP56, OSFP, SFP+)
|
||||
|
||||
Пример:
|
||||
```
|
||||
0000:0e:00.0: NVIDIA ConnectX-7 HHHL Adapter card, 400GbE / NDR IB (default mode), Single-port OSFP
|
||||
Slot: 108
|
||||
P/N: MCX75310AAS-NEAT
|
||||
S/N: MT2430600249
|
||||
Ports: 1 x OSFP
|
||||
```
|
||||
|
||||
### 6. GPU Devices
|
||||
|
||||
Для каждого GPU извлекается:
|
||||
- **Model**: Модель GPU (например, NVIDIA H100 80GB HBM3)
|
||||
- **BDF (Bus:Device.Function)**: PCI адрес (например, 0000:0f:00.0)
|
||||
- **UUID**: Уникальный идентификатор GPU (например, GPU-64674e47-e036-c12a-3e8d-55a2a9ac8db3)
|
||||
- **Video BIOS**: Версия BIOS видеокарты (например, 96.00.99.00.01)
|
||||
- **IRQ**: Прерывание (например, 17)
|
||||
- **Bus Type**: Тип шины (PCIe)
|
||||
- **DMA Size**: Размер DMA (например, 52 bits)
|
||||
- **DMA Mask**: Маска DMA (например, 0xfffffffffffff)
|
||||
- **Device Minor**: Номер устройства (например, 0)
|
||||
- **Manufacturer**: NVIDIA
|
||||
|
||||
Пример:
|
||||
```
|
||||
0000:0f:00.0: NVIDIA H100 80GB HBM3
|
||||
UUID: GPU-64674e47-e036-c12a-3e8d-55a2a9ac8db3
|
||||
Video BIOS: 96.00.99.00.01
|
||||
IRQ: 17
|
||||
```
|
||||
|
||||
### 7. Events
|
||||
|
||||
- **Memory Configuration**: Сводка по модулям памяти (количество, производители, общий размер)
|
||||
- **GPU Detection**: Обнаруженные GPU устройства
|
||||
- **Driver Version**: Версия NVIDIA драйвера
|
||||
|
||||
## Пример использования
|
||||
|
||||
```bash
|
||||
# Запуск с nvidia-bug-report файлом
|
||||
./logpile --file nvidia-bug-report-2KD501412.log.gz
|
||||
|
||||
# Веб-интерфейс будет доступен на http://localhost:8082
|
||||
```
|
||||
|
||||
## Пример вывода
|
||||
|
||||
```
|
||||
✓ Detected vendor: NVIDIA Bug Report Parser
|
||||
✓ CPUs: 2
|
||||
✓ Memory: 32 modules
|
||||
✓ Power Supplies: 8
|
||||
✓ GPUs: 8
|
||||
✓ Network Adapters: 12
|
||||
|
||||
System Information:
|
||||
Serial Number: 2KD501412
|
||||
UUID: 2e4054bc-1dd2-11b2-0284-6b0a21737950
|
||||
Version: 0
|
||||
|
||||
CPU Information:
|
||||
Socket 0: Intel(R) Xeon(R) Platinum 8480+
|
||||
S/N: 5DB0D6C0DD30ABD8, Cores: 56, Threads: 112
|
||||
Socket 1: Intel(R) Xeon(R) Platinum 8480+
|
||||
S/N: 5DB017C05685B3ED, Cores: 56, Threads: 112
|
||||
|
||||
Power Supplies:
|
||||
PSU0: V0310DT000000000 (DELTA)
|
||||
S/N: DGPLV251500LZ
|
||||
Power: 2700 W, Revision: 00.01.04
|
||||
Status: Present, OK
|
||||
PSU1: V0310DT000000000 (DELTA)
|
||||
S/N: DGPLV251500GY
|
||||
Power: 2700 W, Revision: 00.01.04
|
||||
Status: Present, OK
|
||||
[... 6 more PSUs ...]
|
||||
|
||||
Memory Modules:
|
||||
CPU0_C0D0: 64 GB, Hynix
|
||||
P/N: HMCG94AGBRA179N, S/N: 80AD0224322B3834E6
|
||||
Type: DDR5, Speed: 4400/5600 MHz
|
||||
[... 31 more modules ...]
|
||||
|
||||
Network Adapters: 12 devices
|
||||
0000:0e:00.0: NVIDIA ConnectX-7 HHHL Adapter card, 400GbE / NDR IB (default mode), Single-port OSFP
|
||||
Slot: 108
|
||||
P/N: MCX75310AAS-NEAT
|
||||
S/N: MT2430600249
|
||||
Ports: 1 x OSFP
|
||||
0000:1f:00.0: ConnectX-6 Dx EN adapter card, 100GbE, Dual-port QSFP56
|
||||
Slot: 12
|
||||
P/N: MCX623106AN-CDAT
|
||||
S/N: MT2434J00PCD
|
||||
Ports: 2 x QSFP56
|
||||
[... 10 more adapters ...]
|
||||
|
||||
GPUs: 8 devices
|
||||
0000:0f:00.0: NVIDIA H100 80GB HBM3
|
||||
UUID: GPU-64674e47-e036-c12a-3e8d-55a2a9ac8db3
|
||||
Video BIOS: 96.00.99.00.01
|
||||
IRQ: 17
|
||||
0000:34:00.0: NVIDIA H100 80GB HBM3
|
||||
UUID: GPU-fa796345-c23a-54aa-1b67-709ac2542852
|
||||
Video BIOS: 96.00.99.00.01
|
||||
IRQ: 16
|
||||
[... 6 more GPUs ...]
|
||||
```
|
||||
|
||||
## Версионирование
|
||||
|
||||
**Текущая версия парсера:** 1.0.0
|
||||
|
||||
### История версий
|
||||
|
||||
- **1.0.0** - Первоначальная версия с парсингом System Info, CPU, Memory, PSU, GPU, Network Adapters и Driver
|
||||
|
||||
## Структура данных
|
||||
|
||||
Парсер использует следующие секции в bug report:
|
||||
1. **dmidecode output (System Information)** - для извлечения информации о сервере
|
||||
2. **dmidecode output (Processor Information)** - для извлечения информации о CPU
|
||||
3. **dmidecode output (Memory Device)** - для извлечения информации о памяти
|
||||
4. **dmidecode output (System Power Supply)** - для извлечения информации о блоках питания
|
||||
5. **lspci -vvv output (Ethernet/Network/Infiniband controller)** - для извлечения информации о сетевых адаптерах
|
||||
6. **lspci VPD (Vital Product Data)** - для извлечения P/N, S/N и модели сетевых адаптеров
|
||||
7. **/proc/driver/nvidia/gpus/.../information** - для детальной информации о GPU
|
||||
8. **NVRM version** - для версии драйвера
|
||||
|
||||
## Известные ограничения
|
||||
|
||||
1. Ошибки и предупреждения из логов пока не извлекаются
|
||||
2. Некоторые специфичные характеристики GPU (температура, утилизация) не парсятся
|
||||
3. Информация о производительности и метрики GPU требуют парсинга других секций
|
||||
|
||||
## Расширение
|
||||
|
||||
Для добавления новых возможностей:
|
||||
|
||||
1. **Ошибки драйвера**: Парсить секции с ошибками NVIDIA драйвера
|
||||
2. **nvidia-smi output**: Извлекать детальную информацию из вывода nvidia-smi (температура, утилизация)
|
||||
3. **GPU производительность**: Парсить метрики производительности и использования памяти GPU
|
||||
4. **PCIe информация**: Извлекать детали о PCIe конфигурации (скорость линка, ширина)
|
||||
|
||||
## Пример структуры файла
|
||||
|
||||
```
|
||||
Start of NVIDIA bug report log file
|
||||
nvidia-bug-report.sh Version: 34275561
|
||||
Date: Thu Jul 17 18:18:18 EDT 2025
|
||||
|
||||
[... system info ...]
|
||||
|
||||
Memory Device
|
||||
Data Width: 64 bits
|
||||
Size: 64 GB
|
||||
Form Factor: DIMM
|
||||
Locator: CPU0_C0D0
|
||||
Type: DDR5
|
||||
Speed: 5600 MT/s
|
||||
Manufacturer: Hynix
|
||||
Serial Number: 80AD0224322B3834E6
|
||||
Part Number: HMCG94AGBRA179N
|
||||
|
||||
[... more memory modules ...]
|
||||
|
||||
*** /proc/driver/nvidia/./gpus/0000:0f:00.0/power
|
||||
[... GPU info ...]
|
||||
```
|
||||
140
internal/parser/vendors/nvidia_bug_report/cpu.go
vendored
Normal file
140
internal/parser/vendors/nvidia_bug_report/cpu.go
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// parseCPUInfo extracts CPU information from dmidecode output
|
||||
func parseCPUInfo(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
|
||||
var currentCPU *models.CPU
|
||||
inProcessorInfo := false
|
||||
cpuSocket := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Start of Processor Information section
|
||||
if strings.Contains(trimmed, "Processor Information") {
|
||||
inProcessorInfo = true
|
||||
currentCPU = &models.CPU{
|
||||
Socket: cpuSocket,
|
||||
}
|
||||
cpuSocket++
|
||||
continue
|
||||
}
|
||||
|
||||
// End of current section (empty line or new section with Handle)
|
||||
if inProcessorInfo && (trimmed == "" || strings.HasPrefix(trimmed, "Handle ")) {
|
||||
// Save CPU if it has valid data
|
||||
if currentCPU != nil && currentCPU.Model != "" {
|
||||
result.Hardware.CPUs = append(result.Hardware.CPUs, *currentCPU)
|
||||
}
|
||||
inProcessorInfo = false
|
||||
currentCPU = nil
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse fields within Processor Information section
|
||||
if inProcessorInfo && currentCPU != nil && strings.Contains(line, ":") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if value == "" || value == "Not Specified" || value == "Unknown" || value == "UNKNOWN" || value == "<OUT OF SPEC>" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "Version":
|
||||
// CPU model name
|
||||
currentCPU.Model = value
|
||||
case "Serial Number":
|
||||
currentCPU.SerialNumber = value
|
||||
case "Part Number":
|
||||
// Store part number if available
|
||||
// Could be stored in a custom field if needed
|
||||
case "Core Count":
|
||||
if cores, err := strconv.Atoi(value); err == nil {
|
||||
currentCPU.Cores = cores
|
||||
}
|
||||
case "Core Enabled":
|
||||
// Could store this if needed
|
||||
case "Thread Count":
|
||||
if threads, err := strconv.Atoi(value); err == nil {
|
||||
currentCPU.Threads = threads
|
||||
}
|
||||
case "Max Speed":
|
||||
// Parse speed like "3800 MHz"
|
||||
if speed := parseCPUSpeed(value); speed > 0 {
|
||||
currentCPU.MaxFreqMHz = speed
|
||||
}
|
||||
case "Current Speed":
|
||||
// Parse current speed like "2000 MHz"
|
||||
if speed := parseCPUSpeed(value); speed > 0 {
|
||||
currentCPU.FrequencyMHz = speed
|
||||
}
|
||||
case "Voltage":
|
||||
// Could parse voltage if needed (e.g., "1.6 V")
|
||||
case "Status":
|
||||
// Status like "Populated, Enabled"
|
||||
// Check if CPU is enabled
|
||||
if !strings.Contains(value, "Populated") {
|
||||
// Skip unpopulated CPUs
|
||||
currentCPU = nil
|
||||
inProcessorInfo = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last CPU if exists
|
||||
if currentCPU != nil && currentCPU.Model != "" {
|
||||
result.Hardware.CPUs = append(result.Hardware.CPUs, *currentCPU)
|
||||
}
|
||||
}
|
||||
|
||||
// parseCPUSpeed parses CPU speed strings like "3800 MHz" or "2.0 GHz"
|
||||
func parseCPUSpeed(speedStr string) int {
|
||||
parts := strings.Fields(speedStr)
|
||||
if len(parts) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Try to parse the number (may be int or float)
|
||||
speedStr = parts[0]
|
||||
var speed float64
|
||||
var err error
|
||||
|
||||
if strings.Contains(speedStr, ".") {
|
||||
speed, err = strconv.ParseFloat(speedStr, 64)
|
||||
} else {
|
||||
var speedInt int
|
||||
speedInt, err = strconv.Atoi(speedStr)
|
||||
speed = float64(speedInt)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
unit := strings.ToUpper(parts[1])
|
||||
switch unit {
|
||||
case "MHZ":
|
||||
return int(speed)
|
||||
case "GHZ":
|
||||
return int(speed * 1000)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
170
internal/parser/vendors/nvidia_bug_report/gpu.go
vendored
Normal file
170
internal/parser/vendors/nvidia_bug_report/gpu.go
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// parseGPUInfo extracts GPU information from the bug report
|
||||
func parseGPUInfo(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
|
||||
var currentGPU *models.GPU
|
||||
inGPUInfo := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Look for GPU information section markers (but skip ls listings)
|
||||
if strings.Contains(line, "/proc/driver/nvidia") && strings.Contains(line, "/gpus/") &&
|
||||
strings.Contains(line, "/information") && !strings.Contains(line, "ls:") {
|
||||
// Extract PCI address
|
||||
re := regexp.MustCompile(`/gpus/([\da-f]{4}:[\da-f]{2}:[\da-f]{2}\.[\da-f])`)
|
||||
matches := re.FindStringSubmatch(line)
|
||||
if len(matches) > 1 {
|
||||
pciAddr := matches[1]
|
||||
|
||||
// Save previous GPU if exists
|
||||
if currentGPU != nil {
|
||||
result.Hardware.GPUs = append(result.Hardware.GPUs, *currentGPU)
|
||||
}
|
||||
|
||||
// Start new GPU entry
|
||||
currentGPU = &models.GPU{
|
||||
BDF: pciAddr,
|
||||
Manufacturer: "NVIDIA",
|
||||
}
|
||||
inGPUInfo = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// End of GPU info section (separator line or new section, but not ls lines)
|
||||
if inGPUInfo && (strings.HasPrefix(line, "___") || (strings.HasPrefix(line, "***") && !strings.Contains(line, "ls:"))) {
|
||||
inGPUInfo = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse GPU fields within information section
|
||||
if inGPUInfo && currentGPU != nil && strings.Contains(line, ":") {
|
||||
// Split on first colon and trim whitespace/tabs
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "Model":
|
||||
currentGPU.Model = value
|
||||
case "IRQ":
|
||||
if irq, err := strconv.Atoi(value); err == nil {
|
||||
currentGPU.IRQ = irq
|
||||
}
|
||||
case "GPU UUID":
|
||||
currentGPU.UUID = value
|
||||
case "Video BIOS":
|
||||
currentGPU.VideoBIOS = value
|
||||
case "Bus Type":
|
||||
currentGPU.BusType = value
|
||||
case "DMA Size":
|
||||
currentGPU.DMASize = value
|
||||
case "DMA Mask":
|
||||
currentGPU.DMAMask = value
|
||||
case "Bus Location":
|
||||
// BDF already set from path, but verify consistency
|
||||
if currentGPU.BDF != value {
|
||||
// Use the value from the information section as it's more explicit
|
||||
currentGPU.BDF = value
|
||||
}
|
||||
case "Device Minor":
|
||||
if minor, err := strconv.Atoi(value); err == nil {
|
||||
currentGPU.DeviceMinor = minor
|
||||
}
|
||||
case "GPU Excluded":
|
||||
// Store as status if "Yes"
|
||||
if strings.ToLower(value) == "yes" {
|
||||
currentGPU.Status = "Excluded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last GPU if exists
|
||||
if currentGPU != nil {
|
||||
result.Hardware.GPUs = append(result.Hardware.GPUs, *currentGPU)
|
||||
}
|
||||
|
||||
// Create event for GPU summary
|
||||
if len(result.Hardware.GPUs) > 0 {
|
||||
result.Events = append(result.Events, models.Event{
|
||||
Timestamp: time.Now(),
|
||||
Source: "NVIDIA Driver",
|
||||
EventType: "GPU Detection",
|
||||
Description: "NVIDIA GPUs detected",
|
||||
Severity: models.SeverityInfo,
|
||||
RawData: formatGPUSummary(result.Hardware.GPUs),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// parseDriverVersion extracts NVIDIA driver version
|
||||
func parseDriverVersion(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Look for NVRM version line
|
||||
if strings.Contains(line, "NVRM version:") {
|
||||
// Extract version info
|
||||
parts := strings.Split(line, "NVRM version:")
|
||||
if len(parts) > 1 {
|
||||
version := strings.TrimSpace(parts[1])
|
||||
|
||||
result.Events = append(result.Events, models.Event{
|
||||
Timestamp: time.Now(),
|
||||
Source: "NVIDIA Driver",
|
||||
EventType: "Driver Version",
|
||||
Description: "NVIDIA driver version detected",
|
||||
Severity: models.SeverityInfo,
|
||||
RawData: version,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formatGPUSummary creates a summary string for GPUs
|
||||
func formatGPUSummary(gpus []models.GPU) string {
|
||||
if len(gpus) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var summary strings.Builder
|
||||
for i, gpu := range gpus {
|
||||
if i > 0 {
|
||||
summary.WriteString("; ")
|
||||
}
|
||||
summary.WriteString(gpu.BDF)
|
||||
if gpu.Model != "" {
|
||||
summary.WriteString(" (")
|
||||
summary.WriteString(gpu.Model)
|
||||
summary.WriteString(")")
|
||||
}
|
||||
}
|
||||
|
||||
return summary.String()
|
||||
}
|
||||
183
internal/parser/vendors/nvidia_bug_report/memory.go
vendored
Normal file
183
internal/parser/vendors/nvidia_bug_report/memory.go
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// parseMemoryModules extracts memory module information from dmidecode output
|
||||
func parseMemoryModules(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
|
||||
var currentModule *models.MemoryDIMM
|
||||
inMemoryDevice := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Start of Memory Device section
|
||||
if strings.Contains(trimmed, "Memory Device") && !strings.Contains(trimmed, "Array") {
|
||||
inMemoryDevice = true
|
||||
currentModule = &models.MemoryDIMM{
|
||||
Present: true,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// End of current section (empty line or new section)
|
||||
if inMemoryDevice && (trimmed == "" || strings.HasPrefix(trimmed, "Handle ")) {
|
||||
// Save module if it has valid data
|
||||
if currentModule != nil && currentModule.Slot != "" && currentModule.SizeMB > 0 {
|
||||
result.Hardware.Memory = append(result.Hardware.Memory, *currentModule)
|
||||
}
|
||||
inMemoryDevice = false
|
||||
currentModule = nil
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse fields within Memory Device section
|
||||
if inMemoryDevice && currentModule != nil && strings.Contains(line, ":") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if value == "" || value == "Not Specified" || value == "Unknown" || value == "NO DIMM" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "Size":
|
||||
// Parse size like "64 GB" or "32768 MB"
|
||||
currentModule.SizeMB = parseMemorySize(value)
|
||||
case "Locator":
|
||||
currentModule.Slot = value
|
||||
currentModule.Location = value
|
||||
case "Bank Locator":
|
||||
// Store in location if slot is empty
|
||||
if currentModule.Location == "" {
|
||||
currentModule.Location = value
|
||||
}
|
||||
case "Type":
|
||||
currentModule.Type = value
|
||||
case "Type Detail":
|
||||
currentModule.Technology = value
|
||||
case "Speed":
|
||||
// Parse speed like "5600 MT/s"
|
||||
currentModule.MaxSpeedMHz = parseMemorySpeed(value)
|
||||
case "Configured Memory Speed":
|
||||
currentModule.CurrentSpeedMHz = parseMemorySpeed(value)
|
||||
case "Manufacturer":
|
||||
currentModule.Manufacturer = value
|
||||
case "Serial Number":
|
||||
currentModule.SerialNumber = value
|
||||
case "Part Number":
|
||||
currentModule.PartNumber = strings.TrimSpace(value)
|
||||
case "Rank":
|
||||
// Parse rank
|
||||
if rank, err := strconv.Atoi(value); err == nil {
|
||||
currentModule.Ranks = rank
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last module if exists
|
||||
if currentModule != nil && currentModule.Slot != "" && currentModule.SizeMB > 0 {
|
||||
result.Hardware.Memory = append(result.Hardware.Memory, *currentModule)
|
||||
}
|
||||
|
||||
// Create event for memory summary
|
||||
if len(result.Hardware.Memory) > 0 {
|
||||
totalMemoryGB := 0
|
||||
for _, mem := range result.Hardware.Memory {
|
||||
totalMemoryGB += mem.SizeMB / 1024
|
||||
}
|
||||
|
||||
result.Events = append(result.Events, models.Event{
|
||||
Timestamp: time.Now(),
|
||||
Source: "DMI",
|
||||
EventType: "Memory Configuration",
|
||||
Description: "Memory modules detected",
|
||||
Severity: models.SeverityInfo,
|
||||
RawData: formatMemorySummary(result.Hardware.Memory, totalMemoryGB),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// parseMemorySize parses memory size strings like "64 GB" or "32768 MB"
|
||||
func parseMemorySize(sizeStr string) int {
|
||||
parts := strings.Fields(sizeStr)
|
||||
if len(parts) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
size, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
unit := strings.ToUpper(parts[1])
|
||||
switch unit {
|
||||
case "GB":
|
||||
return size * 1024
|
||||
case "MB":
|
||||
return size
|
||||
case "TB":
|
||||
return size * 1024 * 1024
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// parseMemorySpeed parses speed strings like "5600 MT/s" or "4400 MHz"
|
||||
func parseMemorySpeed(speedStr string) int {
|
||||
parts := strings.Fields(speedStr)
|
||||
if len(parts) < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
speed, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return speed
|
||||
}
|
||||
|
||||
// formatMemorySummary creates a summary string for memory modules
|
||||
func formatMemorySummary(modules []models.MemoryDIMM, totalGB int) string {
|
||||
if len(modules) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Group by manufacturer
|
||||
manufacturerCount := make(map[string]int)
|
||||
for _, mem := range modules {
|
||||
if mem.Manufacturer != "" {
|
||||
manufacturerCount[mem.Manufacturer]++
|
||||
}
|
||||
}
|
||||
|
||||
summary := ""
|
||||
for mfr, count := range manufacturerCount {
|
||||
if summary != "" {
|
||||
summary += ", "
|
||||
}
|
||||
summary += mfr + ": " + strconv.Itoa(count) + " modules"
|
||||
}
|
||||
|
||||
if summary == "" {
|
||||
summary = strconv.Itoa(len(modules)) + " modules"
|
||||
}
|
||||
|
||||
return summary + ", Total: " + strconv.Itoa(totalGB) + " GB"
|
||||
}
|
||||
160
internal/parser/vendors/nvidia_bug_report/network_adapter.go
vendored
Normal file
160
internal/parser/vendors/nvidia_bug_report/network_adapter.go
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// parseNetworkAdapters extracts network adapter information from lspci output
|
||||
func parseNetworkAdapters(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
|
||||
var currentAdapter *models.NetworkAdapter
|
||||
inVPD := false
|
||||
currentBDF := ""
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Check if this is a new PCI device line
|
||||
re := regexp.MustCompile(`^([\da-f]{4}:[\da-f]{2}:[\da-f]{2}\.[\da-f])\s+`)
|
||||
matches := re.FindStringSubmatch(line)
|
||||
|
||||
if len(matches) > 0 {
|
||||
// Save previous adapter if exists before processing new device
|
||||
if currentAdapter != nil && currentAdapter.Model != "" {
|
||||
result.Hardware.NetworkAdapters = append(result.Hardware.NetworkAdapters, *currentAdapter)
|
||||
}
|
||||
currentAdapter = nil
|
||||
inVPD = false
|
||||
}
|
||||
|
||||
// Match PCI device line: "0000:1f:00.0 Ethernet controller [0200]: Mellanox Technologies..."
|
||||
if strings.Contains(line, "Ethernet controller") || strings.Contains(line, "Network controller") || strings.Contains(line, "Infiniband controller") {
|
||||
// Extract BDF (Bus:Device.Function)
|
||||
if len(matches) > 1 {
|
||||
currentBDF = matches[1]
|
||||
currentAdapter = &models.NetworkAdapter{
|
||||
Location: currentBDF,
|
||||
Present: true,
|
||||
}
|
||||
|
||||
// Extract vendor and device info
|
||||
// Format: "Vendor description [DeviceClass]: Vendor Name Device Name [VendorID:DeviceID]"
|
||||
re2 := regexp.MustCompile(`:\s+(.+?)\s+\[([0-9a-f]{4}):([0-9a-f]{4})\]`)
|
||||
matches2 := re2.FindStringSubmatch(line)
|
||||
if len(matches2) > 3 {
|
||||
// Parse vendor name from description
|
||||
vendorDesc := matches2[1]
|
||||
if idx := strings.Index(vendorDesc, " "); idx > 0 {
|
||||
currentAdapter.Vendor = strings.Split(vendorDesc, " ")[0]
|
||||
}
|
||||
|
||||
// Parse vendor ID and device ID
|
||||
if vendorID, err := strconv.ParseInt(matches2[2], 16, 32); err == nil {
|
||||
currentAdapter.VendorID = int(vendorID)
|
||||
}
|
||||
if deviceID, err := strconv.ParseInt(matches2[3], 16, 32); err == nil {
|
||||
currentAdapter.DeviceID = int(deviceID)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if not processing an adapter
|
||||
if currentAdapter == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse Physical Slot
|
||||
if strings.HasPrefix(trimmed, "Physical Slot:") {
|
||||
slotStr := strings.TrimPrefix(trimmed, "Physical Slot:")
|
||||
currentAdapter.Slot = strings.TrimSpace(slotStr)
|
||||
continue
|
||||
}
|
||||
|
||||
// Start of Vital Product Data section
|
||||
if strings.Contains(trimmed, "Vital Product Data") {
|
||||
inVPD = true
|
||||
continue
|
||||
}
|
||||
|
||||
// End of VPD section
|
||||
if inVPD && (trimmed == "End" || strings.HasPrefix(trimmed, "Capabilities:")) {
|
||||
if trimmed == "End" {
|
||||
inVPD = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse Product Name in VPD
|
||||
if inVPD && strings.HasPrefix(trimmed, "Product Name:") {
|
||||
productName := strings.TrimPrefix(trimmed, "Product Name:")
|
||||
currentAdapter.Model = strings.TrimSpace(productName)
|
||||
|
||||
// Extract port count from model name
|
||||
if strings.Contains(currentAdapter.Model, "Dual-port") {
|
||||
currentAdapter.PortCount = 2
|
||||
} else if strings.Contains(currentAdapter.Model, "Single-port") {
|
||||
currentAdapter.PortCount = 1
|
||||
} else if strings.Contains(currentAdapter.Model, "Quad-port") {
|
||||
currentAdapter.PortCount = 4
|
||||
}
|
||||
|
||||
// Extract port type from model name
|
||||
if strings.Contains(currentAdapter.Model, "QSFP56") {
|
||||
currentAdapter.PortType = "QSFP56"
|
||||
} else if strings.Contains(currentAdapter.Model, "QSFP28") {
|
||||
currentAdapter.PortType = "QSFP28"
|
||||
} else if strings.Contains(currentAdapter.Model, "OSFP") {
|
||||
currentAdapter.PortType = "OSFP"
|
||||
} else if strings.Contains(currentAdapter.Model, "SFP") {
|
||||
currentAdapter.PortType = "SFP+"
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse VPD fields
|
||||
if inVPD && strings.HasPrefix(trimmed, "[") {
|
||||
// Match pattern: [TAG] Description: Value
|
||||
re := regexp.MustCompile(`^\[([A-Z0-9]+)\]\s+([^:]+):\s+(.+)`)
|
||||
matches := re.FindStringSubmatch(trimmed)
|
||||
if len(matches) > 3 {
|
||||
tag := matches[1]
|
||||
value := strings.TrimSpace(matches[3])
|
||||
|
||||
switch tag {
|
||||
case "PN":
|
||||
// Part number
|
||||
currentAdapter.PartNumber = value
|
||||
case "SN":
|
||||
// Serial number
|
||||
currentAdapter.SerialNumber = value
|
||||
case "EC":
|
||||
// Engineering changes - could be stored as firmware/revision
|
||||
if currentAdapter.Firmware == "" {
|
||||
currentAdapter.Firmware = value
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// End of current device section (empty line followed by hex dump or new device)
|
||||
if currentAdapter != nil && trimmed == "" {
|
||||
// Check if next lines are hex dump (config space)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Save last adapter if exists
|
||||
if currentAdapter != nil && currentAdapter.Model != "" {
|
||||
result.Hardware.NetworkAdapters = append(result.Hardware.NetworkAdapters, *currentAdapter)
|
||||
}
|
||||
}
|
||||
107
internal/parser/vendors/nvidia_bug_report/parser.go
vendored
Normal file
107
internal/parser/vendors/nvidia_bug_report/parser.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
// Package nvidia_bug_report provides parser for NVIDIA bug report files
|
||||
// Generated by nvidia-bug-report.sh script
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
// parserVersion - version of this parser module
|
||||
const parserVersion = "1.0.0"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
}
|
||||
|
||||
// Parser implements VendorParser for NVIDIA bug reports
|
||||
type Parser struct{}
|
||||
|
||||
// Name returns human-readable parser name
|
||||
func (p *Parser) Name() string {
|
||||
return "NVIDIA Bug Report Parser"
|
||||
}
|
||||
|
||||
// Vendor returns vendor identifier
|
||||
func (p *Parser) Vendor() string {
|
||||
return "nvidia_bug_report"
|
||||
}
|
||||
|
||||
// Version returns parser version
|
||||
func (p *Parser) Version() string {
|
||||
return parserVersion
|
||||
}
|
||||
|
||||
// Detect checks if this is an NVIDIA bug report
|
||||
// Returns confidence 0-100
|
||||
func (p *Parser) Detect(files []parser.ExtractedFile) int {
|
||||
// Only detect if there's exactly one file
|
||||
if len(files) != 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
file := files[0]
|
||||
|
||||
// Check filename
|
||||
if !strings.Contains(strings.ToLower(file.Path), "nvidia-bug-report") {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Check content markers
|
||||
content := string(file.Content)
|
||||
if !strings.Contains(content, "nvidia-bug-report.sh") ||
|
||||
!strings.Contains(content, "NVIDIA bug report log file") {
|
||||
return 0
|
||||
}
|
||||
|
||||
// High confidence for nvidia-bug-report files
|
||||
return 85
|
||||
}
|
||||
|
||||
// Parse parses NVIDIA bug report file
|
||||
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),
|
||||
}
|
||||
|
||||
// Initialize hardware config
|
||||
result.Hardware = &models.HardwareConfig{
|
||||
CPUs: make([]models.CPU, 0),
|
||||
Memory: make([]models.MemoryDIMM, 0),
|
||||
GPUs: make([]models.GPU, 0),
|
||||
PowerSupply: make([]models.PSU, 0),
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
content := string(files[0].Content)
|
||||
|
||||
// Parse system information
|
||||
parseSystemInfo(content, result)
|
||||
|
||||
// Parse CPU information
|
||||
parseCPUInfo(content, result)
|
||||
|
||||
// Parse memory modules
|
||||
parseMemoryModules(content, result)
|
||||
|
||||
// Parse power supplies
|
||||
parsePSUInfo(content, result)
|
||||
|
||||
// Parse GPU information
|
||||
parseGPUInfo(content, result)
|
||||
|
||||
// Parse network adapters
|
||||
parseNetworkAdapters(content, result)
|
||||
|
||||
// Parse driver version
|
||||
parseDriverVersion(content, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
116
internal/parser/vendors/nvidia_bug_report/psu.go
vendored
Normal file
116
internal/parser/vendors/nvidia_bug_report/psu.go
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// parsePSUInfo extracts Power Supply information from dmidecode output
|
||||
func parsePSUInfo(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
|
||||
var currentPSU *models.PSU
|
||||
inPowerSupply := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Start of System Power Supply section
|
||||
if strings.Contains(trimmed, "System Power Supply") {
|
||||
inPowerSupply = true
|
||||
currentPSU = &models.PSU{}
|
||||
continue
|
||||
}
|
||||
|
||||
// End of current section (empty line or new section with Handle)
|
||||
if inPowerSupply && (trimmed == "" || strings.HasPrefix(trimmed, "Handle ")) {
|
||||
// Save PSU if it has valid data
|
||||
if currentPSU != nil && currentPSU.Slot != "" {
|
||||
// Only add if PSU is present
|
||||
if strings.Contains(strings.ToLower(currentPSU.Status), "present") {
|
||||
result.Hardware.PowerSupply = append(result.Hardware.PowerSupply, *currentPSU)
|
||||
}
|
||||
}
|
||||
inPowerSupply = false
|
||||
currentPSU = nil
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse fields within System Power Supply section
|
||||
if inPowerSupply && currentPSU != nil && strings.Contains(line, ":") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
if value == "" || value == "Not Specified" || value == "Unknown" || value == "UNKNOWN" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "Location":
|
||||
currentPSU.Slot = value
|
||||
case "Name":
|
||||
// Use Name as Model if Model is not set later
|
||||
if currentPSU.Model == "" {
|
||||
currentPSU.Model = value
|
||||
}
|
||||
case "Manufacturer":
|
||||
currentPSU.Vendor = value
|
||||
case "Serial Number":
|
||||
currentPSU.SerialNumber = value
|
||||
case "Model Part Number":
|
||||
// Use Model Part Number as the primary model identifier
|
||||
currentPSU.Model = value
|
||||
case "Revision":
|
||||
currentPSU.Firmware = value
|
||||
case "Max Power Capacity":
|
||||
// Parse wattage like "2700 W"
|
||||
if wattage := parsePowerWattage(value); wattage > 0 {
|
||||
currentPSU.WattageW = wattage
|
||||
}
|
||||
case "Status":
|
||||
currentPSU.Status = value
|
||||
case "Type":
|
||||
// Could store PSU type if needed (e.g., "Switching")
|
||||
case "Plugged":
|
||||
// Could track if PSU is plugged
|
||||
case "Hot Replaceable":
|
||||
// Could track if hot-swappable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last PSU if exists
|
||||
if currentPSU != nil && currentPSU.Slot != "" {
|
||||
if strings.Contains(strings.ToLower(currentPSU.Status), "present") {
|
||||
result.Hardware.PowerSupply = append(result.Hardware.PowerSupply, *currentPSU)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parsePowerWattage parses power capacity strings like "2700 W" or "1200 Watts"
|
||||
func parsePowerWattage(powerStr string) int {
|
||||
parts := strings.Fields(powerStr)
|
||||
if len(parts) < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Try to parse the number
|
||||
wattageStr := parts[0]
|
||||
wattage, err := strconv.Atoi(wattageStr)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Check if unit is specified (W, Watts, etc.) and convert if needed
|
||||
// For now, assume it's always in Watts
|
||||
return wattage
|
||||
}
|
||||
61
internal/parser/vendors/nvidia_bug_report/system_info.go
vendored
Normal file
61
internal/parser/vendors/nvidia_bug_report/system_info.go
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
package nvidia_bug_report
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
// parseSystemInfo extracts System Information from dmidecode output
|
||||
func parseSystemInfo(content string, result *models.AnalysisResult) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
|
||||
inSystemInfo := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Start of System Information section
|
||||
if trimmed == "System Information" {
|
||||
inSystemInfo = true
|
||||
continue
|
||||
}
|
||||
|
||||
// End of section (empty line or new Handle)
|
||||
if inSystemInfo && (trimmed == "" || strings.HasPrefix(trimmed, "Handle ")) {
|
||||
inSystemInfo = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse fields within System Information section
|
||||
if inSystemInfo && strings.Contains(line, ":") {
|
||||
parts := strings.SplitN(trimmed, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
// Skip empty, NULL, or "Not specified" values
|
||||
if value == "" || value == "NULL" || value == "Not specified" || value == "Not Specified" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "Manufacturer":
|
||||
result.Hardware.BoardInfo.Manufacturer = value
|
||||
case "Product Name":
|
||||
result.Hardware.BoardInfo.ProductName = value
|
||||
case "Version":
|
||||
result.Hardware.BoardInfo.Version = value
|
||||
case "Serial Number":
|
||||
result.Hardware.BoardInfo.SerialNumber = value
|
||||
case "UUID":
|
||||
result.Hardware.BoardInfo.UUID = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user