Features: - Modular parser architecture for vendor-specific formats - Inspur/Kaytus parser supporting asset.json, devicefrusdr.log, component.log, idl.log, and syslog files - PCI Vendor/Device ID lookup for hardware identification - Web interface with tabs: Events, Sensors, Config, Serials, Firmware - Server specification summary with component grouping - Export to CSV, JSON, TXT formats - BMC alarm parsing from IDL logs (memory errors, PSU events, etc.) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
462 lines
12 KiB
Go
462 lines
12 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/exporter"
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
|
)
|
|
|
|
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
tmplContent, err := WebFS.ReadFile("templates/index.html")
|
|
if err != nil {
|
|
http.Error(w, "Template not found", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
tmpl, err := template.New("index").Parse(string(tmplContent))
|
|
if err != nil {
|
|
http.Error(w, "Template parse error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
tmpl.Execute(w, nil)
|
|
}
|
|
|
|
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
|
// Max 100MB file
|
|
if err := r.ParseMultipartForm(100 << 20); err != nil {
|
|
jsonError(w, "File too large", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
file, header, err := r.FormFile("archive")
|
|
if err != nil {
|
|
jsonError(w, "Failed to read file", http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Parse archive
|
|
p := parser.NewBMCParser()
|
|
if err := p.ParseFromReader(file, header.Filename); err != nil {
|
|
jsonError(w, "Failed to parse archive: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
result := p.Result()
|
|
s.SetResult(result)
|
|
s.SetDetectedVendor(p.DetectedVendor())
|
|
|
|
jsonResponse(w, map[string]interface{}{
|
|
"status": "ok",
|
|
"message": "File uploaded and parsed successfully",
|
|
"filename": header.Filename,
|
|
"vendor": p.DetectedVendor(),
|
|
"stats": map[string]int{
|
|
"events": len(result.Events),
|
|
"sensors": len(result.Sensors),
|
|
"fru": len(result.FRU),
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleGetParsers(w http.ResponseWriter, r *http.Request) {
|
|
jsonResponse(w, map[string]interface{}{
|
|
"parsers": parser.ListParsers(),
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleGetEvents(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
if result == nil {
|
|
jsonResponse(w, []interface{}{})
|
|
return
|
|
}
|
|
jsonResponse(w, result.Events)
|
|
}
|
|
|
|
func (s *Server) handleGetSensors(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
if result == nil {
|
|
jsonResponse(w, []interface{}{})
|
|
return
|
|
}
|
|
jsonResponse(w, result.Sensors)
|
|
}
|
|
|
|
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
if result == nil || result.Hardware == nil {
|
|
jsonResponse(w, map[string]interface{}{})
|
|
return
|
|
}
|
|
|
|
// Build specification summary
|
|
spec := buildSpecification(result)
|
|
|
|
jsonResponse(w, map[string]interface{}{
|
|
"hardware": result.Hardware,
|
|
"specification": spec,
|
|
})
|
|
}
|
|
|
|
// SpecLine represents a single line in specification
|
|
type SpecLine struct {
|
|
Category string `json:"category"`
|
|
Name string `json:"name"`
|
|
Quantity int `json:"quantity"`
|
|
}
|
|
|
|
func buildSpecification(result *models.AnalysisResult) []SpecLine {
|
|
var spec []SpecLine
|
|
hw := result.Hardware
|
|
if hw == nil {
|
|
return spec
|
|
}
|
|
|
|
// CPUs - group by model
|
|
cpuGroups := make(map[string]int)
|
|
cpuDetails := make(map[string]models.CPU)
|
|
for _, cpu := range hw.CPUs {
|
|
cpuGroups[cpu.Model]++
|
|
cpuDetails[cpu.Model] = cpu
|
|
}
|
|
for model, count := range cpuGroups {
|
|
cpu := cpuDetails[model]
|
|
name := fmt.Sprintf("Intel %s (%.1fGHz %dC %dW)",
|
|
model,
|
|
float64(cpu.FrequencyMHz)/1000,
|
|
cpu.Cores,
|
|
cpu.TDP)
|
|
spec = append(spec, SpecLine{Category: "Процессор", Name: name, Quantity: count})
|
|
}
|
|
|
|
// Memory - group by size and type
|
|
memGroups := make(map[string]int)
|
|
for _, mem := range hw.Memory {
|
|
key := fmt.Sprintf("%s %dGB", mem.Type, mem.SizeMB/1024)
|
|
memGroups[key]++
|
|
}
|
|
for key, count := range memGroups {
|
|
spec = append(spec, SpecLine{Category: "Память", Name: key, Quantity: count})
|
|
}
|
|
|
|
// Storage - group by type and capacity
|
|
storGroups := make(map[string]int)
|
|
for _, stor := range hw.Storage {
|
|
var key string
|
|
if stor.SizeGB >= 1000 {
|
|
key = fmt.Sprintf("%s %s %.2fTB", stor.Type, stor.Interface, float64(stor.SizeGB)/1000)
|
|
} else {
|
|
key = fmt.Sprintf("%s %s %dGB", stor.Type, stor.Interface, stor.SizeGB)
|
|
}
|
|
storGroups[key]++
|
|
}
|
|
for key, count := range storGroups {
|
|
spec = append(spec, SpecLine{Category: "Накопитель", Name: key, Quantity: count})
|
|
}
|
|
|
|
// PCIe devices - group by device class/name and manufacturer
|
|
pcieGroups := make(map[string]int)
|
|
pcieDetails := make(map[string]models.PCIeDevice)
|
|
for _, pcie := range hw.PCIeDevices {
|
|
// Create unique key from manufacturer + device class/name
|
|
key := pcie.DeviceClass
|
|
if pcie.Manufacturer != "" {
|
|
key = pcie.Manufacturer + " " + pcie.DeviceClass
|
|
}
|
|
if pcie.PartNumber != "" && pcie.PartNumber != pcie.DeviceClass {
|
|
key = key + " (" + pcie.PartNumber + ")"
|
|
}
|
|
pcieGroups[key]++
|
|
pcieDetails[key] = pcie
|
|
}
|
|
for key, count := range pcieGroups {
|
|
pcie := pcieDetails[key]
|
|
category := "PCIe устройство"
|
|
name := key
|
|
|
|
// Determine category based on device class or known GPU names
|
|
deviceClass := pcie.DeviceClass
|
|
isGPU := isGPUDevice(deviceClass)
|
|
isNetwork := deviceClass == "Network" || strings.Contains(deviceClass, "ConnectX")
|
|
|
|
if isGPU {
|
|
category = "Графический процессор"
|
|
} else if isNetwork {
|
|
category = "Сетевой адаптер"
|
|
} else if deviceClass == "NVMe" || deviceClass == "RAID" || deviceClass == "SAS" || deviceClass == "SATA" || deviceClass == "Storage" {
|
|
category = "Контроллер"
|
|
}
|
|
|
|
spec = append(spec, SpecLine{Category: category, Name: name, Quantity: count})
|
|
}
|
|
|
|
// Power supplies - group by model/wattage
|
|
psuGroups := make(map[string]int)
|
|
for _, psu := range hw.PowerSupply {
|
|
key := psu.Model
|
|
if key == "" && psu.WattageW > 0 {
|
|
key = fmt.Sprintf("%dW", psu.WattageW)
|
|
}
|
|
if key != "" {
|
|
psuGroups[key]++
|
|
}
|
|
}
|
|
for key, count := range psuGroups {
|
|
spec = append(spec, SpecLine{Category: "Блок питания", Name: key, Quantity: count})
|
|
}
|
|
|
|
return spec
|
|
}
|
|
|
|
func (s *Server) handleGetSerials(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
if result == nil {
|
|
jsonResponse(w, []interface{}{})
|
|
return
|
|
}
|
|
|
|
// Collect all serial numbers from various sources
|
|
type SerialEntry struct {
|
|
Component string `json:"component"`
|
|
SerialNumber string `json:"serial_number"`
|
|
Manufacturer string `json:"manufacturer,omitempty"`
|
|
PartNumber string `json:"part_number,omitempty"`
|
|
Category string `json:"category"`
|
|
}
|
|
|
|
var serials []SerialEntry
|
|
|
|
// From FRU
|
|
for _, fru := range result.FRU {
|
|
if fru.SerialNumber == "" {
|
|
continue
|
|
}
|
|
name := fru.ProductName
|
|
if name == "" {
|
|
name = fru.Description
|
|
}
|
|
serials = append(serials, SerialEntry{
|
|
Component: name,
|
|
SerialNumber: fru.SerialNumber,
|
|
Manufacturer: fru.Manufacturer,
|
|
PartNumber: fru.PartNumber,
|
|
Category: "FRU",
|
|
})
|
|
}
|
|
|
|
// From Hardware
|
|
if result.Hardware != nil {
|
|
// Board
|
|
if result.Hardware.BoardInfo.SerialNumber != "" {
|
|
serials = append(serials, SerialEntry{
|
|
Component: result.Hardware.BoardInfo.ProductName,
|
|
SerialNumber: result.Hardware.BoardInfo.SerialNumber,
|
|
Manufacturer: result.Hardware.BoardInfo.Manufacturer,
|
|
PartNumber: result.Hardware.BoardInfo.PartNumber,
|
|
Category: "Board",
|
|
})
|
|
}
|
|
|
|
// CPUs
|
|
for _, cpu := range result.Hardware.CPUs {
|
|
sn := cpu.SerialNumber
|
|
if sn == "" {
|
|
sn = cpu.PPIN // Use PPIN as fallback identifier
|
|
}
|
|
if sn == "" {
|
|
continue
|
|
}
|
|
serials = append(serials, SerialEntry{
|
|
Component: cpu.Model,
|
|
SerialNumber: sn,
|
|
Category: "CPU",
|
|
})
|
|
}
|
|
|
|
// Memory DIMMs
|
|
for _, mem := range result.Hardware.Memory {
|
|
if mem.SerialNumber == "" {
|
|
continue
|
|
}
|
|
serials = append(serials, SerialEntry{
|
|
Component: mem.PartNumber,
|
|
SerialNumber: mem.SerialNumber,
|
|
Manufacturer: mem.Manufacturer,
|
|
PartNumber: mem.PartNumber,
|
|
Category: "Memory",
|
|
})
|
|
}
|
|
|
|
// Storage
|
|
for _, stor := range result.Hardware.Storage {
|
|
if stor.SerialNumber == "" {
|
|
continue
|
|
}
|
|
serials = append(serials, SerialEntry{
|
|
Component: stor.Model,
|
|
SerialNumber: stor.SerialNumber,
|
|
Manufacturer: stor.Manufacturer,
|
|
PartNumber: stor.Slot,
|
|
Category: "Storage",
|
|
})
|
|
}
|
|
|
|
// PCIe devices
|
|
for _, pcie := range result.Hardware.PCIeDevices {
|
|
if pcie.SerialNumber == "" {
|
|
continue
|
|
}
|
|
serials = append(serials, SerialEntry{
|
|
Component: pcie.DeviceClass + " (" + pcie.Slot + ")",
|
|
SerialNumber: pcie.SerialNumber,
|
|
Manufacturer: pcie.Manufacturer,
|
|
PartNumber: pcie.PartNumber,
|
|
Category: "PCIe",
|
|
})
|
|
}
|
|
|
|
// Network cards
|
|
for _, nic := range result.Hardware.NetworkCards {
|
|
if nic.SerialNumber == "" {
|
|
continue
|
|
}
|
|
serials = append(serials, SerialEntry{
|
|
Component: nic.Model,
|
|
SerialNumber: nic.SerialNumber,
|
|
Category: "Network",
|
|
})
|
|
}
|
|
|
|
// Power supplies
|
|
for _, psu := range result.Hardware.PowerSupply {
|
|
if psu.SerialNumber == "" {
|
|
continue
|
|
}
|
|
serials = append(serials, SerialEntry{
|
|
Component: psu.Model,
|
|
SerialNumber: psu.SerialNumber,
|
|
PartNumber: psu.Slot,
|
|
Category: "PSU",
|
|
})
|
|
}
|
|
}
|
|
|
|
jsonResponse(w, serials)
|
|
}
|
|
|
|
func (s *Server) handleGetFirmware(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
if result == nil || result.Hardware == nil {
|
|
jsonResponse(w, []interface{}{})
|
|
return
|
|
}
|
|
jsonResponse(w, result.Hardware.Firmware)
|
|
}
|
|
|
|
func (s *Server) handleGetStatus(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
if result == nil {
|
|
jsonResponse(w, map[string]interface{}{
|
|
"loaded": false,
|
|
})
|
|
return
|
|
}
|
|
|
|
jsonResponse(w, map[string]interface{}{
|
|
"loaded": true,
|
|
"filename": result.Filename,
|
|
"vendor": s.GetDetectedVendor(),
|
|
"stats": map[string]int{
|
|
"events": len(result.Events),
|
|
"sensors": len(result.Sensors),
|
|
"fru": len(result.FRU),
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleExportCSV(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
|
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=serials.csv")
|
|
|
|
exp := exporter.New(result)
|
|
exp.ExportCSV(w)
|
|
}
|
|
|
|
func (s *Server) handleExportJSON(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=report.json")
|
|
|
|
exp := exporter.New(result)
|
|
exp.ExportJSON(w)
|
|
}
|
|
|
|
func (s *Server) handleExportTXT(w http.ResponseWriter, r *http.Request) {
|
|
result := s.GetResult()
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", "attachment; filename=report.txt")
|
|
|
|
exp := exporter.New(result)
|
|
exp.ExportTXT(w)
|
|
}
|
|
|
|
func (s *Server) handleClear(w http.ResponseWriter, r *http.Request) {
|
|
s.SetResult(nil)
|
|
s.SetDetectedVendor("")
|
|
jsonResponse(w, map[string]string{
|
|
"status": "ok",
|
|
"message": "Data cleared",
|
|
})
|
|
}
|
|
|
|
func jsonResponse(w http.ResponseWriter, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
func jsonError(w http.ResponseWriter, message string, code int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
|
}
|
|
|
|
// isGPUDevice checks if device class indicates a GPU
|
|
func isGPUDevice(deviceClass string) bool {
|
|
// Standard PCI class names
|
|
if deviceClass == "VGA" || deviceClass == "3D Controller" || deviceClass == "Display" {
|
|
return true
|
|
}
|
|
// Known GPU model patterns
|
|
gpuPatterns := []string{
|
|
"L40", "A100", "A10", "A16", "A30", "H100", "H200", "V100",
|
|
"RTX", "GTX", "Quadro", "Tesla",
|
|
"Instinct", "Radeon",
|
|
"AST2500", "AST2600", // ASPEED BMC VGA
|
|
}
|
|
upperClass := strings.ToUpper(deviceClass)
|
|
for _, pattern := range gpuPatterns {
|
|
if strings.Contains(upperClass, strings.ToUpper(pattern)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|