feat: improve inspur parsing and pci.ids integration
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "third_party/pciids"]
|
||||||
|
path = third_party/pciids
|
||||||
|
url = https://github.com/pciutils/pciids.git
|
||||||
25
CLAUDE.md
25
CLAUDE.md
@@ -44,6 +44,31 @@ Registry: `internal/collector/registry.go`
|
|||||||
- progress logs include active collection stage and snapshot progress.
|
- progress logs include active collection stage and snapshot progress.
|
||||||
- `ipmi` is currently a mock collector scaffold.
|
- `ipmi` is currently a mock collector scaffold.
|
||||||
|
|
||||||
|
## Inspur/Kaytus parser notes
|
||||||
|
|
||||||
|
- Base hardware inventory comes from `asset.json` + `component.log` + `devicefrusdr.log`.
|
||||||
|
- Additional runtime enrichment is applied from `redis-dump.rdb` (if present):
|
||||||
|
- GPU serial/firmware/UUID and selected runtime metrics;
|
||||||
|
- NIC firmware/serial/part fields where text logs are incomplete.
|
||||||
|
- GPU/NIC enrichment from Redis is conservative (fills missing fields, avoids unsafe remapping).
|
||||||
|
|
||||||
|
### External PCI IDs lookup (no hardcoded model mapping)
|
||||||
|
|
||||||
|
`internal/parser/vendors/pciids` now loads IDs from a repo file
|
||||||
|
(`internal/parser/vendors/pciids/pci.ids`, embedded at build time) plus optional external overrides.
|
||||||
|
|
||||||
|
Lookup priority:
|
||||||
|
1. embedded `internal/parser/vendors/pciids/pci.ids`,
|
||||||
|
2. `./pci.ids`,
|
||||||
|
3. `/usr/share/hwdata/pci.ids`,
|
||||||
|
4. `/usr/share/misc/pci.ids`,
|
||||||
|
5. `/opt/homebrew/share/pciids/pci.ids`,
|
||||||
|
6. `LOGPILE_PCI_IDS_PATH` (highest priority overrides; supports path list).
|
||||||
|
|
||||||
|
Implication:
|
||||||
|
- for unknown device IDs (e.g. new NVIDIA GPU IDs), model naming can be updated via `pci.ids`
|
||||||
|
without changing parser code.
|
||||||
|
|
||||||
## Export behavior
|
## Export behavior
|
||||||
|
|
||||||
Endpoints:
|
Endpoints:
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: build run clean test build-all
|
.PHONY: build run clean test build-all update-pci-ids
|
||||||
|
|
||||||
BINARY_NAME=logpile
|
BINARY_NAME=logpile
|
||||||
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
@@ -6,6 +6,7 @@ COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
|
|||||||
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)"
|
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
@if [ "$(SKIP_PCI_IDS_UPDATE)" != "1" ]; then ./scripts/update-pci-ids.sh --best-effort; fi
|
||||||
CGO_ENABLED=0 go build $(LDFLAGS) -o bin/$(BINARY_NAME) ./cmd/logpile
|
CGO_ENABLED=0 go build $(LDFLAGS) -o bin/$(BINARY_NAME) ./cmd/logpile
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
@@ -19,6 +20,7 @@ test:
|
|||||||
|
|
||||||
# Cross-platform builds
|
# Cross-platform builds
|
||||||
build-all: clean
|
build-all: clean
|
||||||
|
@if [ "$(SKIP_PCI_IDS_UPDATE)" != "1" ]; then ./scripts/update-pci-ids.sh --best-effort; fi
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-amd64 ./cmd/logpile
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-amd64 ./cmd/logpile
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-arm64 ./cmd/logpile
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-arm64 ./cmd/logpile
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-amd64 ./cmd/logpile
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-amd64 ./cmd/logpile
|
||||||
@@ -33,3 +35,6 @@ fmt:
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
|
update-pci-ids:
|
||||||
|
./scripts/update-pci-ids.sh --sync-submodule
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -17,6 +17,65 @@ LOGPile — standalone Go-приложение для анализа диагн
|
|||||||
- Загрузка JSON snapshot обратно через `/api/upload` для оффлайн-работы.
|
- Загрузка JSON snapshot обратно через `/api/upload` для оффлайн-работы.
|
||||||
- Экспорт в CSV / JSON.
|
- Экспорт в CSV / JSON.
|
||||||
|
|
||||||
|
## Дополнительные источники данных (Inspur/Kaytus)
|
||||||
|
|
||||||
|
Для архивов Inspur/Kaytus парсер использует не только `asset.json` и `component.log`,
|
||||||
|
но и runtime-снимок `onekeylog/runningdata/redis-dump.rdb` (если файл присутствует).
|
||||||
|
|
||||||
|
Что это даёт:
|
||||||
|
- обогащение GPU: `serial_number`, `firmware` (VBIOS/FW), часть runtime telemetry;
|
||||||
|
- обогащение NIC: firmware/serial/part-number (когда в текстовых логах поля пустые).
|
||||||
|
|
||||||
|
## Внешний PCI IDs (без хардкода моделей)
|
||||||
|
|
||||||
|
Источник PCI IDs в проекте: официальный репозиторий
|
||||||
|
[`pciutils/pciids`](https://github.com/pciutils/pciids), подключён как git submodule:
|
||||||
|
`third_party/pciids`.
|
||||||
|
Локальная копия для встроенного lookup хранится в:
|
||||||
|
`internal/parser/vendors/pciids/pci.ids`.
|
||||||
|
|
||||||
|
Обновление локальной копии:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make update-pci-ids
|
||||||
|
```
|
||||||
|
|
||||||
|
Команда запускает `scripts/update-pci-ids.sh`, который скачивает актуальный
|
||||||
|
`pci.ids` из submodule (`git submodule update --init --remote third_party/pciids`)
|
||||||
|
и синхронизирует его в `internal/parser/vendors/pciids/pci.ids`.
|
||||||
|
|
||||||
|
Автообновление при сборке:
|
||||||
|
- `make build` и `make build-all` запускают `scripts/update-pci-ids.sh --best-effort`;
|
||||||
|
- если submodule уже инициализирован, `pci.ids` синхронизируется перед сборкой;
|
||||||
|
- если submodule не инициализирован/недоступен, используется текущая копия файла,
|
||||||
|
сборка не прерывается.
|
||||||
|
|
||||||
|
Отключить автообновление при сборке:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SKIP_PCI_IDS_UPDATE=1 make build
|
||||||
|
```
|
||||||
|
|
||||||
|
Если репозиторий клонирован без submodule:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --init third_party/pciids
|
||||||
|
```
|
||||||
|
|
||||||
|
Парсер использует такой порядок lookup:
|
||||||
|
1. встроенный в бинарник `internal/parser/vendors/pciids/pci.ids`;
|
||||||
|
2. `./pci.ids`;
|
||||||
|
3. `/usr/share/hwdata/pci.ids`;
|
||||||
|
4. `/usr/share/misc/pci.ids`;
|
||||||
|
5. `/opt/homebrew/share/pciids/pci.ids`;
|
||||||
|
6. `LOGPILE_PCI_IDS_PATH` (можно передать несколько путей через `:`; имеет наивысший приоритет и переопределяет предыдущие значения).
|
||||||
|
|
||||||
|
Пример запуска:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LOGPILE_PCI_IDS_PATH=/path/to/pci.ids ./bin/logpile
|
||||||
|
```
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- Go 1.22+
|
- Go 1.22+
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedfishConnector struct {
|
type RedfishConnector struct {
|
||||||
@@ -725,12 +726,27 @@ func parseDrive(doc map[string]interface{}) models.Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
|
func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
|
||||||
|
vendorID := asHexOrInt(doc["VendorId"])
|
||||||
|
deviceID := asHexOrInt(doc["DeviceId"])
|
||||||
|
model := firstNonEmpty(asString(doc["Model"]), asString(doc["Name"]))
|
||||||
|
if isMissingOrRawPCIModel(model) {
|
||||||
|
if resolved := pciids.DeviceName(vendorID, deviceID); resolved != "" {
|
||||||
|
model = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vendor := asString(doc["Manufacturer"])
|
||||||
|
if strings.TrimSpace(vendor) == "" {
|
||||||
|
vendor = pciids.VendorName(vendorID)
|
||||||
|
}
|
||||||
|
|
||||||
return models.NetworkAdapter{
|
return models.NetworkAdapter{
|
||||||
Slot: firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])),
|
Slot: firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])),
|
||||||
Location: asString(doc["Location"]),
|
Location: asString(doc["Location"]),
|
||||||
Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"),
|
Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"),
|
||||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
Model: strings.TrimSpace(model),
|
||||||
Vendor: asString(doc["Manufacturer"]),
|
Vendor: strings.TrimSpace(vendor),
|
||||||
|
VendorID: vendorID,
|
||||||
|
DeviceID: deviceID,
|
||||||
SerialNumber: asString(doc["SerialNumber"]),
|
SerialNumber: asString(doc["SerialNumber"]),
|
||||||
PartNumber: asString(doc["PartNumber"]),
|
PartNumber: asString(doc["PartNumber"]),
|
||||||
Status: mapStatus(doc["Status"]),
|
Status: mapStatus(doc["Status"]),
|
||||||
@@ -824,6 +840,15 @@ func parseGPU(doc map[string]interface{}, functionDocs []map[string]interface{},
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isMissingOrRawPCIModel(gpu.Model) {
|
||||||
|
if resolved := pciids.DeviceName(gpu.VendorID, gpu.DeviceID); resolved != "" {
|
||||||
|
gpu.Model = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(gpu.Manufacturer) == "" {
|
||||||
|
gpu.Manufacturer = pciids.VendorName(gpu.VendorID)
|
||||||
|
}
|
||||||
|
|
||||||
return gpu
|
return gpu
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,6 +894,17 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
|
|||||||
if dev.DeviceClass == "" {
|
if dev.DeviceClass == "" {
|
||||||
dev.DeviceClass = "PCIe device"
|
dev.DeviceClass = "PCIe device"
|
||||||
}
|
}
|
||||||
|
if isGenericPCIeClassLabel(dev.DeviceClass) {
|
||||||
|
if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
|
||||||
|
dev.DeviceClass = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dev.Manufacturer) == "" {
|
||||||
|
dev.Manufacturer = pciids.VendorName(dev.VendorID)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dev.PartNumber) == "" {
|
||||||
|
dev.PartNumber = pciids.DeviceName(dev.VendorID, dev.DeviceID)
|
||||||
|
}
|
||||||
return dev
|
return dev
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +914,7 @@ func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
|
|||||||
slot = fmt.Sprintf("PCIeFn%d", idx)
|
slot = fmt.Sprintf("PCIeFn%d", idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return models.PCIeDevice{
|
dev := models.PCIeDevice{
|
||||||
Slot: slot,
|
Slot: slot,
|
||||||
BDF: asString(doc["FunctionId"]),
|
BDF: asString(doc["FunctionId"]),
|
||||||
VendorID: asHexOrInt(doc["VendorId"]),
|
VendorID: asHexOrInt(doc["VendorId"]),
|
||||||
@@ -891,6 +927,54 @@ func parsePCIeFunction(doc map[string]interface{}, idx int) models.PCIeDevice {
|
|||||||
MaxLinkWidth: asInt(doc["MaxLinkWidth"]),
|
MaxLinkWidth: asInt(doc["MaxLinkWidth"]),
|
||||||
MaxLinkSpeed: firstNonEmpty(asString(doc["MaxLinkSpeedGTs"]), asString(doc["MaxLinkSpeed"])),
|
MaxLinkSpeed: firstNonEmpty(asString(doc["MaxLinkSpeedGTs"]), asString(doc["MaxLinkSpeed"])),
|
||||||
}
|
}
|
||||||
|
if isGenericPCIeClassLabel(dev.DeviceClass) {
|
||||||
|
if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
|
||||||
|
dev.DeviceClass = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dev.Manufacturer) == "" {
|
||||||
|
dev.Manufacturer = pciids.VendorName(dev.VendorID)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dev.PartNumber) == "" {
|
||||||
|
dev.PartNumber = pciids.DeviceName(dev.VendorID, dev.DeviceID)
|
||||||
|
}
|
||||||
|
return dev
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMissingOrRawPCIModel(model string) bool {
|
||||||
|
model = strings.TrimSpace(model)
|
||||||
|
if model == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
l := strings.ToLower(model)
|
||||||
|
if l == "unknown" || l == "n/a" || l == "na" || l == "none" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(l, "0x") && len(l) <= 6 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(model) <= 4 {
|
||||||
|
isHex := true
|
||||||
|
for _, c := range l {
|
||||||
|
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
|
||||||
|
isHex = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isHex {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGenericPCIeClassLabel(v string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||||
|
case "", "pcie device", "display", "display controller", "vga", "3d controller", "network", "network controller", "storage", "storage controller", "other", "unknown":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(v)), "0x")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func looksLikeGPU(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
|
func looksLikeGPU(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
|
||||||
|
|||||||
40
internal/collector/redfish_pciids_test.go
Normal file
40
internal/collector/redfish_pciids_test.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseNIC_ResolvesModelFromPCIIDs(t *testing.T) {
|
||||||
|
doc := map[string]interface{}{
|
||||||
|
"Id": "NIC1",
|
||||||
|
"VendorId": "0x8086",
|
||||||
|
"DeviceId": "0x1521",
|
||||||
|
"Model": "0x1521",
|
||||||
|
}
|
||||||
|
|
||||||
|
nic := parseNIC(doc)
|
||||||
|
if nic.Model == "" {
|
||||||
|
t.Fatalf("expected model resolved from pci.ids")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToUpper(nic.Model), "I350") {
|
||||||
|
t.Fatalf("expected I350 in model, got %q", nic.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePCIeFunction_ResolvesDeviceClassFromPCIIDs(t *testing.T) {
|
||||||
|
doc := map[string]interface{}{
|
||||||
|
"Id": "PCIE1",
|
||||||
|
"VendorId": "0x9005",
|
||||||
|
"DeviceId": "0x028f",
|
||||||
|
"ClassCode": "0x010700",
|
||||||
|
}
|
||||||
|
|
||||||
|
dev := parsePCIeFunction(doc, 0)
|
||||||
|
if dev.DeviceClass == "" || strings.EqualFold(dev.DeviceClass, "PCIe device") {
|
||||||
|
t.Fatalf("expected device class resolved from pci.ids, got %q", dev.DeviceClass)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(dev.DeviceClass)), "0x") {
|
||||||
|
t.Fatalf("expected resolved name instead of raw hex, got %q", dev.DeviceClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
)
|
)
|
||||||
@@ -34,7 +35,7 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
|
|
||||||
// FRU data
|
// FRU data
|
||||||
for _, fru := range e.result.FRU {
|
for _, fru := range e.result.FRU {
|
||||||
if fru.SerialNumber == "" {
|
if !hasUsableSerial(fru.SerialNumber) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := fru.ProductName
|
name := fru.ProductName
|
||||||
@@ -53,9 +54,36 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
|
|
||||||
// Hardware data
|
// Hardware data
|
||||||
if e.result.Hardware != nil {
|
if e.result.Hardware != nil {
|
||||||
|
// Board
|
||||||
|
if hasUsableSerial(e.result.Hardware.BoardInfo.SerialNumber) {
|
||||||
|
if err := writer.Write([]string{
|
||||||
|
e.result.Hardware.BoardInfo.ProductName,
|
||||||
|
strings.TrimSpace(e.result.Hardware.BoardInfo.SerialNumber),
|
||||||
|
e.result.Hardware.BoardInfo.Manufacturer,
|
||||||
|
"Board",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPUs
|
||||||
|
for _, cpu := range e.result.Hardware.CPUs {
|
||||||
|
if !hasUsableSerial(cpu.SerialNumber) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := writer.Write([]string{
|
||||||
|
cpu.Model,
|
||||||
|
strings.TrimSpace(cpu.SerialNumber),
|
||||||
|
"",
|
||||||
|
"CPU",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Memory
|
// Memory
|
||||||
for _, mem := range e.result.Hardware.Memory {
|
for _, mem := range e.result.Hardware.Memory {
|
||||||
if mem.SerialNumber == "" {
|
if !hasUsableSerial(mem.SerialNumber) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
location := mem.Location
|
location := mem.Location
|
||||||
@@ -64,7 +92,7 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
}
|
}
|
||||||
if err := writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
mem.PartNumber,
|
mem.PartNumber,
|
||||||
mem.SerialNumber,
|
strings.TrimSpace(mem.SerialNumber),
|
||||||
mem.Manufacturer,
|
mem.Manufacturer,
|
||||||
location,
|
location,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -74,12 +102,12 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
for _, stor := range e.result.Hardware.Storage {
|
for _, stor := range e.result.Hardware.Storage {
|
||||||
if stor.SerialNumber == "" {
|
if !hasUsableSerial(stor.SerialNumber) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
stor.Model,
|
stor.Model,
|
||||||
stor.SerialNumber,
|
strings.TrimSpace(stor.SerialNumber),
|
||||||
stor.Manufacturer,
|
stor.Manufacturer,
|
||||||
stor.Slot,
|
stor.Slot,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -87,20 +115,88 @@ func (e *Exporter) ExportCSV(w io.Writer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GPUs
|
||||||
|
for _, gpu := range e.result.Hardware.GPUs {
|
||||||
|
if !hasUsableSerial(gpu.SerialNumber) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
component := gpu.Model
|
||||||
|
if component == "" {
|
||||||
|
component = "GPU"
|
||||||
|
}
|
||||||
|
if err := writer.Write([]string{
|
||||||
|
component,
|
||||||
|
strings.TrimSpace(gpu.SerialNumber),
|
||||||
|
gpu.Manufacturer,
|
||||||
|
gpu.Slot,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PCIe devices
|
// PCIe devices
|
||||||
for _, pcie := range e.result.Hardware.PCIeDevices {
|
for _, pcie := range e.result.Hardware.PCIeDevices {
|
||||||
if pcie.SerialNumber == "" {
|
if !hasUsableSerial(pcie.SerialNumber) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
pcie.DeviceClass,
|
pcie.DeviceClass,
|
||||||
pcie.SerialNumber,
|
strings.TrimSpace(pcie.SerialNumber),
|
||||||
pcie.Manufacturer,
|
pcie.Manufacturer,
|
||||||
pcie.Slot,
|
pcie.Slot,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Network adapters
|
||||||
|
for _, nic := range e.result.Hardware.NetworkAdapters {
|
||||||
|
if !hasUsableSerial(nic.SerialNumber) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
location := nic.Location
|
||||||
|
if location == "" {
|
||||||
|
location = nic.Slot
|
||||||
|
}
|
||||||
|
if err := writer.Write([]string{
|
||||||
|
nic.Model,
|
||||||
|
strings.TrimSpace(nic.SerialNumber),
|
||||||
|
nic.Vendor,
|
||||||
|
location,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy network cards
|
||||||
|
for _, nic := range e.result.Hardware.NetworkCards {
|
||||||
|
if !hasUsableSerial(nic.SerialNumber) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := writer.Write([]string{
|
||||||
|
nic.Model,
|
||||||
|
strings.TrimSpace(nic.SerialNumber),
|
||||||
|
"",
|
||||||
|
"Network",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power supplies
|
||||||
|
for _, psu := range e.result.Hardware.PowerSupply {
|
||||||
|
if !hasUsableSerial(psu.SerialNumber) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := writer.Write([]string{
|
||||||
|
psu.Model,
|
||||||
|
strings.TrimSpace(psu.SerialNumber),
|
||||||
|
psu.Vendor,
|
||||||
|
psu.Slot,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -112,3 +208,16 @@ func (e *Exporter) ExportJSON(w io.Writer) error {
|
|||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
return encoder.Encode(e.result)
|
return encoder.Encode(e.result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasUsableSerial(serial string) bool {
|
||||||
|
s := strings.TrimSpace(serial)
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(s) {
|
||||||
|
case "N/A", "NA", "NONE", "NULL", "UNKNOWN", "-":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
79
internal/exporter/exporter_csv_test.go
Normal file
79
internal/exporter/exporter_csv_test.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package exporter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExportCSV_IncludesAllComponentTypesWithUsableSerials(t *testing.T) {
|
||||||
|
result := &models.AnalysisResult{
|
||||||
|
FRU: []models.FRUInfo{
|
||||||
|
{ProductName: "FRU Board", SerialNumber: "FRU-001", Manufacturer: "ACME"},
|
||||||
|
},
|
||||||
|
Hardware: &models.HardwareConfig{
|
||||||
|
BoardInfo: models.BoardInfo{
|
||||||
|
ProductName: "X12",
|
||||||
|
SerialNumber: "BOARD-001",
|
||||||
|
Manufacturer: "Supermicro",
|
||||||
|
},
|
||||||
|
CPUs: []models.CPU{
|
||||||
|
{Socket: 0, Model: "Xeon", SerialNumber: "CPU-001"},
|
||||||
|
},
|
||||||
|
Memory: []models.MemoryDIMM{
|
||||||
|
{Slot: "DIMM0", PartNumber: "MEM-PN", SerialNumber: "MEM-001", Manufacturer: "Samsung"},
|
||||||
|
},
|
||||||
|
Storage: []models.Storage{
|
||||||
|
{Slot: "U.2-1", Model: "PM9A3", SerialNumber: "SSD-001", Manufacturer: "Samsung"},
|
||||||
|
},
|
||||||
|
GPUs: []models.GPU{
|
||||||
|
{Slot: "GPU1", Model: "H200", SerialNumber: "GPU-001", Manufacturer: "NVIDIA"},
|
||||||
|
},
|
||||||
|
PCIeDevices: []models.PCIeDevice{
|
||||||
|
{Slot: "PCIe1", DeviceClass: "NVSwitch", SerialNumber: "PCIE-001", Manufacturer: "NVIDIA"},
|
||||||
|
},
|
||||||
|
NetworkAdapters: []models.NetworkAdapter{
|
||||||
|
{Slot: "Slot 17", Location: "#CPU0_PCIE4", Model: "I350", SerialNumber: "NIC-001", Vendor: "Intel"},
|
||||||
|
{Slot: "Slot 18", Model: "skip-na", SerialNumber: "N/A", Vendor: "Intel"},
|
||||||
|
},
|
||||||
|
NetworkCards: []models.NIC{
|
||||||
|
{Model: "Legacy NIC", SerialNumber: "LNIC-001"},
|
||||||
|
},
|
||||||
|
PowerSupply: []models.PSU{
|
||||||
|
{Slot: "PSU0", Model: "GW-CRPS3000LW", SerialNumber: "PSU-001", Vendor: "Great Wall"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := New(result).ExportCSV(&buf); err != nil {
|
||||||
|
t.Fatalf("ExportCSV failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := csv.NewReader(bytes.NewReader(buf.Bytes())).ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read csv: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) < 2 {
|
||||||
|
t.Fatalf("expected data rows, got %d", len(rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
serials := make(map[string]bool)
|
||||||
|
for _, row := range rows[1:] {
|
||||||
|
if len(row) > 1 {
|
||||||
|
serials[row[1]] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []string{"FRU-001", "BOARD-001", "CPU-001", "MEM-001", "SSD-001", "GPU-001", "PCIE-001", "NIC-001", "LNIC-001", "PSU-001"}
|
||||||
|
for _, sn := range want {
|
||||||
|
if !serials[sn] {
|
||||||
|
t.Fatalf("expected serial %s in csv export", sn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if serials["N/A"] {
|
||||||
|
t.Fatalf("did not expect unusable serial N/A in export")
|
||||||
|
}
|
||||||
|
}
|
||||||
69
internal/parser/vendors/inspur/asset.go
vendored
69
internal/parser/vendors/inspur/asset.go
vendored
@@ -3,12 +3,15 @@ package inspur
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var rawHexPCIDeviceRegex = regexp.MustCompile(`(?i)^0x[0-9a-f]+$`)
|
||||||
|
|
||||||
// AssetJSON represents the structure of Inspur asset.json file
|
// AssetJSON represents the structure of Inspur asset.json file
|
||||||
type AssetJSON struct {
|
type AssetJSON struct {
|
||||||
VersionInfo []struct {
|
VersionInfo []struct {
|
||||||
@@ -207,8 +210,8 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
|||||||
VendorID: pcie.VendorId,
|
VendorID: pcie.VendorId,
|
||||||
DeviceID: pcie.DeviceId,
|
DeviceID: pcie.DeviceId,
|
||||||
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
||||||
LinkWidth: pcie.NegotiatedLinkWidth,
|
LinkWidth: pcie.NegotiatedLinkWidth,
|
||||||
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
||||||
MaxLinkWidth: pcie.MaxLinkWidth,
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
||||||
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
|
DeviceClass: pcieClassToString(pcie.ClassCode, pcie.SubClassCode),
|
||||||
@@ -225,25 +228,22 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
|||||||
}
|
}
|
||||||
// Use device name from PCI IDs database if available
|
// Use device name from PCI IDs database if available
|
||||||
if deviceName != "" {
|
if deviceName != "" {
|
||||||
device.DeviceClass = deviceName
|
device.DeviceClass = normalizeModelLabel(deviceName)
|
||||||
}
|
}
|
||||||
config.PCIeDevices = append(config.PCIeDevices, device)
|
config.PCIeDevices = append(config.PCIeDevices, device)
|
||||||
|
|
||||||
// Extract GPUs (class 3 = display controller)
|
// Extract GPUs (class 3 = display controller)
|
||||||
if pcie.ClassCode == 3 {
|
if pcie.ClassCode == 3 {
|
||||||
gpuModel := deviceName
|
gpuModel := normalizeGPUModel(pcie.VendorId, pcie.DeviceId, deviceName, pcie.ClassCode, pcie.SubClassCode)
|
||||||
if gpuModel == "" {
|
|
||||||
gpuModel = pcieClassToString(pcie.ClassCode, pcie.SubClassCode)
|
|
||||||
}
|
|
||||||
gpu := models.GPU{
|
gpu := models.GPU{
|
||||||
Slot: pcie.LocString,
|
Slot: pcie.LocString,
|
||||||
Model: gpuModel,
|
Model: gpuModel,
|
||||||
Manufacturer: vendor,
|
Manufacturer: vendor,
|
||||||
VendorID: pcie.VendorId,
|
VendorID: pcie.VendorId,
|
||||||
DeviceID: pcie.DeviceId,
|
DeviceID: pcie.DeviceId,
|
||||||
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
||||||
CurrentLinkWidth: pcie.NegotiatedLinkWidth,
|
CurrentLinkWidth: pcie.NegotiatedLinkWidth,
|
||||||
CurrentLinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
CurrentLinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
||||||
MaxLinkWidth: pcie.MaxLinkWidth,
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
MaxLinkSpeed: pcieLinkSpeedToString(pcie.MaxLinkSpeed),
|
||||||
}
|
}
|
||||||
@@ -260,6 +260,45 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeModelLabel(v string) string {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(strings.Fields(v), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeGPUModel(vendorID, deviceID int, model string, classCode, subClass int) string {
|
||||||
|
model = normalizeModelLabel(model)
|
||||||
|
|
||||||
|
if model == "" || rawHexPCIDeviceRegex.MatchString(model) || isGenericGPUModelLabel(model) {
|
||||||
|
if pciModel := normalizeModelLabel(pciids.DeviceName(vendorID, deviceID)); pciModel != "" {
|
||||||
|
model = pciModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if model == "" || isGenericGPUModelLabel(model) {
|
||||||
|
model = pcieClassToString(classCode, subClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last fallback for unknown NVIDIA display devices: expose PCI DeviceID
|
||||||
|
// instead of generic "3D Controller".
|
||||||
|
if (model == "" || strings.EqualFold(model, "3D Controller")) && vendorID == 0x10de && deviceID > 0 {
|
||||||
|
return fmt.Sprintf("0x%04X", deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGenericGPUModelLabel(model string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(model)) {
|
||||||
|
case "", "gpu", "display", "display controller", "vga", "3d controller", "other", "unknown":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func memoryTypeToString(memType int) string {
|
func memoryTypeToString(memType int) string {
|
||||||
switch memType {
|
switch memType {
|
||||||
case 26:
|
case 26:
|
||||||
|
|||||||
48
internal/parser/vendors/inspur/asset_gpu_model_test.go
vendored
Normal file
48
internal/parser/vendors/inspur/asset_gpu_model_test.go
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseAssetJSON_NVIDIAGPUModelFromPCIIDs(t *testing.T) {
|
||||||
|
raw := []byte(`{
|
||||||
|
"VersionInfo": [],
|
||||||
|
"CpuInfo": [],
|
||||||
|
"MemInfo": {"MemCommonInfo": [], "DimmInfo": []},
|
||||||
|
"HddInfo": [],
|
||||||
|
"PcieInfo": [{
|
||||||
|
"VendorId": 4318,
|
||||||
|
"DeviceId": 9019,
|
||||||
|
"BusNumber": 12,
|
||||||
|
"DeviceNumber": 0,
|
||||||
|
"FunctionNumber": 0,
|
||||||
|
"MaxLinkWidth": 16,
|
||||||
|
"MaxLinkSpeed": 5,
|
||||||
|
"NegotiatedLinkWidth": 16,
|
||||||
|
"CurrentLinkSpeed": 5,
|
||||||
|
"ClassCode": 3,
|
||||||
|
"SubClassCode": 2,
|
||||||
|
"PcieSlot": 11,
|
||||||
|
"LocString": "#CPU0_PCIE2",
|
||||||
|
"PartNumber": null,
|
||||||
|
"SerialNumber": null,
|
||||||
|
"Mac": []
|
||||||
|
}]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
hw, err := ParseAssetJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseAssetJSON failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(hw.GPUs) != 1 {
|
||||||
|
t.Fatalf("expected 1 GPU, got %d", len(hw.GPUs))
|
||||||
|
}
|
||||||
|
if hw.GPUs[0].Model != "GH100 [H200 NVL]" {
|
||||||
|
t.Fatalf("expected model GH100 [H200 NVL], got %q", hw.GPUs[0].Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeGPUModel_FallbackToDeviceIDForUnknownNVIDIA(t *testing.T) {
|
||||||
|
got := normalizeGPUModel(0x10de, 0xbeef, "0xBEEF\t", 3, 2)
|
||||||
|
if got != "0xBEEF" {
|
||||||
|
t.Fatalf("expected 0xBEEF, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
90
internal/parser/vendors/inspur/component.go
vendored
90
internal/parser/vendors/inspur/component.go
vendored
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseComponentLog parses component.log file and extracts detailed hardware info
|
// ParseComponentLog parses component.log file and extracts detailed hardware info
|
||||||
@@ -52,20 +53,20 @@ func ParseComponentLogEvents(content []byte) []models.Event {
|
|||||||
// MemoryRESTInfo represents the RESTful Memory info structure
|
// MemoryRESTInfo represents the RESTful Memory info structure
|
||||||
type MemoryRESTInfo struct {
|
type MemoryRESTInfo struct {
|
||||||
MemModules []struct {
|
MemModules []struct {
|
||||||
MemModID int `json:"mem_mod_id"`
|
MemModID int `json:"mem_mod_id"`
|
||||||
ConfigStatus int `json:"config_status"`
|
ConfigStatus int `json:"config_status"`
|
||||||
MemModSlot string `json:"mem_mod_slot"`
|
MemModSlot string `json:"mem_mod_slot"`
|
||||||
MemModStatus int `json:"mem_mod_status"`
|
MemModStatus int `json:"mem_mod_status"`
|
||||||
MemModSize int `json:"mem_mod_size"`
|
MemModSize int `json:"mem_mod_size"`
|
||||||
MemModType string `json:"mem_mod_type"`
|
MemModType string `json:"mem_mod_type"`
|
||||||
MemModTechnology string `json:"mem_mod_technology"`
|
MemModTechnology string `json:"mem_mod_technology"`
|
||||||
MemModFrequency int `json:"mem_mod_frequency"`
|
MemModFrequency int `json:"mem_mod_frequency"`
|
||||||
MemModCurrentFreq int `json:"mem_mod_current_frequency"`
|
MemModCurrentFreq int `json:"mem_mod_current_frequency"`
|
||||||
MemModVendor string `json:"mem_mod_vendor"`
|
MemModVendor string `json:"mem_mod_vendor"`
|
||||||
MemModPartNum string `json:"mem_mod_part_num"`
|
MemModPartNum string `json:"mem_mod_part_num"`
|
||||||
MemModSerial string `json:"mem_mod_serial_num"`
|
MemModSerial string `json:"mem_mod_serial_num"`
|
||||||
MemModRanks int `json:"mem_mod_ranks"`
|
MemModRanks int `json:"mem_mod_ranks"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
} `json:"mem_modules"`
|
} `json:"mem_modules"`
|
||||||
TotalMemoryCount int `json:"total_memory_count"`
|
TotalMemoryCount int `json:"total_memory_count"`
|
||||||
PresentMemoryCount int `json:"present_memory_count"`
|
PresentMemoryCount int `json:"present_memory_count"`
|
||||||
@@ -112,21 +113,21 @@ func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
|||||||
// PSURESTInfo represents the RESTful PSU info structure
|
// PSURESTInfo represents the RESTful PSU info structure
|
||||||
type PSURESTInfo struct {
|
type PSURESTInfo struct {
|
||||||
PowerSupplies []struct {
|
PowerSupplies []struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Present int `json:"present"`
|
Present int `json:"present"`
|
||||||
VendorID string `json:"vendor_id"`
|
VendorID string `json:"vendor_id"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
SerialNum string `json:"serial_num"`
|
SerialNum string `json:"serial_num"`
|
||||||
PartNum string `json:"part_num"`
|
PartNum string `json:"part_num"`
|
||||||
FwVer string `json:"fw_ver"`
|
FwVer string `json:"fw_ver"`
|
||||||
InputType string `json:"input_type"`
|
InputType string `json:"input_type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
RatedPower int `json:"rated_power"`
|
RatedPower int `json:"rated_power"`
|
||||||
PSInPower int `json:"ps_in_power"`
|
PSInPower int `json:"ps_in_power"`
|
||||||
PSOutPower int `json:"ps_out_power"`
|
PSOutPower int `json:"ps_out_power"`
|
||||||
PSInVolt float64 `json:"ps_in_volt"`
|
PSInVolt float64 `json:"ps_in_volt"`
|
||||||
PSOutVolt float64 `json:"ps_out_volt"`
|
PSOutVolt float64 `json:"ps_out_volt"`
|
||||||
PSUMaxTemp int `json:"psu_max_temperature"`
|
PSUMaxTemp int `json:"psu_max_temperature"`
|
||||||
} `json:"power_supplies"`
|
} `json:"power_supplies"`
|
||||||
PresentPowerReading int `json:"present_power_reading"`
|
PresentPowerReading int `json:"present_power_reading"`
|
||||||
}
|
}
|
||||||
@@ -304,17 +305,28 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model := normalizeModelLabel(adapter.Model)
|
||||||
|
if model == "" || looksLikeRawDeviceID(model) {
|
||||||
|
if resolved := normalizeModelLabel(pciids.DeviceName(adapter.VendorID, adapter.DeviceID)); resolved != "" {
|
||||||
|
model = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vendor := normalizeModelLabel(adapter.Vendor)
|
||||||
|
if vendor == "" {
|
||||||
|
vendor = normalizeModelLabel(pciids.VendorName(adapter.VendorID))
|
||||||
|
}
|
||||||
|
|
||||||
hw.NetworkAdapters = append(hw.NetworkAdapters, models.NetworkAdapter{
|
hw.NetworkAdapters = append(hw.NetworkAdapters, models.NetworkAdapter{
|
||||||
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
|
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
|
||||||
Location: adapter.Location,
|
Location: adapter.Location,
|
||||||
Present: adapter.Present == 1,
|
Present: adapter.Present == 1,
|
||||||
Model: strings.TrimSpace(adapter.Model),
|
Model: model,
|
||||||
Vendor: strings.TrimSpace(adapter.Vendor),
|
Vendor: vendor,
|
||||||
VendorID: adapter.VendorID,
|
VendorID: adapter.VendorID,
|
||||||
DeviceID: adapter.DeviceID,
|
DeviceID: adapter.DeviceID,
|
||||||
SerialNumber: strings.TrimSpace(adapter.SN),
|
SerialNumber: normalizeRedisValue(adapter.SN),
|
||||||
PartNumber: strings.TrimSpace(adapter.PN),
|
PartNumber: normalizeRedisValue(adapter.PN),
|
||||||
Firmware: adapter.FwVer,
|
Firmware: normalizeRedisValue(adapter.FwVer),
|
||||||
PortCount: adapter.PortNum,
|
PortCount: adapter.PortNum,
|
||||||
PortType: adapter.PortType,
|
PortType: adapter.PortType,
|
||||||
MACAddresses: macs,
|
MACAddresses: macs,
|
||||||
@@ -323,6 +335,16 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rawDeviceIDLikeRegex = regexp.MustCompile(`(?i)^(?:0x)?[0-9a-f]{3,4}$`)
|
||||||
|
|
||||||
|
func looksLikeRawDeviceID(v string) bool {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return rawDeviceIDLikeRegex.MatchString(v)
|
||||||
|
}
|
||||||
|
|
||||||
func parseMemoryEvents(text string) []models.Event {
|
func parseMemoryEvents(text string) []models.Event {
|
||||||
var events []models.Event
|
var events []models.Event
|
||||||
|
|
||||||
|
|||||||
52
internal/parser/vendors/inspur/component_test.go
vendored
Normal file
52
internal/parser/vendors/inspur/component_test.go
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseNetworkAdapterInfo_ResolvesModelFromPCIIDsForRawHexModel(t *testing.T) {
|
||||||
|
text := `RESTful Network Adapter info:
|
||||||
|
{
|
||||||
|
"sys_adapters": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "NIC1",
|
||||||
|
"Location": "#CPU0_PCIE4",
|
||||||
|
"present": 1,
|
||||||
|
"slot": 4,
|
||||||
|
"vendor_id": 32902,
|
||||||
|
"device_id": 5409,
|
||||||
|
"vendor": "",
|
||||||
|
"model": "0x1521",
|
||||||
|
"fw_ver": "",
|
||||||
|
"status": "OK",
|
||||||
|
"sn": "",
|
||||||
|
"pn": "",
|
||||||
|
"port_num": 4,
|
||||||
|
"port_type": "Base-T",
|
||||||
|
"ports": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
RESTful fan`
|
||||||
|
|
||||||
|
hw := &models.HardwareConfig{}
|
||||||
|
parseNetworkAdapterInfo(text, hw)
|
||||||
|
|
||||||
|
if len(hw.NetworkAdapters) != 1 {
|
||||||
|
t.Fatalf("expected 1 network adapter, got %d", len(hw.NetworkAdapters))
|
||||||
|
}
|
||||||
|
got := hw.NetworkAdapters[0]
|
||||||
|
if got.Model == "" {
|
||||||
|
t.Fatalf("expected NIC model resolved from pci.ids, got empty")
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.ToUpper(got.Model), "I350") {
|
||||||
|
t.Fatalf("expected I350 in model, got %q", got.Model)
|
||||||
|
}
|
||||||
|
if got.Vendor == "" {
|
||||||
|
t.Fatalf("expected NIC vendor resolved from pci.ids")
|
||||||
|
}
|
||||||
|
}
|
||||||
17
internal/parser/vendors/inspur/parser.go
vendored
17
internal/parser/vendors/inspur/parser.go
vendored
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
// parserVersion - version of this parser module
|
// parserVersion - version of this parser module
|
||||||
// IMPORTANT: Increment this version when making changes to parser logic!
|
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||||
const parserVersion = "1.1.0"
|
const parserVersion = "1.2.1"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
parser.Register(&Parser{})
|
parser.Register(&Parser{})
|
||||||
@@ -125,6 +125,12 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
|||||||
result.Events = append(result.Events, componentEvents...)
|
result.Events = append(result.Events, componentEvents...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
|
||||||
|
// when text logs miss these fields.
|
||||||
|
if f := parser.FindFileByName(files, "redis-dump.rdb"); f != nil && result.Hardware != nil {
|
||||||
|
enrichFromRedisDump(f.Content, result.Hardware)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse IDL-like logs (plain and structured JSON logs with embedded IDL messages)
|
// Parse IDL-like logs (plain and structured JSON logs with embedded IDL messages)
|
||||||
idlFiles := parser.FindFileByPattern(files, "/idl.log", "idl_json.log", "run_json.log")
|
idlFiles := parser.FindFileByPattern(files, "/idl.log", "idl_json.log", "run_json.log")
|
||||||
for _, f := range idlFiles {
|
for _, f := range idlFiles {
|
||||||
@@ -199,14 +205,9 @@ func (p *Parser) parseDeviceFruSDR(content []byte, result *models.AnalysisResult
|
|||||||
// This supplements data from asset.json with serial numbers, firmware, etc.
|
// This supplements data from asset.json with serial numbers, firmware, etc.
|
||||||
pcieDevicesFromREST := ParsePCIeDevices(content)
|
pcieDevicesFromREST := ParsePCIeDevices(content)
|
||||||
|
|
||||||
// Merge PCIe data: keep asset.json data but add RESTful data if available
|
// Merge PCIe data: asset.json is the base inventory, RESTful data enriches names/links/serials.
|
||||||
if result.Hardware != nil {
|
if result.Hardware != nil {
|
||||||
// If asset.json didn't have PCIe devices, use RESTful data
|
result.Hardware.PCIeDevices = MergePCIeDevices(result.Hardware.PCIeDevices, pcieDevicesFromREST)
|
||||||
if len(result.Hardware.PCIeDevices) == 0 && len(pcieDevicesFromREST) > 0 {
|
|
||||||
result.Hardware.PCIeDevices = pcieDevicesFromREST
|
|
||||||
}
|
|
||||||
// If we have both, merge them (RESTful data takes precedence for detailed info)
|
|
||||||
// For now, we keep asset.json data which has more details
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse GPU devices and add temperature data from sensors
|
// Parse GPU devices and add temperature data from sensors
|
||||||
|
|||||||
217
internal/parser/vendors/inspur/pcie.go
vendored
217
internal/parser/vendors/inspur/pcie.go
vendored
@@ -3,36 +3,38 @@ package inspur
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PCIeRESTInfo represents the RESTful PCIE Device info structure
|
// PCIeRESTInfo represents the RESTful PCIE Device info structure
|
||||||
type PCIeRESTInfo []struct {
|
type PCIeRESTInfo []struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Present int `json:"present"`
|
Present int `json:"present"`
|
||||||
Enable int `json:"enable"`
|
Enable int `json:"enable"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
VendorID int `json:"vendor_id"`
|
VendorID int `json:"vendor_id"`
|
||||||
VendorName string `json:"vendor_name"`
|
VendorName string `json:"vendor_name"`
|
||||||
DeviceID int `json:"device_id"`
|
DeviceID int `json:"device_id"`
|
||||||
DeviceName string `json:"device_name"`
|
DeviceName string `json:"device_name"`
|
||||||
BusNum int `json:"bus_num"`
|
BusNum int `json:"bus_num"`
|
||||||
DevNum int `json:"dev_num"`
|
DevNum int `json:"dev_num"`
|
||||||
FuncNum int `json:"func_num"`
|
FuncNum int `json:"func_num"`
|
||||||
MaxLinkWidth int `json:"max_link_width"`
|
MaxLinkWidth int `json:"max_link_width"`
|
||||||
MaxLinkSpeed int `json:"max_link_speed"`
|
MaxLinkSpeed int `json:"max_link_speed"`
|
||||||
CurrentLinkWidth int `json:"current_link_width"`
|
CurrentLinkWidth int `json:"current_link_width"`
|
||||||
CurrentLinkSpeed int `json:"current_link_speed"`
|
CurrentLinkSpeed int `json:"current_link_speed"`
|
||||||
Slot int `json:"slot"`
|
Slot int `json:"slot"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
DeviceLocator string `json:"DeviceLocator"`
|
DeviceLocator string `json:"DeviceLocator"`
|
||||||
DevType int `json:"dev_type"`
|
DevType int `json:"dev_type"`
|
||||||
DevSubtype int `json:"dev_subtype"`
|
DevSubtype int `json:"dev_subtype"`
|
||||||
PartNum string `json:"part_num"`
|
PartNum string `json:"part_num"`
|
||||||
SerialNum string `json:"serial_num"`
|
SerialNum string `json:"serial_num"`
|
||||||
FwVer string `json:"fw_ver"`
|
FwVer string `json:"fw_ver"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParsePCIeDevices parses RESTful PCIE Device info from devicefrusdr.log
|
// ParsePCIeDevices parses RESTful PCIE Device info from devicefrusdr.log
|
||||||
@@ -73,9 +75,27 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
|
|
||||||
// Determine device class based on dev_type
|
// Determine device class based on dev_type
|
||||||
deviceClass := determineDeviceClass(pcie.DevType, pcie.DevSubtype, pcie.DeviceName)
|
deviceClass := determineDeviceClass(pcie.DevType, pcie.DevSubtype, pcie.DeviceName)
|
||||||
|
_, pciDeviceName := pciids.DeviceInfo(pcie.VendorID, pcie.DeviceID)
|
||||||
|
|
||||||
// Build BDF string
|
// Build BDF string in canonical form (bb:dd.f)
|
||||||
bdf := fmt.Sprintf("%04x/%02x/%02x/%02x", 0, pcie.BusNum, pcie.DevNum, pcie.FuncNum)
|
bdf := formatBDF(pcie.BusNum, pcie.DevNum, pcie.FuncNum)
|
||||||
|
|
||||||
|
partNumber := strings.TrimSpace(pcie.PartNum)
|
||||||
|
if partNumber == "" {
|
||||||
|
partNumber = sanitizePCIeDeviceName(pcie.DeviceName)
|
||||||
|
}
|
||||||
|
if partNumber == "" {
|
||||||
|
partNumber = normalizeModelLabel(pciDeviceName)
|
||||||
|
}
|
||||||
|
if isGenericPCIeClass(deviceClass) {
|
||||||
|
if resolved := normalizeModelLabel(pciDeviceName); resolved != "" {
|
||||||
|
deviceClass = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manufacturer := strings.TrimSpace(pcie.VendorName)
|
||||||
|
if manufacturer == "" {
|
||||||
|
manufacturer = normalizeModelLabel(pciids.VendorName(pcie.VendorID))
|
||||||
|
}
|
||||||
|
|
||||||
device := models.PCIeDevice{
|
device := models.PCIeDevice{
|
||||||
Slot: pcie.Location,
|
Slot: pcie.Location,
|
||||||
@@ -83,12 +103,12 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
DeviceID: pcie.DeviceID,
|
DeviceID: pcie.DeviceID,
|
||||||
BDF: bdf,
|
BDF: bdf,
|
||||||
DeviceClass: deviceClass,
|
DeviceClass: deviceClass,
|
||||||
Manufacturer: pcie.VendorName,
|
Manufacturer: manufacturer,
|
||||||
LinkWidth: pcie.CurrentLinkWidth,
|
LinkWidth: pcie.CurrentLinkWidth,
|
||||||
LinkSpeed: currentSpeed,
|
LinkSpeed: currentSpeed,
|
||||||
MaxLinkWidth: pcie.MaxLinkWidth,
|
MaxLinkWidth: pcie.MaxLinkWidth,
|
||||||
MaxLinkSpeed: maxSpeed,
|
MaxLinkSpeed: maxSpeed,
|
||||||
PartNumber: strings.TrimSpace(pcie.PartNum),
|
PartNumber: partNumber,
|
||||||
SerialNumber: strings.TrimSpace(pcie.SerialNum),
|
SerialNumber: strings.TrimSpace(pcie.SerialNum),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +118,149 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
return devices
|
return devices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rawHexDeviceNameRegex = regexp.MustCompile(`(?i)^0x[0-9a-f]+$`)
|
||||||
|
|
||||||
|
func sanitizePCIeDeviceName(name string) string {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.EqualFold(name, "N/A") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if rawHexDeviceNameRegex.MatchString(name) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergePCIeDevices enriches base devices (from asset.json) with detailed RESTful PCIe data.
|
||||||
|
// Matching is done by BDF first, then by slot fallback.
|
||||||
|
func MergePCIeDevices(base []models.PCIeDevice, rest []models.PCIeDevice) []models.PCIeDevice {
|
||||||
|
if len(rest) == 0 {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
if len(base) == 0 {
|
||||||
|
return append([]models.PCIeDevice(nil), rest...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ref struct {
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
byBDF := make(map[string]ref, len(base))
|
||||||
|
bySlot := make(map[string]ref, len(base))
|
||||||
|
|
||||||
|
for i := range base {
|
||||||
|
bdf := normalizePCIeBDF(base[i].BDF)
|
||||||
|
if bdf != "" {
|
||||||
|
byBDF[bdf] = ref{index: i}
|
||||||
|
}
|
||||||
|
slot := strings.ToLower(strings.TrimSpace(base[i].Slot))
|
||||||
|
if slot != "" {
|
||||||
|
bySlot[slot] = ref{index: i}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, detailed := range rest {
|
||||||
|
idx := -1
|
||||||
|
if bdf := normalizePCIeBDF(detailed.BDF); bdf != "" {
|
||||||
|
if found, ok := byBDF[bdf]; ok {
|
||||||
|
idx = found.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx == -1 {
|
||||||
|
slot := strings.ToLower(strings.TrimSpace(detailed.Slot))
|
||||||
|
if slot != "" {
|
||||||
|
if found, ok := bySlot[slot]; ok {
|
||||||
|
idx = found.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == -1 {
|
||||||
|
base = append(base, detailed)
|
||||||
|
newIdx := len(base) - 1
|
||||||
|
if bdf := normalizePCIeBDF(detailed.BDF); bdf != "" {
|
||||||
|
byBDF[bdf] = ref{index: newIdx}
|
||||||
|
}
|
||||||
|
if slot := strings.ToLower(strings.TrimSpace(detailed.Slot)); slot != "" {
|
||||||
|
bySlot[slot] = ref{index: newIdx}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
enrichPCIeDevice(&base[idx], detailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichPCIeDevice(dst *models.PCIeDevice, src models.PCIeDevice) {
|
||||||
|
if dst == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dst.Slot) == "" {
|
||||||
|
dst.Slot = src.Slot
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dst.BDF) == "" {
|
||||||
|
dst.BDF = src.BDF
|
||||||
|
}
|
||||||
|
if dst.VendorID == 0 {
|
||||||
|
dst.VendorID = src.VendorID
|
||||||
|
}
|
||||||
|
if dst.DeviceID == 0 {
|
||||||
|
dst.DeviceID = src.DeviceID
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dst.Manufacturer) == "" {
|
||||||
|
dst.Manufacturer = src.Manufacturer
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dst.SerialNumber) == "" {
|
||||||
|
dst.SerialNumber = src.SerialNumber
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dst.PartNumber) == "" {
|
||||||
|
dst.PartNumber = src.PartNumber
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dst.LinkSpeed) == "" || strings.EqualFold(strings.TrimSpace(dst.LinkSpeed), "unknown") {
|
||||||
|
dst.LinkSpeed = src.LinkSpeed
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(dst.MaxLinkSpeed) == "" || strings.EqualFold(strings.TrimSpace(dst.MaxLinkSpeed), "unknown") {
|
||||||
|
dst.MaxLinkSpeed = src.MaxLinkSpeed
|
||||||
|
}
|
||||||
|
if dst.LinkWidth == 0 {
|
||||||
|
dst.LinkWidth = src.LinkWidth
|
||||||
|
}
|
||||||
|
if dst.MaxLinkWidth == 0 {
|
||||||
|
dst.MaxLinkWidth = src.MaxLinkWidth
|
||||||
|
}
|
||||||
|
if isGenericPCIeClass(dst.DeviceClass) && !isGenericPCIeClass(src.DeviceClass) {
|
||||||
|
dst.DeviceClass = src.DeviceClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePCIeBDF(bdf string) string {
|
||||||
|
bdf = strings.TrimSpace(strings.ToLower(bdf))
|
||||||
|
if bdf == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(bdf, "/") {
|
||||||
|
parts := strings.Split(bdf, "/")
|
||||||
|
if len(parts) == 4 {
|
||||||
|
return fmt.Sprintf("%s:%s.%s", parts[1], parts[2], parts[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bdf
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGenericPCIeClass(class string) bool {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(class)) {
|
||||||
|
case "", "unknown", "other", "bridge", "network", "storage", "sas", "sata", "display", "vga", "3d controller", "serial bus":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// determineDeviceClass maps device type to human-readable class
|
// determineDeviceClass maps device type to human-readable class
|
||||||
func determineDeviceClass(devType, devSubtype int, deviceName string) string {
|
func determineDeviceClass(devType, devSubtype int, deviceName string) string {
|
||||||
// dev_type mapping:
|
// dev_type mapping:
|
||||||
|
|||||||
77
internal/parser/vendors/inspur/pcie_test.go
vendored
Normal file
77
internal/parser/vendors/inspur/pcie_test.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsePCIeDevices_UsesDeviceNameAsModelWhenPartNumberMissing(t *testing.T) {
|
||||||
|
content := []byte(`RESTful PCIE Device info:
|
||||||
|
[{"id":1,"present":1,"vendor_id":32902,"vendor_name":"Intel","device_id":5409,"device_name":"I350T4V2","bus_num":69,"dev_num":0,"func_num":0,"max_link_width":4,"max_link_speed":2,"current_link_width":4,"current_link_speed":2,"location":"#CPU0_PCIE4","dev_type":2,"dev_subtype":0,"part_num":"","serial_num":"","fw_ver":""}]
|
||||||
|
BMC sdr Info:`)
|
||||||
|
|
||||||
|
devices := ParsePCIeDevices(content)
|
||||||
|
if len(devices) != 1 {
|
||||||
|
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||||
|
}
|
||||||
|
if devices[0].PartNumber != "I350T4V2" {
|
||||||
|
t.Fatalf("expected part/model I350T4V2, got %q", devices[0].PartNumber)
|
||||||
|
}
|
||||||
|
if devices[0].BDF != "45:00.0" {
|
||||||
|
t.Fatalf("expected BDF 45:00.0, got %q", devices[0].BDF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergePCIeDevices_EnrichesGenericAssetEntry(t *testing.T) {
|
||||||
|
base := []models.PCIeDevice{
|
||||||
|
{
|
||||||
|
Slot: "#CPU1_PCIE9",
|
||||||
|
BDF: "98:00.0",
|
||||||
|
VendorID: 0x9005,
|
||||||
|
DeviceID: 0x028f,
|
||||||
|
DeviceClass: "SAS",
|
||||||
|
Manufacturer: "Adaptec / Microsemi",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rest := []models.PCIeDevice{
|
||||||
|
{
|
||||||
|
Slot: "#CPU1_PCIE9",
|
||||||
|
BDF: "98:00.0",
|
||||||
|
VendorID: 0x9005,
|
||||||
|
DeviceID: 0x028f,
|
||||||
|
DeviceClass: "Storage Controller",
|
||||||
|
Manufacturer: "Microchip",
|
||||||
|
PartNumber: "PM8222-SHBA",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := MergePCIeDevices(base, rest)
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected 1 merged device, got %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0].PartNumber != "PM8222-SHBA" {
|
||||||
|
t.Fatalf("expected merged part number PM8222-SHBA, got %q", got[0].PartNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePCIeDevices_ResolvesModelFromPCIIDsWhenDeviceNameIsRawHex(t *testing.T) {
|
||||||
|
content := []byte(`RESTful PCIE Device info:
|
||||||
|
[{"id":5,"present":1,"vendor_id":36869,"vendor_name":"","device_id":655,"device_name":"0x028F","bus_num":152,"dev_num":0,"func_num":0,"max_link_width":8,"max_link_speed":3,"current_link_width":8,"current_link_speed":3,"location":"#CPU1_PCIE9","dev_type":1,"dev_subtype":7,"part_num":"","serial_num":"","fw_ver":""}]
|
||||||
|
BMC sdr Info:`)
|
||||||
|
|
||||||
|
devices := ParsePCIeDevices(content)
|
||||||
|
if len(devices) != 1 {
|
||||||
|
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||||
|
}
|
||||||
|
if devices[0].PartNumber == "" {
|
||||||
|
t.Fatalf("expected part number resolved from pci.ids, got empty")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(devices[0].PartNumber)), "0x") {
|
||||||
|
t.Fatalf("expected resolved name instead of raw hex, got %q", devices[0].PartNumber)
|
||||||
|
}
|
||||||
|
if devices[0].Manufacturer == "" {
|
||||||
|
t.Fatalf("expected manufacturer resolved from pci.ids")
|
||||||
|
}
|
||||||
|
}
|
||||||
559
internal/parser/vendors/inspur/redis_dump.go
vendored
Normal file
559
internal/parser/vendors/inspur/redis_dump.go
vendored
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reRedisGPUKey = regexp.MustCompile(`GPUInfo:REDIS_GPUINFO_T([0-9]+):([A-Za-z0-9_]+)`)
|
||||||
|
reRedisNICKey = regexp.MustCompile(`RedisNicInfo:redis_nic_info_t:stNicDeviceInfo([0-9]+):([A-Za-z0-9_]+)`)
|
||||||
|
reRedisRAIDSerial = regexp.MustCompile(`RAIDMSCCInfo:redis_pcie_mscc_raid_info_t([0-9]+):RAIDInfo:SerialNum`)
|
||||||
|
reRedisPCIESNPN = regexp.MustCompile(`AssetInfoPCIE:SNPN([0-9]+):(SN|PN)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type redisGPUSnapshot struct {
|
||||||
|
ByIndex map[int]map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type redisNICSnapshot struct {
|
||||||
|
ByIndex map[int]map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type redisPCIESerialSnapshot struct {
|
||||||
|
ByPart map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichFromRedisDump(content []byte, hw *models.HardwareConfig) {
|
||||||
|
if hw == nil || len(content) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gpuSnap := parseRedisGPUSnapshot(content)
|
||||||
|
nicSnap := parseRedisNICSnapshot(content)
|
||||||
|
raidSerials := parseRedisRAIDSerials(content)
|
||||||
|
pcieSnap := parseRedisPCIESerialSnapshot(content)
|
||||||
|
|
||||||
|
applyRedisGPUEnrichment(hw, gpuSnap)
|
||||||
|
applyRedisNICEnrichment(hw, nicSnap)
|
||||||
|
applyRedisPCIESNPNEnrichment(hw, pcieSnap)
|
||||||
|
applyRedisPCIeEnrichment(hw, raidSerials)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRedisRAIDSerials(content []byte) []string {
|
||||||
|
matches := reRedisRAIDSerial.FindAllSubmatchIndex(content, -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]bool, len(matches))
|
||||||
|
serials := make([]string, 0, len(matches))
|
||||||
|
for _, m := range matches {
|
||||||
|
if len(m) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value := normalizeRedisValue(extractRedisCandidateValue(content, m[1]))
|
||||||
|
if value == "" || seen[value] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = true
|
||||||
|
serials = append(serials, value)
|
||||||
|
}
|
||||||
|
return serials
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRedisPCIESerialSnapshot(content []byte) redisPCIESerialSnapshot {
|
||||||
|
type rec struct {
|
||||||
|
PN string
|
||||||
|
SN string
|
||||||
|
}
|
||||||
|
tmp := make(map[int]rec)
|
||||||
|
|
||||||
|
matches := reRedisPCIESNPN.FindAllSubmatchIndex(content, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
if len(m) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idxStr := string(content[m[2]:m[3]])
|
||||||
|
field := string(content[m[4]:m[5]])
|
||||||
|
idx, err := strconv.Atoi(idxStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := normalizeRedisValue(extractRedisCandidateValue(content, m[1]))
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r := tmp[idx]
|
||||||
|
if field == "PN" {
|
||||||
|
r.PN = value
|
||||||
|
} else if field == "SN" {
|
||||||
|
r.SN = value
|
||||||
|
}
|
||||||
|
tmp[idx] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
out := redisPCIESerialSnapshot{ByPart: make(map[string]string)}
|
||||||
|
for _, r := range tmp {
|
||||||
|
pn := normalizeRedisValue(r.PN)
|
||||||
|
sn := normalizeRedisValue(r.SN)
|
||||||
|
if pn == "" || sn == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.ByPart[strings.ToLower(strings.TrimSpace(pn))] = sn
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRedisGPUSnapshot(content []byte) redisGPUSnapshot {
|
||||||
|
snap := redisGPUSnapshot{ByIndex: make(map[int]map[string]string)}
|
||||||
|
matches := reRedisGPUKey.FindAllSubmatchIndex(content, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
if len(m) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idxStr := string(content[m[2]:m[3]])
|
||||||
|
field := string(content[m[4]:m[5]])
|
||||||
|
idx, err := strconv.Atoi(idxStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := extractRedisInlineValue(content, m[1])
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
byField, ok := snap.ByIndex[idx]
|
||||||
|
if !ok {
|
||||||
|
byField = make(map[string]string)
|
||||||
|
snap.ByIndex[idx] = byField
|
||||||
|
}
|
||||||
|
byField[field] = value
|
||||||
|
}
|
||||||
|
return snap
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRedisNICSnapshot(content []byte) redisNICSnapshot {
|
||||||
|
snap := redisNICSnapshot{ByIndex: make(map[int]map[string]string)}
|
||||||
|
matches := reRedisNICKey.FindAllSubmatchIndex(content, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
if len(m) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idxStr := string(content[m[2]:m[3]])
|
||||||
|
field := string(content[m[4]:m[5]])
|
||||||
|
idx, err := strconv.Atoi(idxStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := extractRedisInlineValue(content, m[1])
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
byField, ok := snap.ByIndex[idx]
|
||||||
|
if !ok {
|
||||||
|
byField = make(map[string]string)
|
||||||
|
snap.ByIndex[idx] = byField
|
||||||
|
}
|
||||||
|
byField[field] = value
|
||||||
|
}
|
||||||
|
return snap
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRedisInlineValue(content []byte, start int) string {
|
||||||
|
if start < 0 || start >= len(content) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
i := start
|
||||||
|
for i < len(content) && content[i] <= 0x20 {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i >= len(content) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
j := i
|
||||||
|
for j < len(content) {
|
||||||
|
c := content[j]
|
||||||
|
if c == 0 || c < 0x20 || c > 0x7e {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
|
||||||
|
if j <= i {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := strings.TrimSpace(string(content[i:j]))
|
||||||
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded := maybeDecodeHexString(raw)
|
||||||
|
if decoded != "" {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRedisCandidateValue(content []byte, start int) string {
|
||||||
|
// Fast-path for simple inline string values.
|
||||||
|
if v := extractRedisInlineValue(content, start); normalizeRedisValue(v) != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
if start < 0 || start >= len(content) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
end := start + 256
|
||||||
|
if end > len(content) {
|
||||||
|
end = len(content)
|
||||||
|
}
|
||||||
|
window := content[start:end]
|
||||||
|
|
||||||
|
for _, token := range splitAlphaNumTokens(window) {
|
||||||
|
if len(token) < 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(token)
|
||||||
|
if strings.Contains(lower, "redis") || strings.Contains(lower, "sensor") || strings.Contains(lower, "fullsdr") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if decoded := maybeDecodeHexString(token); normalizeRedisValue(decoded) != "" {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
if normalizeRedisValue(token) != "" {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitAlphaNumTokens(b []byte) []string {
|
||||||
|
var out []string
|
||||||
|
start := -1
|
||||||
|
for i := 0; i < len(b); i++ {
|
||||||
|
c := rune(b[i])
|
||||||
|
if unicode.IsLetter(c) || unicode.IsDigit(c) {
|
||||||
|
if start == -1 {
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if start != -1 {
|
||||||
|
out = append(out, string(b[start:i]))
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start != -1 {
|
||||||
|
out = append(out, string(b[start:]))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeDecodeHexString(s string) string {
|
||||||
|
if len(s) < 8 || len(s)%2 != 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range s {
|
||||||
|
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
decoded := strings.TrimSpace(strings.TrimRight(string(b), "\x00"))
|
||||||
|
if decoded == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, c := range decoded {
|
||||||
|
if c < 0x20 || c > 0x7e {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyRedisGPUEnrichment(hw *models.HardwareConfig, snap redisGPUSnapshot) {
|
||||||
|
if len(hw.GPUs) == 0 || len(snap.ByIndex) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type redisGPU struct {
|
||||||
|
Index int
|
||||||
|
Data map[string]string
|
||||||
|
}
|
||||||
|
redisGPUs := make([]redisGPU, 0, len(snap.ByIndex))
|
||||||
|
for idx, data := range snap.ByIndex {
|
||||||
|
if data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data["NV_GPU_SerialNumber"] == "" && data["NV_GPU_FWVersion"] == "" && data["NV_GPU_UUID"] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
redisGPUs = append(redisGPUs, redisGPU{Index: idx, Data: data})
|
||||||
|
}
|
||||||
|
if len(redisGPUs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(redisGPUs, func(i, j int) bool { return redisGPUs[i].Index < redisGPUs[j].Index })
|
||||||
|
|
||||||
|
target := make([]*models.GPU, 0, len(hw.GPUs))
|
||||||
|
for i := range hw.GPUs {
|
||||||
|
gpu := &hw.GPUs[i]
|
||||||
|
if isNVIDIAGPU(gpu) {
|
||||||
|
target = append(target, gpu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(target) == 0 || len(target) != len(redisGPUs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(target, func(i, j int) bool {
|
||||||
|
left := strings.TrimSpace(target[i].BDF)
|
||||||
|
right := strings.TrimSpace(target[j].BDF)
|
||||||
|
if left != "" && right != "" {
|
||||||
|
return left < right
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot)
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := range target {
|
||||||
|
applyRedisGPUFields(target[i], redisGPUs[i].Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNVIDIAGPU(gpu *models.GPU) bool {
|
||||||
|
if gpu == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if gpu.VendorID == 0x10de {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
man := strings.ToLower(strings.TrimSpace(gpu.Manufacturer))
|
||||||
|
return strings.Contains(man, "nvidia")
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyRedisGPUFields(gpu *models.GPU, fields map[string]string) {
|
||||||
|
if gpu == nil || fields == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if serial := normalizeRedisValue(fields["NV_GPU_SerialNumber"]); serial != "" && isMissingGPUField(gpu.SerialNumber) {
|
||||||
|
gpu.SerialNumber = serial
|
||||||
|
}
|
||||||
|
if fw := normalizeRedisValue(fields["NV_GPU_FWVersion"]); fw != "" && isMissingGPUField(gpu.Firmware) {
|
||||||
|
gpu.Firmware = fw
|
||||||
|
}
|
||||||
|
if uuid := normalizeRedisValue(fields["NV_GPU_UUID"]); uuid != "" && isMissingGPUField(gpu.UUID) {
|
||||||
|
gpu.UUID = uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
if part := normalizeRedisValue(fields["NVGPUPartNumber"]); part != "" && isMissingGPUField(gpu.PartNumber) {
|
||||||
|
gpu.PartNumber = part
|
||||||
|
}
|
||||||
|
if model := normalizeRedisValue(fields["NVGPUMarketingName"]); model != "" && isGenericGPUModel(gpu.Model) {
|
||||||
|
gpu.Model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
if gpu.ClockSpeed == 0 {
|
||||||
|
if mhz, ok := parseIntField(fields["OperatingSpeedMHz"]); ok {
|
||||||
|
gpu.ClockSpeed = mhz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gpu.Power == 0 {
|
||||||
|
if pwr, ok := parseIntField(fields["GPUTotalPower"]); ok {
|
||||||
|
gpu.Power = pwr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gpu.Temperature == 0 {
|
||||||
|
if temp, ok := parseIntField(fields["Temp"]); ok {
|
||||||
|
gpu.Temperature = temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if gpu.MemTemperature == 0 {
|
||||||
|
if temp, ok := parseIntField(fields["MemTemp"]); ok {
|
||||||
|
gpu.MemTemperature = temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIntField(v string) (int, bool) {
|
||||||
|
v = normalizeRedisValue(v)
|
||||||
|
if v == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeRedisValue(v string) string {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
l := strings.ToLower(v)
|
||||||
|
if l == "n/a" || l == "na" || l == "null" || l == "unknown" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMissingGPUField(v string) bool {
|
||||||
|
return normalizeRedisValue(v) == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isGenericGPUModel(model string) bool {
|
||||||
|
m := strings.ToLower(strings.TrimSpace(model))
|
||||||
|
switch m {
|
||||||
|
case "", "unknown", "display", "display controller", "3d controller", "vga", "gpu":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyRedisNICEnrichment(hw *models.HardwareConfig, snap redisNICSnapshot) {
|
||||||
|
if len(hw.NetworkAdapters) == 0 || len(snap.ByIndex) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type redisNIC struct {
|
||||||
|
Index int
|
||||||
|
Data map[string]string
|
||||||
|
}
|
||||||
|
redisNICs := make([]redisNIC, 0, len(snap.ByIndex))
|
||||||
|
for idx, data := range snap.ByIndex {
|
||||||
|
if data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normalizeRedisValue(data["FWVersion"]) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
redisNICs = append(redisNICs, redisNIC{Index: idx, Data: data})
|
||||||
|
}
|
||||||
|
if len(redisNICs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(redisNICs, func(i, j int) bool { return redisNICs[i].Index < redisNICs[j].Index })
|
||||||
|
|
||||||
|
target := make([]*models.NetworkAdapter, 0, len(hw.NetworkAdapters))
|
||||||
|
for i := range hw.NetworkAdapters {
|
||||||
|
nic := &hw.NetworkAdapters[i]
|
||||||
|
if nic.Present {
|
||||||
|
target = append(target, nic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(target) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(target, func(i, j int) bool {
|
||||||
|
left := strings.TrimSpace(target[i].Location)
|
||||||
|
right := strings.TrimSpace(target[j].Location)
|
||||||
|
if left != "" && right != "" {
|
||||||
|
return left < right
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot)
|
||||||
|
})
|
||||||
|
|
||||||
|
limit := len(target)
|
||||||
|
if len(redisNICs) < limit {
|
||||||
|
limit = len(redisNICs)
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
nic := target[i]
|
||||||
|
data := redisNICs[i].Data
|
||||||
|
|
||||||
|
if fw := normalizeRedisValue(data["FWVersion"]); fw != "" && normalizeRedisValue(nic.Firmware) == "" {
|
||||||
|
nic.Firmware = fw
|
||||||
|
}
|
||||||
|
if serial := normalizeRedisValue(data["SerialNum"]); serial != "" && normalizeRedisValue(nic.SerialNumber) == "" {
|
||||||
|
nic.SerialNumber = serial
|
||||||
|
}
|
||||||
|
if part := normalizeRedisValue(data["PartNum"]); part != "" && normalizeRedisValue(nic.PartNumber) == "" {
|
||||||
|
nic.PartNumber = part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyRedisPCIeEnrichment(hw *models.HardwareConfig, raidSerials []string) {
|
||||||
|
if hw == nil || len(hw.PCIeDevices) == 0 || len(raidSerials) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target := make([]*models.PCIeDevice, 0, len(hw.PCIeDevices))
|
||||||
|
for i := range hw.PCIeDevices {
|
||||||
|
dev := &hw.PCIeDevices[i]
|
||||||
|
if normalizeRedisValue(dev.SerialNumber) != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
class := strings.ToLower(strings.TrimSpace(dev.DeviceClass))
|
||||||
|
part := strings.ToLower(strings.TrimSpace(dev.PartNumber))
|
||||||
|
if strings.Contains(class, "raid") || strings.Contains(class, "sas") || strings.Contains(class, "storage") ||
|
||||||
|
strings.Contains(part, "raid") || strings.Contains(part, "sas") || strings.Contains(part, "hba") {
|
||||||
|
target = append(target, dev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(target) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(target, func(i, j int) bool {
|
||||||
|
left := strings.TrimSpace(target[i].BDF)
|
||||||
|
right := strings.TrimSpace(target[j].BDF)
|
||||||
|
if left != "" && right != "" {
|
||||||
|
return left < right
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(target[i].Slot) < strings.TrimSpace(target[j].Slot)
|
||||||
|
})
|
||||||
|
|
||||||
|
limit := len(target)
|
||||||
|
if len(raidSerials) < limit {
|
||||||
|
limit = len(raidSerials)
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
target[i].SerialNumber = raidSerials[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyRedisPCIESNPNEnrichment(hw *models.HardwareConfig, snap redisPCIESerialSnapshot) {
|
||||||
|
if hw == nil || len(hw.PCIeDevices) == 0 || len(snap.ByPart) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range hw.PCIeDevices {
|
||||||
|
dev := &hw.PCIeDevices[i]
|
||||||
|
if normalizeRedisValue(dev.SerialNumber) != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
part := strings.ToLower(strings.TrimSpace(dev.PartNumber))
|
||||||
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if serial := normalizeRedisValue(snap.ByPart[part]); serial != "" {
|
||||||
|
dev.SerialNumber = serial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
internal/parser/vendors/inspur/redis_dump_test.go
vendored
Normal file
144
internal/parser/vendors/inspur/redis_dump_test.go
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package inspur
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractRedisInlineValue_DecodesHexEncodedString(t *testing.T) {
|
||||||
|
data := []byte("RedisNicInfo:redis_nic_info_t:stNicDeviceInfo0:FWVersion 32362e34332e32353636000000000000\x00tail")
|
||||||
|
key := []byte("RedisNicInfo:redis_nic_info_t:stNicDeviceInfo0:FWVersion")
|
||||||
|
pos := indexBytes(data, key)
|
||||||
|
if pos < 0 {
|
||||||
|
t.Fatal("key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
got := extractRedisInlineValue(data, pos+len(key))
|
||||||
|
if got != "26.43.2566" {
|
||||||
|
t.Fatalf("expected decoded fw 26.43.2566, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyRedisGPUEnrichment_FillsSerialFirmwareUUID(t *testing.T) {
|
||||||
|
hw := &models.HardwareConfig{
|
||||||
|
GPUs: []models.GPU{
|
||||||
|
{Slot: "#CPU0_PCIE2", BDF: "0c:00.0", VendorID: 0x10de, Model: "3D Controller"},
|
||||||
|
{Slot: "#CPU0_PCIE1", BDF: "58:00.0", VendorID: 0x10de, Model: "3D Controller"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
snap := redisGPUSnapshot{
|
||||||
|
ByIndex: map[int]map[string]string{
|
||||||
|
1: {
|
||||||
|
"NV_GPU_SerialNumber": "1321125009572",
|
||||||
|
"NV_GPU_FWVersion": "96.00.B7.00.02",
|
||||||
|
"NV_GPU_UUID": "GPU-AAA",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"NV_GPU_SerialNumber": "1321125010420",
|
||||||
|
"NV_GPU_FWVersion": "96.00.B7.00.02",
|
||||||
|
"NV_GPU_UUID": "GPU-BBB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRedisGPUEnrichment(hw, snap)
|
||||||
|
|
||||||
|
if hw.GPUs[0].SerialNumber != "1321125009572" || hw.GPUs[0].Firmware != "96.00.B7.00.02" || hw.GPUs[0].UUID != "GPU-AAA" {
|
||||||
|
t.Fatalf("unexpected gpu0 enrichment: %+v", hw.GPUs[0])
|
||||||
|
}
|
||||||
|
if hw.GPUs[1].SerialNumber != "1321125010420" || hw.GPUs[1].Firmware != "96.00.B7.00.02" || hw.GPUs[1].UUID != "GPU-BBB" {
|
||||||
|
t.Fatalf("unexpected gpu1 enrichment: %+v", hw.GPUs[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyRedisGPUEnrichment_SkipsOnCountMismatch(t *testing.T) {
|
||||||
|
hw := &models.HardwareConfig{
|
||||||
|
GPUs: []models.GPU{
|
||||||
|
{Slot: "#CPU0_PCIE2", BDF: "0c:00.0", VendorID: 0x10de, Model: "3D Controller"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
snap := redisGPUSnapshot{
|
||||||
|
ByIndex: map[int]map[string]string{
|
||||||
|
1: {"NV_GPU_SerialNumber": "1321125009572"},
|
||||||
|
2: {"NV_GPU_SerialNumber": "1321125010420"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRedisGPUEnrichment(hw, snap)
|
||||||
|
if hw.GPUs[0].SerialNumber != "" {
|
||||||
|
t.Fatalf("expected no enrichment on count mismatch, got %q", hw.GPUs[0].SerialNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRedisRAIDSerials_DecodesHexSerial(t *testing.T) {
|
||||||
|
raw := []byte("RAIDMSCCInfo:redis_pcie_mscc_raid_info_t0:RAIDInfo:SerialNum\x80%@`5341523531314532 \x00tail")
|
||||||
|
got := parseRedisRAIDSerials(raw)
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected 1 raid serial, got %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0] != "SAR511E2" {
|
||||||
|
t.Fatalf("expected decoded serial SAR511E2, got %q", got[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyRedisPCIeEnrichment_FillsStorageControllerSerial(t *testing.T) {
|
||||||
|
hw := &models.HardwareConfig{
|
||||||
|
PCIeDevices: []models.PCIeDevice{
|
||||||
|
{Slot: "#CPU1_PCIE9", BDF: "98:00.0", DeviceClass: "Smart Storage PQI SAS", PartNumber: "PM8222-SHBA"},
|
||||||
|
{Slot: "#CPU0_PCIE3", BDF: "32:00.0", DeviceClass: "Fibre Channel", PartNumber: "LPE32002"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyRedisPCIeEnrichment(hw, []string{"SAR511E2"})
|
||||||
|
|
||||||
|
if hw.PCIeDevices[0].SerialNumber != "SAR511E2" {
|
||||||
|
t.Fatalf("expected PM8222 serial SAR511E2, got %q", hw.PCIeDevices[0].SerialNumber)
|
||||||
|
}
|
||||||
|
if hw.PCIeDevices[1].SerialNumber != "" {
|
||||||
|
t.Fatalf("expected non-storage device serial untouched, got %q", hw.PCIeDevices[1].SerialNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRedisPCIESerialSnapshot_MapsPNToSN(t *testing.T) {
|
||||||
|
raw := []byte("" +
|
||||||
|
"AssetInfoPCIE:SNPN9:PN PM8222-SHBA\x00" +
|
||||||
|
"AssetInfoPCIE:SNPN9:SN SAR511E2\x00")
|
||||||
|
|
||||||
|
snap := parseRedisPCIESerialSnapshot(raw)
|
||||||
|
got := snap.ByPart["pm8222-shba"]
|
||||||
|
if got != "SAR511E2" {
|
||||||
|
t.Fatalf("expected SN SAR511E2 for PM8222-SHBA, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyRedisPCIESNPNEnrichment_FillsByPartNumber(t *testing.T) {
|
||||||
|
hw := &models.HardwareConfig{
|
||||||
|
PCIeDevices: []models.PCIeDevice{
|
||||||
|
{Slot: "#CPU1_PCIE9", PartNumber: "PM8222-SHBA"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
snap := redisPCIESerialSnapshot{ByPart: map[string]string{"pm8222-shba": "SAR511E2"}}
|
||||||
|
|
||||||
|
applyRedisPCIESNPNEnrichment(hw, snap)
|
||||||
|
if hw.PCIeDevices[0].SerialNumber != "SAR511E2" {
|
||||||
|
t.Fatalf("expected serial SAR511E2, got %q", hw.PCIeDevices[0].SerialNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexBytes(haystack, needle []byte) int {
|
||||||
|
for i := 0; i+len(needle) <= len(haystack); i++ {
|
||||||
|
match := true
|
||||||
|
for j := 0; j < len(needle); j++ {
|
||||||
|
if haystack[i+j] != needle[j] {
|
||||||
|
match = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
41507
internal/parser/vendors/pciids/pci.ids
vendored
Normal file
41507
internal/parser/vendors/pciids/pci.ids
vendored
Normal file
File diff suppressed because it is too large
Load Diff
222
internal/parser/vendors/pciids/pciids.go
vendored
222
internal/parser/vendors/pciids/pciids.go
vendored
@@ -1,12 +1,27 @@
|
|||||||
package pciids
|
package pciids
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed pci.ids
|
||||||
|
embeddedPCIIDs string
|
||||||
|
|
||||||
|
loadOnce sync.Once
|
||||||
|
vendors map[int]string
|
||||||
|
devices map[string]string
|
||||||
)
|
)
|
||||||
|
|
||||||
// VendorName returns vendor name by PCI Vendor ID
|
// VendorName returns vendor name by PCI Vendor ID
|
||||||
func VendorName(vendorID int) string {
|
func VendorName(vendorID int) string {
|
||||||
|
loadPCIIDs()
|
||||||
if name, ok := vendors[vendorID]; ok {
|
if name, ok := vendors[vendorID]; ok {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
@@ -15,6 +30,7 @@ func VendorName(vendorID int) string {
|
|||||||
|
|
||||||
// DeviceName returns device name by Vendor ID and Device ID
|
// DeviceName returns device name by Vendor ID and Device ID
|
||||||
func DeviceName(vendorID, deviceID int) string {
|
func DeviceName(vendorID, deviceID int) string {
|
||||||
|
loadPCIIDs()
|
||||||
key := fmt.Sprintf("%04x:%04x", vendorID, deviceID)
|
key := fmt.Sprintf("%04x:%04x", vendorID, deviceID)
|
||||||
if name, ok := devices[key]; ok {
|
if name, ok := devices[key]; ok {
|
||||||
return name
|
return name
|
||||||
@@ -46,7 +62,6 @@ func VendorNameFromString(s string) string {
|
|||||||
} else if c >= 'a' && c <= 'f' {
|
} else if c >= 'a' && c <= 'f' {
|
||||||
id = id*16 + int(c-'a'+10)
|
id = id*16 + int(c-'a'+10)
|
||||||
} else {
|
} else {
|
||||||
// Not a valid hex string, return original
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,124 +69,99 @@ func VendorNameFromString(s string) string {
|
|||||||
return VendorName(id)
|
return VendorName(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common PCI Vendor IDs
|
func loadPCIIDs() {
|
||||||
// Source: https://pci-ids.ucw.cz/
|
loadOnce.Do(func() {
|
||||||
var vendors = map[int]string{
|
vendors = make(map[int]string)
|
||||||
// Storage controllers and SSDs
|
devices = make(map[string]string)
|
||||||
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
|
parsePCIIDs(strings.NewReader(embeddedPCIIDs), vendors, devices)
|
||||||
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
|
for _, path := range candidatePCIIDsPaths() {
|
||||||
0x10DE: "NVIDIA",
|
f, err := os.Open(path)
|
||||||
0x1002: "AMD/ATI",
|
if err != nil {
|
||||||
0x102B: "Matrox Electronics",
|
continue
|
||||||
0x1A03: "ASPEED Technology",
|
}
|
||||||
|
parsePCIIDs(f, vendors, devices)
|
||||||
// Storage controllers (RAID/HBA)
|
_ = f.Close()
|
||||||
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)
|
func candidatePCIIDsPaths() []string {
|
||||||
var devices = map[string]string{
|
paths := []string{
|
||||||
// NVIDIA GPUs (0x10DE)
|
"pci.ids",
|
||||||
"10de:26b9": "L40S 48GB",
|
"/usr/share/hwdata/pci.ids",
|
||||||
"10de:26b1": "L40 48GB",
|
"/usr/share/misc/pci.ids",
|
||||||
"10de:2684": "RTX 4090",
|
"/opt/homebrew/share/pciids/pci.ids",
|
||||||
"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)
|
// Env paths have highest priority, so they are applied last.
|
||||||
"1002:744c": "Instinct MI250X",
|
if env := strings.TrimSpace(os.Getenv("LOGPILE_PCI_IDS_PATH")); env != "" {
|
||||||
"1002:7408": "Instinct MI100",
|
for _, p := range strings.Split(env, string(os.PathListSeparator)) {
|
||||||
"1002:73a5": "RX 6950 XT",
|
p = strings.TrimSpace(p)
|
||||||
"1002:73bf": "RX 6900 XT",
|
if p != "" {
|
||||||
"1002:73df": "RX 6700 XT",
|
paths = append(paths, p)
|
||||||
"1002:7480": "RX 7900 XTX",
|
}
|
||||||
"1002:7483": "RX 7900 XT",
|
}
|
||||||
|
}
|
||||||
// ASPEED (0x1A03) - BMC VGA
|
return paths
|
||||||
"1a03:2000": "AST2500 VGA",
|
}
|
||||||
"1a03:1150": "AST2600 VGA",
|
|
||||||
|
func parsePCIIDs(r interface{ Read([]byte) (int, error) }, outVendors map[int]string, outDevices map[string]string) {
|
||||||
// Intel GPUs
|
scanner := bufio.NewScanner(r)
|
||||||
"8086:56c0": "Data Center GPU Flex 170",
|
currentVendor := -1
|
||||||
"8086:56c1": "Data Center GPU Flex 140",
|
|
||||||
|
for scanner.Scan() {
|
||||||
// Mellanox/NVIDIA NICs (0x15B3)
|
line := scanner.Text()
|
||||||
"15b3:1017": "ConnectX-5 100GbE",
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
"15b3:1019": "ConnectX-5 Ex",
|
continue
|
||||||
"15b3:101b": "ConnectX-6",
|
}
|
||||||
"15b3:101d": "ConnectX-6 Dx",
|
|
||||||
"15b3:101f": "ConnectX-6 Lx",
|
// Subdevice line (tab-tab) - ignored for now
|
||||||
"15b3:1021": "ConnectX-7",
|
if strings.HasPrefix(line, "\t\t") {
|
||||||
"15b3:a2d6": "ConnectX-4 Lx",
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device line
|
||||||
|
if strings.HasPrefix(line, "\t") {
|
||||||
|
if currentVendor < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimLeft(line, "\t")
|
||||||
|
fields := strings.Fields(trimmed)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deviceID, err := strconv.ParseInt(fields[0], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(trimmed[len(fields[0]):])
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%04x:%04x", currentVendor, int(deviceID))
|
||||||
|
outDevices[key] = name
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vendor line
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
currentVendor = -1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vendorID, err := strconv.ParseInt(fields[0], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
currentVendor = -1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(line[len(fields[0]):])
|
||||||
|
if name == "" {
|
||||||
|
currentVendor = -1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentVendor = int(vendorID)
|
||||||
|
outVendors[currentVendor] = name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
internal/parser/vendors/pciids/pciids_external_test.go
vendored
Normal file
38
internal/parser/vendors/pciids/pciids_external_test.go
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package pciids
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExternalPCIIDsLookup(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
idsPath := filepath.Join(dir, "pci.ids")
|
||||||
|
content := "" +
|
||||||
|
"# sample\n" +
|
||||||
|
"10de NVIDIA Corporation\n" +
|
||||||
|
"\t233b NVIDIA H200 SXM\n" +
|
||||||
|
"8086 Intel Corporation\n" +
|
||||||
|
"\t1521 I350 Gigabit Network Connection\n"
|
||||||
|
|
||||||
|
if err := os.WriteFile(idsPath, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("write pci.ids: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("LOGPILE_PCI_IDS_PATH", idsPath)
|
||||||
|
loadOnce = sync.Once{}
|
||||||
|
vendors = nil
|
||||||
|
devices = nil
|
||||||
|
|
||||||
|
if got := DeviceName(0x10de, 0x233b); got != "NVIDIA H200 SXM" {
|
||||||
|
t.Fatalf("expected external device name, got %q", got)
|
||||||
|
}
|
||||||
|
if got := VendorName(0x10de); got != "NVIDIA Corporation" {
|
||||||
|
t.Fatalf("expected external vendor name, got %q", got)
|
||||||
|
}
|
||||||
|
if got := DeviceName(0x8086, 0x1521); got != "I350 Gigabit Network Connection" {
|
||||||
|
t.Fatalf("expected external intel device name, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -309,6 +309,23 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var serials []SerialEntry
|
var serials []SerialEntry
|
||||||
|
seenByLocationSerial := make(map[string]bool)
|
||||||
|
markSeen := func(location, serial string) {
|
||||||
|
loc := strings.ToLower(strings.TrimSpace(location))
|
||||||
|
sn := strings.ToLower(strings.TrimSpace(serial))
|
||||||
|
if loc == "" || sn == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenByLocationSerial[loc+"|"+sn] = true
|
||||||
|
}
|
||||||
|
alreadySeen := func(location, serial string) bool {
|
||||||
|
loc := strings.ToLower(strings.TrimSpace(location))
|
||||||
|
sn := strings.ToLower(strings.TrimSpace(serial))
|
||||||
|
if loc == "" || sn == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return seenByLocationSerial[loc+"|"+sn]
|
||||||
|
}
|
||||||
|
|
||||||
// From FRU
|
// From FRU
|
||||||
for _, fru := range result.FRU {
|
for _, fru := range result.FRU {
|
||||||
@@ -403,6 +420,7 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
Manufacturer: gpu.Manufacturer,
|
Manufacturer: gpu.Manufacturer,
|
||||||
Category: "GPU",
|
Category: "GPU",
|
||||||
})
|
})
|
||||||
|
markSeen(gpu.Slot, gpu.SerialNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PCIe devices
|
// PCIe devices
|
||||||
@@ -410,7 +428,10 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
if !hasUsableSerial(pcie.SerialNumber) {
|
if !hasUsableSerial(pcie.SerialNumber) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
component := pcie.DeviceClass
|
if alreadySeen(pcie.Slot, pcie.SerialNumber) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
component := normalizePCIeSerialComponentName(pcie)
|
||||||
if strings.EqualFold(strings.TrimSpace(pcie.DeviceClass), "NVSwitch") && strings.TrimSpace(pcie.PartNumber) != "" {
|
if strings.EqualFold(strings.TrimSpace(pcie.DeviceClass), "NVSwitch") && strings.TrimSpace(pcie.PartNumber) != "" {
|
||||||
component = strings.TrimSpace(pcie.PartNumber)
|
component = strings.TrimSpace(pcie.PartNumber)
|
||||||
}
|
}
|
||||||
@@ -422,6 +443,7 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
PartNumber: pcie.PartNumber,
|
PartNumber: pcie.PartNumber,
|
||||||
Category: "PCIe",
|
Category: "PCIe",
|
||||||
})
|
})
|
||||||
|
markSeen(pcie.Slot, pcie.SerialNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network cards
|
// Network cards
|
||||||
@@ -431,9 +453,11 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
serials = append(serials, SerialEntry{
|
serials = append(serials, SerialEntry{
|
||||||
Component: nic.Model,
|
Component: nic.Model,
|
||||||
|
Location: nic.Name,
|
||||||
SerialNumber: strings.TrimSpace(nic.SerialNumber),
|
SerialNumber: strings.TrimSpace(nic.SerialNumber),
|
||||||
Category: "Network",
|
Category: "Network",
|
||||||
})
|
})
|
||||||
|
markSeen(nic.Name, nic.SerialNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Power supplies
|
// Power supplies
|
||||||
@@ -454,6 +478,28 @@ func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonResponse(w, serials)
|
jsonResponse(w, serials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizePCIeSerialComponentName(p models.PCIeDevice) string {
|
||||||
|
className := strings.TrimSpace(p.DeviceClass)
|
||||||
|
part := strings.TrimSpace(p.PartNumber)
|
||||||
|
if part != "" && !strings.EqualFold(part, className) {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
lowerClass := strings.ToLower(className)
|
||||||
|
switch lowerClass {
|
||||||
|
case "display", "display controller", "3d controller", "vga", "network", "network controller", "pcie device", "other", "unknown", "":
|
||||||
|
if part != "" {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if className != "" {
|
||||||
|
return className
|
||||||
|
}
|
||||||
|
if part != "" {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
return "PCIe device"
|
||||||
|
}
|
||||||
|
|
||||||
func hasUsableSerial(serial string) bool {
|
func hasUsableSerial(serial string) bool {
|
||||||
s := strings.TrimSpace(serial)
|
s := strings.TrimSpace(serial)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
@@ -474,33 +520,70 @@ func (s *Server) handleGetFirmware(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jsonResponse(w, buildFirmwareEntries(result.Hardware))
|
||||||
|
}
|
||||||
|
|
||||||
|
type firmwareEntry struct {
|
||||||
|
Component string `json:"component"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFirmwareEntries(hw *models.HardwareConfig) []firmwareEntry {
|
||||||
|
if hw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicate firmware by extracting model name and version
|
// Deduplicate firmware by extracting model name and version
|
||||||
// E.g., "PSU0 (AP-CR3000F12BY)" and "PSU1 (AP-CR3000F12BY)" with same version -> one entry
|
// E.g., "PSU0 (AP-CR3000F12BY)" and "PSU1 (AP-CR3000F12BY)" with same version -> one entry
|
||||||
type FirmwareEntry struct {
|
|
||||||
Component string `json:"component"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
var deduplicated []FirmwareEntry
|
var deduplicated []firmwareEntry
|
||||||
|
|
||||||
for _, fw := range result.Hardware.Firmware {
|
appendEntry := func(component, model, version string) {
|
||||||
// Extract component type and model from device name
|
component = strings.TrimSpace(component)
|
||||||
component, model := extractFirmwareComponentAndModel(fw.DeviceName)
|
model = strings.TrimSpace(model)
|
||||||
key := component + "|" + model + "|" + fw.Version
|
version = strings.TrimSpace(version)
|
||||||
|
if component == "" || version == "" {
|
||||||
if !seen[key] {
|
return
|
||||||
seen[key] = true
|
|
||||||
deduplicated = append(deduplicated, FirmwareEntry{
|
|
||||||
Component: component,
|
|
||||||
Model: model,
|
|
||||||
Version: fw.Version,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
if model == "" {
|
||||||
|
model = "-"
|
||||||
|
}
|
||||||
|
key := component + "|" + model + "|" + version
|
||||||
|
if seen[key] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[key] = true
|
||||||
|
deduplicated = append(deduplicated, firmwareEntry{
|
||||||
|
Component: component,
|
||||||
|
Model: model,
|
||||||
|
Version: version,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResponse(w, deduplicated)
|
for _, fw := range hw.Firmware {
|
||||||
|
component, model := extractFirmwareComponentAndModel(fw.DeviceName)
|
||||||
|
appendEntry(component, model, fw.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for parsers that fill GPU firmware on device inventory only
|
||||||
|
// (e.g. runtime enrichment from redis/HGX) without explicit Hardware.Firmware entries.
|
||||||
|
for _, gpu := range hw.GPUs {
|
||||||
|
version := strings.TrimSpace(gpu.Firmware)
|
||||||
|
if version == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
model := strings.TrimSpace(gpu.PartNumber)
|
||||||
|
if model == "" {
|
||||||
|
model = strings.TrimSpace(gpu.Model)
|
||||||
|
}
|
||||||
|
if model == "" {
|
||||||
|
model = strings.TrimSpace(gpu.Slot)
|
||||||
|
}
|
||||||
|
appendEntry("GPU", model, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduplicated
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractFirmwareComponentAndModel extracts the component type and model from firmware device name
|
// extractFirmwareComponentAndModel extracts the component type and model from firmware device name
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
func TestExtractFirmwareComponentAndModel_GPUUsesPartNumberFromParentheses(t *testing.T) {
|
func TestExtractFirmwareComponentAndModel_GPUUsesPartNumberFromParentheses(t *testing.T) {
|
||||||
component, model := extractFirmwareComponentAndModel("GPU GPUSXM3 (692-2G520-0280-501)")
|
component, model := extractFirmwareComponentAndModel("GPU GPUSXM3 (692-2G520-0280-501)")
|
||||||
@@ -21,3 +25,40 @@ func TestExtractFirmwareComponentAndModel_GPUFallbackWithoutParentheses(t *testi
|
|||||||
t.Fatalf("expected GPU model 692-2G520-0280-501, got %q", model)
|
t.Fatalf("expected GPU model 692-2G520-0280-501, got %q", model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildFirmwareEntries_IncludesGPUFirmwareFallback(t *testing.T) {
|
||||||
|
hw := &models.HardwareConfig{
|
||||||
|
Firmware: []models.FirmwareInfo{
|
||||||
|
{DeviceName: "BIOS", Version: "1.0.0"},
|
||||||
|
},
|
||||||
|
GPUs: []models.GPU{
|
||||||
|
{
|
||||||
|
Slot: "#CPU0_PCIE2",
|
||||||
|
Model: "GH100 [H200 NVL]",
|
||||||
|
PartNumber: "699-2G530-0200-501",
|
||||||
|
Firmware: "96.00.B7.00.02",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slot: "#CPU0_PCIE1",
|
||||||
|
Model: "GH100 [H200 NVL]",
|
||||||
|
PartNumber: "699-2G530-0200-501",
|
||||||
|
Firmware: "96.00.B7.00.02",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := buildFirmwareEntries(hw)
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Fatalf("expected 2 deduplicated firmware entries, got %d", len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasGPU bool
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.Component == "GPU" && e.Version == "96.00.B7.00.02" {
|
||||||
|
hasGPU = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasGPU {
|
||||||
|
t.Fatalf("expected GPU firmware entry from hardware.gpus fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
20
internal/server/handlers_serials_test.go
Normal file
20
internal/server/handlers_serials_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizePCIeSerialComponentName_PrefersPartOverGenericClass(t *testing.T) {
|
||||||
|
got := normalizePCIeSerialComponentName(models.PCIeDevice{DeviceClass:"Display Controller", PartNumber:"GH100 [H200 NVL]"})
|
||||||
|
if got != "GH100 [H200 NVL]" {
|
||||||
|
t.Fatalf("expected part number, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizePCIeSerialComponentName_UsesClassWhenSpecific(t *testing.T) {
|
||||||
|
got := normalizePCIeSerialComponentName(models.PCIeDevice{DeviceClass:"I350 Gigabit Network Connection", PartNumber:"I350T4V2"})
|
||||||
|
if got != "I350T4V2" {
|
||||||
|
t.Fatalf("expected part number for readability, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
scripts/update-pci-ids.sh
Executable file
59
scripts/update-pci-ids.sh
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||||
|
OUT_FILE="$ROOT_DIR/internal/parser/vendors/pciids/pci.ids"
|
||||||
|
SUBMODULE_DIR="$ROOT_DIR/third_party/pciids"
|
||||||
|
SUBMODULE_FILE="$SUBMODULE_DIR/pci.ids"
|
||||||
|
BEST_EFFORT=0
|
||||||
|
SYNC_SUBMODULE=0
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--best-effort)
|
||||||
|
BEST_EFFORT=1
|
||||||
|
;;
|
||||||
|
--sync-submodule)
|
||||||
|
SYNC_SUBMODULE=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "error: unknown argument: $1" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
run_or_warn() {
|
||||||
|
if "$@"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ "$BEST_EFFORT" -eq 1 ] && [ -f "$OUT_FILE" ]; then
|
||||||
|
echo "warning: command failed: $*; keeping existing pci.ids" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$SYNC_SUBMODULE" -eq 1 ]; then
|
||||||
|
run_or_warn git -C "$ROOT_DIR" submodule update --init --remote "$SUBMODULE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s "$SUBMODULE_FILE" ]; then
|
||||||
|
if [ "$BEST_EFFORT" -eq 1 ] && [ -f "$OUT_FILE" ]; then
|
||||||
|
echo "warning: missing submodule pci.ids; keeping existing file" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "error: missing $SUBMODULE_FILE (run: git submodule update --init --remote third_party/pciids)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$OUT_FILE")"
|
||||||
|
|
||||||
|
if [ -f "$OUT_FILE" ] && cmp -s "$SUBMODULE_FILE" "$OUT_FILE"; then
|
||||||
|
echo "pci.ids is already up to date"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$SUBMODULE_FILE" "$OUT_FILE"
|
||||||
|
echo "updated $OUT_FILE from submodule $SUBMODULE_DIR"
|
||||||
1
third_party/pciids
vendored
Submodule
1
third_party/pciids
vendored
Submodule
Submodule third_party/pciids added at 82b1a68f47
@@ -834,10 +834,29 @@ function renderConfig(data) {
|
|||||||
|
|
||||||
// GPU tab
|
// GPU tab
|
||||||
html += '<div class="config-tab-content" id="config-gpu">';
|
html += '<div class="config-tab-content" id="config-gpu">';
|
||||||
if (config.gpus && config.gpus.length > 0) {
|
const gpuRows = (config.gpus && config.gpus.length > 0)
|
||||||
const gpuCount = config.gpus.length;
|
? config.gpus
|
||||||
const gpuModel = config.gpus[0].model || '-';
|
: (config.pcie_devices || [])
|
||||||
const gpuVendor = config.gpus[0].manufacturer || '-';
|
.filter((p) => {
|
||||||
|
const cls = String(p.device_class || '').toLowerCase();
|
||||||
|
const mfr = String(p.manufacturer || '').toLowerCase();
|
||||||
|
return cls.includes('gpu') || cls.includes('display') || cls.includes('3d') || mfr.includes('nvidia') || p.vendor_id === 0x10de;
|
||||||
|
})
|
||||||
|
.map((p) => ({
|
||||||
|
slot: p.slot,
|
||||||
|
model: p.part_number || p.device_class,
|
||||||
|
manufacturer: p.manufacturer,
|
||||||
|
bdf: p.bdf,
|
||||||
|
serial_number: p.serial_number,
|
||||||
|
current_link_width: p.link_width,
|
||||||
|
current_link_speed: p.link_speed,
|
||||||
|
max_link_width: p.max_link_width,
|
||||||
|
max_link_speed: p.max_link_speed
|
||||||
|
}));
|
||||||
|
if (gpuRows.length > 0) {
|
||||||
|
const gpuCount = gpuRows.length;
|
||||||
|
const gpuModel = gpuRows[0].model || '-';
|
||||||
|
const gpuVendor = gpuRows[0].manufacturer || '-';
|
||||||
html += `<h3>Графические процессоры</h3>
|
html += `<h3>Графические процессоры</h3>
|
||||||
<div class="section-overview">
|
<div class="section-overview">
|
||||||
<div class="stat-box"><span class="stat-value">${gpuCount}</span><span class="stat-label">Всего GPU</span></div>
|
<div class="stat-box"><span class="stat-value">${gpuCount}</span><span class="stat-label">Всего GPU</span></div>
|
||||||
@@ -845,7 +864,7 @@ function renderConfig(data) {
|
|||||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(gpuModel)}</span><span class="stat-label">Модель</span></div>
|
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(gpuModel)}</span><span class="stat-label">Модель</span></div>
|
||||||
</div>
|
</div>
|
||||||
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>`;
|
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||||
config.gpus.forEach(gpu => {
|
gpuRows.forEach(gpu => {
|
||||||
const pcieLink = formatPCIeLink(
|
const pcieLink = formatPCIeLink(
|
||||||
gpu.current_link_width || gpu.link_width,
|
gpu.current_link_width || gpu.link_width,
|
||||||
gpu.current_link_speed || gpu.link_speed,
|
gpu.current_link_speed || gpu.link_speed,
|
||||||
@@ -869,11 +888,27 @@ function renderConfig(data) {
|
|||||||
|
|
||||||
// Network tab
|
// Network tab
|
||||||
html += '<div class="config-tab-content" id="config-network">';
|
html += '<div class="config-tab-content" id="config-network">';
|
||||||
if (config.network_adapters && config.network_adapters.length > 0) {
|
const networkRows = (config.network_adapters && config.network_adapters.length > 0)
|
||||||
const nicCount = config.network_adapters.length;
|
? config.network_adapters
|
||||||
const totalPorts = config.network_adapters.reduce((sum, n) => sum + (n.port_count || 0), 0);
|
: (config.pcie_devices || [])
|
||||||
const nicTypes = [...new Set(config.network_adapters.map(n => n.port_type).filter(t => t))];
|
.filter((p) => {
|
||||||
const nicModels = [...new Set(config.network_adapters.map(n => n.model).filter(m => m))];
|
const cls = String(p.device_class || '').toLowerCase();
|
||||||
|
return cls.includes('network') || cls.includes('ethernet') || cls.includes('gigabit');
|
||||||
|
})
|
||||||
|
.map((p) => ({
|
||||||
|
location: p.slot,
|
||||||
|
model: p.part_number || p.device_class,
|
||||||
|
vendor: p.manufacturer,
|
||||||
|
port_count: 0,
|
||||||
|
port_type: '',
|
||||||
|
mac_addresses: p.mac_addresses || [],
|
||||||
|
status: p.status || ''
|
||||||
|
}));
|
||||||
|
if (networkRows.length > 0) {
|
||||||
|
const nicCount = networkRows.length;
|
||||||
|
const totalPorts = networkRows.reduce((sum, n) => sum + (n.port_count || 0), 0);
|
||||||
|
const nicTypes = [...new Set(networkRows.map(n => n.port_type).filter(t => t))];
|
||||||
|
const nicModels = [...new Set(networkRows.map(n => n.model).filter(m => m))];
|
||||||
html += `<h3>Сетевые адаптеры</h3>
|
html += `<h3>Сетевые адаптеры</h3>
|
||||||
<div class="section-overview">
|
<div class="section-overview">
|
||||||
<div class="stat-box"><span class="stat-value">${nicCount}</span><span class="stat-label">Адаптеров</span></div>
|
<div class="stat-box"><span class="stat-value">${nicCount}</span><span class="stat-label">Адаптеров</span></div>
|
||||||
@@ -882,7 +917,7 @@ function renderConfig(data) {
|
|||||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(nicModels.join(', ') || '-')}</span><span class="stat-label">Модели</span></div>
|
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(nicModels.join(', ') || '-')}</span><span class="stat-label">Модели</span></div>
|
||||||
</div>
|
</div>
|
||||||
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>`;
|
<table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>`;
|
||||||
config.network_adapters.forEach(nic => {
|
networkRows.forEach(nic => {
|
||||||
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
|
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
|
||||||
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
|
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
@@ -906,22 +941,40 @@ function renderConfig(data) {
|
|||||||
const hasPCIe = config.pcie_devices && config.pcie_devices.length > 0;
|
const hasPCIe = config.pcie_devices && config.pcie_devices.length > 0;
|
||||||
const hasGPUs = config.gpus && config.gpus.length > 0;
|
const hasGPUs = config.gpus && config.gpus.length > 0;
|
||||||
if (hasPCIe || hasGPUs) {
|
if (hasPCIe || hasGPUs) {
|
||||||
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Тип</th><th>Модель</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th></tr></thead><tbody>';
|
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Модель</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th><th>Серийный номер</th><th>Прошивка</th></tr></thead><tbody>';
|
||||||
|
const pcieRowKey = (slot, bdf, vendorId, deviceId) => {
|
||||||
|
const normalizedBDF = (bdf || '').trim().toLowerCase();
|
||||||
|
if (normalizedBDF) return `bdf:${normalizedBDF}`;
|
||||||
|
const normalizedSlot = (slot || '').trim().toLowerCase();
|
||||||
|
if (normalizedSlot) return `slot:${normalizedSlot}`;
|
||||||
|
return `id:${vendorId || 0}:${deviceId || 0}`;
|
||||||
|
};
|
||||||
|
const gpuByKey = new Map();
|
||||||
|
(config.gpus || []).forEach(gpu => {
|
||||||
|
gpuByKey.set(pcieRowKey(gpu.slot, gpu.bdf, gpu.vendor_id, gpu.device_id), gpu);
|
||||||
|
});
|
||||||
|
|
||||||
(config.pcie_devices || []).forEach(p => {
|
(config.pcie_devices || []).forEach(p => {
|
||||||
|
const key = pcieRowKey(p.slot, p.bdf, p.vendor_id, p.device_id);
|
||||||
|
const matchedGPU = gpuByKey.get(key);
|
||||||
|
|
||||||
const pcieLink = formatPCIeLink(
|
const pcieLink = formatPCIeLink(
|
||||||
p.link_width,
|
p.link_width,
|
||||||
p.link_speed,
|
p.link_speed,
|
||||||
p.max_link_width,
|
p.max_link_width,
|
||||||
p.max_link_speed
|
p.max_link_speed
|
||||||
);
|
);
|
||||||
|
const serial = p.serial_number || (matchedGPU ? matchedGPU.serial_number : '');
|
||||||
|
const firmware = p.firmware || (matchedGPU ? matchedGPU.firmware : '') || findPCIeFirmwareVersion(config.firmware, p);
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(p.slot || '-')}</td>
|
<td>${escapeHtml(p.slot || '-')}</td>
|
||||||
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
||||||
<td>${escapeHtml(p.device_class || '-')}</td>
|
|
||||||
<td>${escapeHtml(p.part_number || '-')}</td>
|
<td>${escapeHtml(p.part_number || '-')}</td>
|
||||||
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
||||||
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
||||||
<td>${pcieLink}</td>
|
<td>${pcieLink}</td>
|
||||||
|
<td><code>${escapeHtml(serial || '-')}</code></td>
|
||||||
|
<td><code>${escapeHtml(firmware || '-')}</code></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -935,11 +988,12 @@ function renderConfig(data) {
|
|||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(gpu.slot || '-')}</td>
|
<td>${escapeHtml(gpu.slot || '-')}</td>
|
||||||
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
|
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
|
||||||
<td>GPU</td>
|
|
||||||
<td>${escapeHtml(gpu.model || gpu.part_number || '-')}</td>
|
<td>${escapeHtml(gpu.model || gpu.part_number || '-')}</td>
|
||||||
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
|
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
|
||||||
<td><code>${gpu.vendor_id ? gpu.vendor_id.toString(16) : '-'}:${gpu.device_id ? gpu.device_id.toString(16) : '-'}</code></td>
|
<td><code>${gpu.vendor_id ? gpu.vendor_id.toString(16) : '-'}:${gpu.device_id ? gpu.device_id.toString(16) : '-'}</code></td>
|
||||||
<td>${pcieLink}</td>
|
<td>${pcieLink}</td>
|
||||||
|
<td><code>${escapeHtml(gpu.serial_number || '-')}</code></td>
|
||||||
|
<td><code>${escapeHtml(gpu.firmware || '-')}</code></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
@@ -1229,6 +1283,24 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findPCIeFirmwareVersion(firmwareEntries, pcieDevice) {
|
||||||
|
if (!Array.isArray(firmwareEntries) || !pcieDevice) return '';
|
||||||
|
|
||||||
|
const slot = (pcieDevice.slot || '').trim().toLowerCase();
|
||||||
|
const model = (pcieDevice.part_number || '').trim().toLowerCase();
|
||||||
|
if (!slot && !model) return '';
|
||||||
|
|
||||||
|
for (const fw of firmwareEntries) {
|
||||||
|
const name = (fw.device_name || '').trim().toLowerCase();
|
||||||
|
const version = (fw.version || '').trim();
|
||||||
|
if (!name || !version) continue;
|
||||||
|
if (slot && name.includes(slot)) return version;
|
||||||
|
if (model && name.includes(model)) return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function formatPCIeLink(currentWidth, currentSpeed, maxWidth, maxSpeed) {
|
function formatPCIeLink(currentWidth, currentSpeed, maxWidth, maxSpeed) {
|
||||||
// Helper to convert speed to generation
|
// Helper to convert speed to generation
|
||||||
function speedToGen(speed) {
|
function speedToGen(speed) {
|
||||||
|
|||||||
Reference in New Issue
Block a user