Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c6daa1c5e | ||
|
|
e420888d71 | ||
|
|
8149360410 | ||
|
|
4262c5b798 | ||
|
|
b2e177af31 |
@@ -174,15 +174,19 @@ func cleanDMIValue(v string) string {
|
|||||||
upper := strings.ToUpper(v)
|
upper := strings.ToUpper(v)
|
||||||
placeholders := []string{
|
placeholders := []string{
|
||||||
"TO BE FILLED BY O.E.M.",
|
"TO BE FILLED BY O.E.M.",
|
||||||
|
"TO BE FILLED BY O.E.M",
|
||||||
"NOT SPECIFIED",
|
"NOT SPECIFIED",
|
||||||
"NOT SETTABLE",
|
"NOT SETTABLE",
|
||||||
"NOT PRESENT",
|
"NOT PRESENT",
|
||||||
|
"NOT AVAILABLE",
|
||||||
"UNKNOWN",
|
"UNKNOWN",
|
||||||
"N/A",
|
"N/A",
|
||||||
"NONE",
|
"NONE",
|
||||||
"NULL",
|
"NULL",
|
||||||
"DEFAULT STRING",
|
"DEFAULT STRING",
|
||||||
"0",
|
"0",
|
||||||
|
"0123456789",
|
||||||
|
"1234567890",
|
||||||
}
|
}
|
||||||
for _, p := range placeholders {
|
for _, p := range placeholders {
|
||||||
if upper == p {
|
if upper == p {
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ func TestCleanDMIValue(t *testing.T) {
|
|||||||
{" Inspur ", "Inspur"},
|
{" Inspur ", "Inspur"},
|
||||||
{"", ""},
|
{"", ""},
|
||||||
{"0", ""},
|
{"0", ""},
|
||||||
|
{"0123456789", ""},
|
||||||
|
{"1234567890", ""},
|
||||||
|
{"Not Available", ""},
|
||||||
|
{"To Be Filled By O.E.M", ""},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := cleanDMIValue(tt.input)
|
got := cleanDMIValue(tt.input)
|
||||||
@@ -109,6 +113,80 @@ func TestParseDMIFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseBoard_Dell(t *testing.T) {
|
||||||
|
type1 := mustReadFile(t, "testdata/dmidecode_type1_dell.txt")
|
||||||
|
type2 := mustReadFile(t, "testdata/dmidecode_type2_dell.txt")
|
||||||
|
|
||||||
|
board := parseBoard(type1, type2)
|
||||||
|
|
||||||
|
if board.SerialNumber != "7SG9F63" {
|
||||||
|
t.Errorf("serial_number: got %q, want %q", board.SerialNumber, "7SG9F63")
|
||||||
|
}
|
||||||
|
if board.Manufacturer == nil || *board.Manufacturer != "Dell Inc." {
|
||||||
|
t.Errorf("manufacturer: got %v, want Dell Inc.", board.Manufacturer)
|
||||||
|
}
|
||||||
|
if board.ProductName == nil || *board.ProductName != "PowerEdge R740xd" {
|
||||||
|
t.Errorf("product_name: got %v, want PowerEdge R740xd", board.ProductName)
|
||||||
|
}
|
||||||
|
// part number comes from type2 Product Name
|
||||||
|
if board.PartNumber == nil || *board.PartNumber != "0F9N89" {
|
||||||
|
t.Errorf("part_number: got %v, want 0F9N89", board.PartNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBoard_HPE(t *testing.T) {
|
||||||
|
type1 := mustReadFile(t, "testdata/dmidecode_type1_hpe.txt")
|
||||||
|
type2 := mustReadFile(t, "testdata/dmidecode_type2_hpe.txt")
|
||||||
|
|
||||||
|
board := parseBoard(type1, type2)
|
||||||
|
|
||||||
|
if board.SerialNumber != "CZJ9320CXN" {
|
||||||
|
t.Errorf("serial_number: got %q, want %q", board.SerialNumber, "CZJ9320CXN")
|
||||||
|
}
|
||||||
|
if board.Manufacturer == nil || *board.Manufacturer != "HPE" {
|
||||||
|
t.Errorf("manufacturer: got %v, want HPE", board.Manufacturer)
|
||||||
|
}
|
||||||
|
if board.ProductName == nil || *board.ProductName != "ProLiant DL380 Gen10" {
|
||||||
|
t.Errorf("product_name: got %v, want ProLiant DL380 Gen10", board.ProductName)
|
||||||
|
}
|
||||||
|
if board.PartNumber == nil || *board.PartNumber != "ProLiant DL380 Gen10" {
|
||||||
|
t.Errorf("part_number: got %v, want ProLiant DL380 Gen10", board.PartNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBoard_Supermicro_Placeholders(t *testing.T) {
|
||||||
|
type1 := mustReadFile(t, "testdata/dmidecode_type1_supermicro.txt")
|
||||||
|
type2 := mustReadFile(t, "testdata/dmidecode_type2_supermicro.txt")
|
||||||
|
|
||||||
|
board := parseBoard(type1, type2)
|
||||||
|
|
||||||
|
if board.SerialNumber != "S214726X2A36789" {
|
||||||
|
t.Errorf("serial_number: got %q, want %q", board.SerialNumber, "S214726X2A36789")
|
||||||
|
}
|
||||||
|
if board.Manufacturer == nil || *board.Manufacturer != "Supermicro" {
|
||||||
|
t.Errorf("manufacturer: got %v, want Supermicro", board.Manufacturer)
|
||||||
|
}
|
||||||
|
if board.ProductName == nil || *board.ProductName != "SYS-6028R-WTR" {
|
||||||
|
t.Errorf("product_name: got %v, want SYS-6028R-WTR", board.ProductName)
|
||||||
|
}
|
||||||
|
// "X10DRW-i" is the real part number from type 2
|
||||||
|
if board.PartNumber == nil || *board.PartNumber != "X10DRW-i" {
|
||||||
|
t.Errorf("part_number: got %v, want X10DRW-i", board.PartNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBIOSFirmware_Dell(t *testing.T) {
|
||||||
|
type0 := mustReadFile(t, "testdata/dmidecode_type0_dell.txt")
|
||||||
|
fw := parseBIOSFirmware(type0)
|
||||||
|
|
||||||
|
if len(fw) != 1 {
|
||||||
|
t.Fatalf("expected 1 firmware record, got %d", len(fw))
|
||||||
|
}
|
||||||
|
if fw[0].Version != "2.5.4" {
|
||||||
|
t.Errorf("version: got %q, want 2.5.4", fw[0].Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustReadFile(t *testing.T, path string) string {
|
func mustReadFile(t *testing.T, path string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
b, err := os.ReadFile(path)
|
b, err := os.ReadFile(path)
|
||||||
|
|||||||
87
audit/internal/collector/memory_test.go
Normal file
87
audit/internal/collector/memory_test.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseMemory_Mixed(t *testing.T) {
|
||||||
|
out := mustReadFile(t, "testdata/dmidecode_type17_mixed.txt")
|
||||||
|
dimms := parseMemory(out)
|
||||||
|
|
||||||
|
if len(dimms) != 3 {
|
||||||
|
t.Fatalf("expected 3 DIMMs, got %d", len(dimms))
|
||||||
|
}
|
||||||
|
|
||||||
|
// slot 0: populated, 16 GB Supermicro-style
|
||||||
|
d0 := dimms[0]
|
||||||
|
if d0.Present == nil || !*d0.Present {
|
||||||
|
t.Errorf("dimm0: expected present=true")
|
||||||
|
}
|
||||||
|
if d0.SizeMB == nil || *d0.SizeMB != 16384 {
|
||||||
|
t.Errorf("dimm0: size_mb=%v, want 16384", d0.SizeMB)
|
||||||
|
}
|
||||||
|
if d0.Slot == nil || *d0.Slot != "P1-DIMMA1" {
|
||||||
|
t.Errorf("dimm0: slot=%v, want P1-DIMMA1", d0.Slot)
|
||||||
|
}
|
||||||
|
if d0.Location == nil || *d0.Location != "P0_Node0_Channel0_Dimm0" {
|
||||||
|
t.Errorf("dimm0: location=%v, want P0_Node0_Channel0_Dimm0", d0.Location)
|
||||||
|
}
|
||||||
|
if d0.Manufacturer == nil || *d0.Manufacturer != "Micron" {
|
||||||
|
t.Errorf("dimm0: manufacturer=%v, want Micron", d0.Manufacturer)
|
||||||
|
}
|
||||||
|
if d0.PartNumber == nil || *d0.PartNumber != "36ASF2G72PZ-2G1A2" {
|
||||||
|
t.Errorf("dimm0: part_number=%v, want 36ASF2G72PZ-2G1A2", d0.PartNumber)
|
||||||
|
}
|
||||||
|
if d0.MaxSpeedMHz == nil || *d0.MaxSpeedMHz != 2133 {
|
||||||
|
t.Errorf("dimm0: max_speed_mhz=%v, want 2133", d0.MaxSpeedMHz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// slot 1: empty
|
||||||
|
d1 := dimms[1]
|
||||||
|
if d1.Present == nil || *d1.Present {
|
||||||
|
t.Errorf("dimm1: expected present=false")
|
||||||
|
}
|
||||||
|
if d1.Status == nil || *d1.Status != statusEmpty {
|
||||||
|
t.Errorf("dimm1: status=%v, want %s", d1.Status, statusEmpty)
|
||||||
|
}
|
||||||
|
if d1.SizeMB != nil {
|
||||||
|
t.Errorf("dimm1: size_mb should be nil for empty slot, got %v", d1.SizeMB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// slot 2: populated, 32768 MB Dell-style size
|
||||||
|
d2 := dimms[2]
|
||||||
|
if d2.Present == nil || !*d2.Present {
|
||||||
|
t.Errorf("dimm2: expected present=true")
|
||||||
|
}
|
||||||
|
if d2.SizeMB == nil || *d2.SizeMB != 32768 {
|
||||||
|
t.Errorf("dimm2: size_mb=%v, want 32768", d2.SizeMB)
|
||||||
|
}
|
||||||
|
if d2.Manufacturer == nil || *d2.Manufacturer != "Samsung" {
|
||||||
|
t.Errorf("dimm2: manufacturer=%v, want Samsung", d2.Manufacturer)
|
||||||
|
}
|
||||||
|
if d2.CurrentSpeedMHz == nil || *d2.CurrentSpeedMHz != 2400 {
|
||||||
|
t.Errorf("dimm2: current_speed_mhz=%v, want 2400", d2.CurrentSpeedMHz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseMemorySizeMB(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"16 GB", 16384},
|
||||||
|
{"32 GB", 32768},
|
||||||
|
{"8 GB", 8192},
|
||||||
|
{"16384 MB", 16384},
|
||||||
|
{"32768 MB", 32768},
|
||||||
|
{"No Module Installed", 0},
|
||||||
|
{"0", 0},
|
||||||
|
{"", 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := parseMemorySizeMB(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("parseMemorySizeMB(%q) = %d, want %d", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
audit/internal/collector/testdata/dmidecode_type0_dell.txt
vendored
Normal file
27
audit/internal/collector/testdata/dmidecode_type0_dell.txt
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# dmidecode 3.2
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 3.1.0 present.
|
||||||
|
|
||||||
|
Handle 0x0000, DMI type 0, 26 bytes
|
||||||
|
BIOS Information
|
||||||
|
Vendor: Dell Inc.
|
||||||
|
Version: 2.5.4
|
||||||
|
Release Date: 01/13/2020
|
||||||
|
Address: 0xF0000
|
||||||
|
Runtime Size: 64 kB
|
||||||
|
ROM Size: 32 MB
|
||||||
|
Characteristics:
|
||||||
|
ISA is supported
|
||||||
|
PCI is supported
|
||||||
|
PNP is supported
|
||||||
|
BIOS is upgradeable
|
||||||
|
BIOS shadowing is allowed
|
||||||
|
Boot from CD is supported
|
||||||
|
Selectable boot is supported
|
||||||
|
EDD is supported
|
||||||
|
ACPI is supported
|
||||||
|
USB legacy is supported
|
||||||
|
BIOS boot specification is supported
|
||||||
|
Targeted content distribution is supported
|
||||||
|
UEFI is supported
|
||||||
|
BIOS Revision: 2.5
|
||||||
59
audit/internal/collector/testdata/dmidecode_type17_mixed.txt
vendored
Normal file
59
audit/internal/collector/testdata/dmidecode_type17_mixed.txt
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# dmidecode 3.1
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 2.8 present.
|
||||||
|
|
||||||
|
Handle 0x0026, DMI type 17, 40 bytes
|
||||||
|
Memory Device
|
||||||
|
Array Handle: 0x0025
|
||||||
|
Error Information Handle: Not Provided
|
||||||
|
Total Width: 72 bits
|
||||||
|
Data Width: 64 bits
|
||||||
|
Size: 16 GB
|
||||||
|
Form Factor: DIMM
|
||||||
|
Set: None
|
||||||
|
Locator: P1-DIMMA1
|
||||||
|
Bank Locator: P0_Node0_Channel0_Dimm0
|
||||||
|
Type: DDR4
|
||||||
|
Type Detail: Synchronous
|
||||||
|
Speed: 2133 MT/s
|
||||||
|
Manufacturer: Micron
|
||||||
|
Serial Number: 1A2B3C4D
|
||||||
|
Asset Tag: Not Specified
|
||||||
|
Part Number: 36ASF2G72PZ-2G1A2
|
||||||
|
Rank: 2
|
||||||
|
Configured Memory Speed: 2133 MT/s
|
||||||
|
|
||||||
|
Handle 0x0027, DMI type 17, 40 bytes
|
||||||
|
Memory Device
|
||||||
|
Array Handle: 0x0025
|
||||||
|
Error Information Handle: Not Provided
|
||||||
|
Total Width: Unknown
|
||||||
|
Data Width: Unknown
|
||||||
|
Size: No Module Installed
|
||||||
|
Form Factor: DIMM
|
||||||
|
Set: None
|
||||||
|
Locator: P1-DIMMA2
|
||||||
|
Bank Locator: P0_Node0_Channel0_Dimm1
|
||||||
|
Type: DDR4
|
||||||
|
Type Detail: Synchronous
|
||||||
|
|
||||||
|
Handle 0x0028, DMI type 17, 84 bytes
|
||||||
|
Memory Device
|
||||||
|
Array Handle: 0x0025
|
||||||
|
Error Information Handle: Not Provided
|
||||||
|
Total Width: 72 bits
|
||||||
|
Data Width: 64 bits
|
||||||
|
Size: 32768 MB
|
||||||
|
Form Factor: DIMM
|
||||||
|
Set: 1
|
||||||
|
Locator: A1
|
||||||
|
Bank Locator: Not Specified
|
||||||
|
Type: DDR4
|
||||||
|
Type Detail: Synchronous Registered (Buffered)
|
||||||
|
Speed: 2933 MT/s
|
||||||
|
Manufacturer: Samsung
|
||||||
|
Serial Number: 5E6F7A8B
|
||||||
|
Asset Tag: Not Specified
|
||||||
|
Part Number: M393A4K40CB2-CVF
|
||||||
|
Rank: 2
|
||||||
|
Configured Memory Speed: 2400 MT/s
|
||||||
14
audit/internal/collector/testdata/dmidecode_type1_dell.txt
vendored
Normal file
14
audit/internal/collector/testdata/dmidecode_type1_dell.txt
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# dmidecode 3.2
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 3.1.0 present.
|
||||||
|
|
||||||
|
Handle 0x0100, DMI type 1, 27 bytes
|
||||||
|
System Information
|
||||||
|
Manufacturer: Dell Inc.
|
||||||
|
Product Name: PowerEdge R740xd
|
||||||
|
Version: Not Specified
|
||||||
|
Serial Number: 7SG9F63
|
||||||
|
UUID: b1c2d3e4-f5a6-7890-bcde-f12345678901
|
||||||
|
Wake-up Type: Power Switch
|
||||||
|
SKU Number: SKU=NotProvided;ModelName=PowerEdge R740xd
|
||||||
|
Family: PowerEdge
|
||||||
14
audit/internal/collector/testdata/dmidecode_type1_hpe.txt
vendored
Normal file
14
audit/internal/collector/testdata/dmidecode_type1_hpe.txt
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# dmidecode 3.3
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 3.1.0 present.
|
||||||
|
|
||||||
|
Handle 0x008E, DMI type 1, 27 bytes
|
||||||
|
System Information
|
||||||
|
Manufacturer: HPE
|
||||||
|
Product Name: ProLiant DL380 Gen10
|
||||||
|
Version: Not Specified
|
||||||
|
Serial Number: CZJ9320CXN
|
||||||
|
UUID: c2d3e4f5-a6b7-8901-cdef-012345678902
|
||||||
|
Wake-up Type: Power Switch
|
||||||
|
SKU Number: 868703-B21
|
||||||
|
Family: ProLiant
|
||||||
14
audit/internal/collector/testdata/dmidecode_type1_supermicro.txt
vendored
Normal file
14
audit/internal/collector/testdata/dmidecode_type1_supermicro.txt
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# dmidecode 3.1
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 2.8 present.
|
||||||
|
|
||||||
|
Handle 0x0001, DMI type 1, 27 bytes
|
||||||
|
System Information
|
||||||
|
Manufacturer: Supermicro
|
||||||
|
Product Name: SYS-6028R-WTR
|
||||||
|
Version: 0123456789
|
||||||
|
Serial Number: S214726X2A36789
|
||||||
|
UUID: d3e4f5a6-b7c8-9012-def0-123456789003
|
||||||
|
Wake-up Type: Power Switch
|
||||||
|
SKU Number: Default string
|
||||||
|
Family: Default string
|
||||||
10
audit/internal/collector/testdata/dmidecode_type2_dell.txt
vendored
Normal file
10
audit/internal/collector/testdata/dmidecode_type2_dell.txt
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# dmidecode 3.2
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 3.1.0 present.
|
||||||
|
|
||||||
|
Handle 0x0200, DMI type 2, 8 bytes
|
||||||
|
Base Board Information
|
||||||
|
Manufacturer: Dell Inc.
|
||||||
|
Product Name: 0F9N89
|
||||||
|
Version: A00
|
||||||
|
Serial Number: 7SG9F63
|
||||||
19
audit/internal/collector/testdata/dmidecode_type2_hpe.txt
vendored
Normal file
19
audit/internal/collector/testdata/dmidecode_type2_hpe.txt
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# dmidecode 3.3
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 3.1.0 present.
|
||||||
|
|
||||||
|
Handle 0x00A4, DMI type 2, 15 bytes
|
||||||
|
Base Board Information
|
||||||
|
Manufacturer: HPE
|
||||||
|
Product Name: ProLiant DL380 Gen10
|
||||||
|
Version: Not Specified
|
||||||
|
Serial Number: CZJ9320CXN
|
||||||
|
Asset Tag: CZJ9320CXN
|
||||||
|
Features:
|
||||||
|
Board is a hosting board
|
||||||
|
Board is removable
|
||||||
|
Board is replaceable
|
||||||
|
Location In Chassis: Not Specified
|
||||||
|
Chassis Handle: 0x0000
|
||||||
|
Type: Motherboard
|
||||||
|
Contained Object Handles: 0
|
||||||
18
audit/internal/collector/testdata/dmidecode_type2_supermicro.txt
vendored
Normal file
18
audit/internal/collector/testdata/dmidecode_type2_supermicro.txt
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# dmidecode 3.1
|
||||||
|
Getting SMBIOS data from sysfs.
|
||||||
|
SMBIOS 2.8 present.
|
||||||
|
|
||||||
|
Handle 0x0002, DMI type 2, 15 bytes
|
||||||
|
Base Board Information
|
||||||
|
Manufacturer: Supermicro
|
||||||
|
Product Name: X10DRW-i
|
||||||
|
Version: 1.02
|
||||||
|
Serial Number: S214726X2A36789
|
||||||
|
Asset Tag: Default string
|
||||||
|
Features:
|
||||||
|
Board is a hosting board
|
||||||
|
Board is replaceable
|
||||||
|
Location In Chassis: Default string
|
||||||
|
Chassis Handle: 0x0003
|
||||||
|
Type: Motherboard
|
||||||
|
Contained Object Handles: 0
|
||||||
@@ -1297,7 +1297,7 @@ func (h *handler) handleAPIInstallToRAM(w http.ResponseWriter, r *http.Request)
|
|||||||
var standardTools = []string{
|
var standardTools = []string{
|
||||||
"dmidecode", "smartctl", "nvme", "lspci", "ipmitool",
|
"dmidecode", "smartctl", "nvme", "lspci", "ipmitool",
|
||||||
"nvidia-smi", "dcgmi", "nv-hostengine", "memtester", "stress-ng", "nvtop",
|
"nvidia-smi", "dcgmi", "nv-hostengine", "memtester", "stress-ng", "nvtop",
|
||||||
"mstflint",
|
"mstflint", "saa",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) handleAPIToolsCheck(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) handleAPIToolsCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -477,6 +477,8 @@ function installToRAM() {
|
|||||||
|
|
||||||
` + renderNVMeFormatCard() + `
|
` + renderNVMeFormatCard() + `
|
||||||
|
|
||||||
|
` + renderSAADMICard() + `
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function checkTools() {
|
function checkTools() {
|
||||||
document.getElementById('tools-table').innerHTML = '<p style="color:var(--muted);font-size:13px">Checking...</p>';
|
document.getElementById('tools-table').innerHTML = '<p style="color:var(--muted);font-size:13px">Checking...</p>';
|
||||||
|
|||||||
302
audit/internal/webui/saa_dmi.go
Normal file
302
audit/internal/webui/saa_dmi.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package webui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dmiField struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Shn string `json:"shn"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type saaChange struct {
|
||||||
|
Shn string `json:"shn"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
shnRE = regexp.MustCompile(`^[A-Za-z0-9_]{1,16}$`)
|
||||||
|
dmiSectionRE = regexp.MustCompile(`^\[(.+?)\]$`)
|
||||||
|
// Item Name {SHN} = value // comment
|
||||||
|
dmiItemRE = regexp.MustCompile(`^(.+?)\s+\{([A-Za-z0-9]{1,16})\}\s*=\s*(.*)$`)
|
||||||
|
dmiVersionRE = regexp.MustCompile(`(?i)^version\s*=`)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// parseDMIFile parses the DMI.txt produced by "saa GetDmiInfo".
|
||||||
|
// Real format (from SAA User Guide 4.8.1):
|
||||||
|
//
|
||||||
|
// [System]
|
||||||
|
// Version {SYVS} = "A Version" // string value
|
||||||
|
// Serial Number {SYSN} = $DEFAULT$ // string value
|
||||||
|
// UUID {SYUU} = 00112233-... // hex value
|
||||||
|
func parseDMIFile(content string) []dmiField {
|
||||||
|
var fields []dmiField
|
||||||
|
currentSection := ""
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dmiVersionRE.MatchString(line) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m := dmiSectionRE.FindStringSubmatch(line); m != nil {
|
||||||
|
currentSection = strings.TrimSpace(m[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m := dmiItemRE.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
itemName := strings.TrimSpace(m[1])
|
||||||
|
shn := m[2]
|
||||||
|
rawValue := strings.TrimSpace(m[3])
|
||||||
|
// strip trailing comment (space + //)
|
||||||
|
if idx := strings.LastIndex(rawValue, " //"); idx >= 0 {
|
||||||
|
rawValue = strings.TrimSpace(rawValue[:idx])
|
||||||
|
}
|
||||||
|
// strip surrounding double quotes from string values
|
||||||
|
if len(rawValue) >= 2 && rawValue[0] == '"' && rawValue[len(rawValue)-1] == '"' {
|
||||||
|
rawValue = rawValue[1 : len(rawValue)-1]
|
||||||
|
}
|
||||||
|
displayName := itemName
|
||||||
|
if currentSection != "" {
|
||||||
|
displayName = currentSection + " / " + itemName
|
||||||
|
}
|
||||||
|
fields = append(fields, dmiField{Name: displayName, Shn: shn, Value: rawValue})
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleAPISAADMIRead(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bee-saa-*")
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "create temp dir: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
dmiFile := filepath.Join(tmpDir, "DMI.txt")
|
||||||
|
out, err := exec.CommandContext(ctx, "saa", "-c", "GetDmiInfo", "--file", dmiFile, "--overwrite").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
msg := strings.TrimSpace(string(out))
|
||||||
|
if msg == "" {
|
||||||
|
msg = err.Error()
|
||||||
|
}
|
||||||
|
writeError(w, http.StatusInternalServerError, "saa GetDmiInfo: "+msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := os.ReadFile(dmiFile)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "read DMI file: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := parseDMIFile(string(raw))
|
||||||
|
if len(fields) == 0 {
|
||||||
|
writeError(w, http.StatusInternalServerError, "no DMI fields found (file may be empty — reboot the server and try again)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) handleAPISAADMIWrite(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Changes []saaChange `json:"changes"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Changes) == 0 {
|
||||||
|
writeError(w, http.StatusUnprocessableEntity, "no changes provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, c := range req.Changes {
|
||||||
|
if !shnRE.MatchString(c.Shn) {
|
||||||
|
writeError(w, http.StatusUnprocessableEntity, "invalid shn: "+c.Shn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(c.Value) == 0 || len(c.Value) > 64 {
|
||||||
|
writeError(w, http.StatusUnprocessableEntity, "value length out of range for shn: "+c.Shn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, ch := range c.Value {
|
||||||
|
if ch < 0x20 || ch > 0x7E {
|
||||||
|
writeError(w, http.StatusUnprocessableEntity, "value contains non-printable character for shn: "+c.Shn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &Task{
|
||||||
|
ID: newJobID("saa-dmi-write"),
|
||||||
|
Name: fmt.Sprintf("SAA DMI Write (%d field(s))", len(req.Changes)),
|
||||||
|
Target: "saa-dmi-write",
|
||||||
|
Priority: defaultTaskPriority("saa-dmi-write", taskParams{}),
|
||||||
|
Status: TaskPending,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
params: taskParams{
|
||||||
|
SAADmiChanges: req.Changes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
globalQueue.enqueue(t)
|
||||||
|
writeJSON(w, map[string]string{"task_id": t.ID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSAADMIWriteTask(ctx context.Context, j *jobState, exportDir string, p taskParams) error {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "bee-saa-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create temp dir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
dmiFile := filepath.Join(tmpDir, "DMI.txt")
|
||||||
|
|
||||||
|
j.append("Reading current DMI configuration...")
|
||||||
|
if err := streamCmdJob(j, exec.CommandContext(ctx, "saa", "-c", "GetDmiInfo", "--file", dmiFile, "--overwrite")); err != nil {
|
||||||
|
return fmt.Errorf("GetDmiInfo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupDir := filepath.Join(exportDir, "dmi-backups")
|
||||||
|
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create backup dir: %w", err)
|
||||||
|
}
|
||||||
|
backupName := "dmi-" + time.Now().UTC().Format("20060102-150405") + ".txt"
|
||||||
|
backupPath := filepath.Join(backupDir, backupName)
|
||||||
|
raw, err := os.ReadFile(dmiFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read DMI file: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(backupPath, raw, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write backup: %w", err)
|
||||||
|
}
|
||||||
|
j.append("Backup saved: dmi-backups/" + backupName)
|
||||||
|
|
||||||
|
for _, c := range p.SAADmiChanges {
|
||||||
|
j.append("Setting " + c.Shn + " = " + c.Value)
|
||||||
|
cmd := exec.CommandContext(ctx, "saa", "-c", "EditDmiInfo", "--file", dmiFile, "--shn", c.Shn, "--value", c.Value)
|
||||||
|
if err := streamCmdJob(j, cmd); err != nil {
|
||||||
|
return fmt.Errorf("EditDmiInfo %s: %w", c.Shn, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
j.append("Applying changes to hardware...")
|
||||||
|
if err := streamCmdJob(j, exec.CommandContext(ctx, "saa", "-c", "ChangeDmiInfo", "--file", dmiFile)); err != nil {
|
||||||
|
return fmt.Errorf("ChangeDmiInfo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.append("Done. Reboot the server for changes to take effect.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderSAADMICard() string {
|
||||||
|
return `<div class="card"><div class="card-head">Supermicro — DMI <button class="btn btn-sm btn-secondary" onclick="saaDMIRead()" style="margin-left:auto">Read</button></div><div class="card-body">
|
||||||
|
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits DMI fields via SAA (In-Band).</p>
|
||||||
|
<div id="saa-dmi-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||||
|
<div id="saa-dmi-table"></div>
|
||||||
|
<div id="saa-dmi-save-row" style="display:none;margin-top:12px">
|
||||||
|
<button class="btn btn-primary" id="saa-dmi-save-btn" onclick="saaDMISave()">Save</button>
|
||||||
|
<span id="saa-dmi-save-msg" style="font-size:13px;color:var(--muted);margin-left:10px"></span>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function saaDMIEsc(s) {
|
||||||
|
return String(s==null?'':s).replace(/[&<>"']/g,function(c){return{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});
|
||||||
|
}
|
||||||
|
function saaDMIUpdateSaveBtn() {
|
||||||
|
var inputs = document.querySelectorAll('#saa-dmi-table input[data-original]');
|
||||||
|
var dirty = [];
|
||||||
|
inputs.forEach(function(inp){if(inp.value!==inp.dataset.original)dirty.push(inp);});
|
||||||
|
var row = document.getElementById('saa-dmi-save-row');
|
||||||
|
var btn = document.getElementById('saa-dmi-save-btn');
|
||||||
|
if(dirty.length>0){row.style.display='';btn.textContent='Save ('+dirty.length+' changed)';}
|
||||||
|
else{row.style.display='none';}
|
||||||
|
}
|
||||||
|
function saaDMIRead() {
|
||||||
|
var status = document.getElementById('saa-dmi-status');
|
||||||
|
var table = document.getElementById('saa-dmi-table');
|
||||||
|
var saveRow = document.getElementById('saa-dmi-save-row');
|
||||||
|
status.textContent = 'Reading...';
|
||||||
|
status.style.color = 'var(--muted)';
|
||||||
|
table.innerHTML = '';
|
||||||
|
saveRow.style.display = 'none';
|
||||||
|
fetch('/api/tools/saa-dmi').then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});}).then(function(fields){
|
||||||
|
status.textContent = fields.length+' field(s) loaded.';
|
||||||
|
var rows = fields.map(function(f){
|
||||||
|
return '<tr>'
|
||||||
|
+'<td style="font-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(f.name)+'</td>'
|
||||||
|
+'<td style="font-family:monospace;font-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(f.shn)+'</td>'
|
||||||
|
+'<td><input type="text" value="'+saaDMIEsc(f.value)+'" data-shn="'+saaDMIEsc(f.shn)+'" data-original="'+saaDMIEsc(f.value)+'" oninput="saaDMIMarkDirty(this)" style="width:100%;font-family:monospace;font-size:13px;border:1px solid var(--line);padding:3px 6px;border-radius:3px"></td>'
|
||||||
|
+'<td id="saa-dmi-dirty-'+saaDMIEsc(f.shn)+'" style="font-size:12px;color:var(--warn,#b45309);width:50px;padding-left:6px"></td>'
|
||||||
|
+'</tr>';
|
||||||
|
}).join('');
|
||||||
|
table.innerHTML = '<table style="width:100%;border-collapse:collapse"><tr><th style="text-align:left;font-size:13px;padding-bottom:6px">Field</th><th style="text-align:left;font-size:13px;padding-bottom:6px">Shn</th><th style="text-align:left;font-size:13px;padding-bottom:6px">Value</th><th></th></tr>'+rows+'</table>';
|
||||||
|
}).catch(function(e){
|
||||||
|
status.textContent = 'Error: '+e.message;
|
||||||
|
status.style.color = 'var(--crit-fg,#9f3a38)';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function saaDMIMarkDirty(inp) {
|
||||||
|
var shn = inp.dataset.shn;
|
||||||
|
var cell = document.getElementById('saa-dmi-dirty-'+shn);
|
||||||
|
if(cell)cell.textContent = inp.value!==inp.dataset.original?'changed':'';
|
||||||
|
saaDMIUpdateSaveBtn();
|
||||||
|
}
|
||||||
|
function saaDMIWaitTask(taskID) {
|
||||||
|
var msg = document.getElementById('saa-dmi-save-msg');
|
||||||
|
msg.textContent = 'Task '+taskID+' queued...';
|
||||||
|
msg.style.color = 'var(--muted)';
|
||||||
|
var timer = setInterval(function(){
|
||||||
|
fetch('/api/tasks').then(function(r){return r.json();}).then(function(tasks){
|
||||||
|
var task = (tasks||[]).find(function(t){return t.id===taskID;});
|
||||||
|
if(!task)return;
|
||||||
|
if(task.status==='done'||task.status==='failed'||task.status==='cancelled'){
|
||||||
|
clearInterval(timer);
|
||||||
|
msg.textContent = task.status==='done'?'Saved. Reboot to apply.':'Failed: '+(task.error||task.status);
|
||||||
|
msg.style.color = task.status==='done'?'var(--ok,green)':'var(--crit-fg,#9f3a38)';
|
||||||
|
document.getElementById('saa-dmi-save-btn').disabled = false;
|
||||||
|
}
|
||||||
|
}).catch(function(){});
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
function saaDMISave() {
|
||||||
|
var inputs = document.querySelectorAll('#saa-dmi-table input[data-original]');
|
||||||
|
var changes = [];
|
||||||
|
inputs.forEach(function(inp){if(inp.value!==inp.dataset.original)changes.push({shn:inp.dataset.shn,value:inp.value});});
|
||||||
|
if(!changes.length)return;
|
||||||
|
var names = changes.map(function(c){return c.shn;}).join(', ');
|
||||||
|
if(!window.confirm('Apply DMI changes for: '+names+'?\n\nThe server will need to be rebooted for changes to take effect.'))return;
|
||||||
|
var btn = document.getElementById('saa-dmi-save-btn');
|
||||||
|
var msg = document.getElementById('saa-dmi-save-msg');
|
||||||
|
btn.disabled = true;
|
||||||
|
msg.textContent = 'Submitting...';
|
||||||
|
msg.style.color = 'var(--muted)';
|
||||||
|
fetch('/api/tools/saa-dmi/write',{
|
||||||
|
method:'POST',
|
||||||
|
headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({changes:changes})
|
||||||
|
}).then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});}).then(function(d){
|
||||||
|
saaDMIWaitTask(d.task_id);
|
||||||
|
}).catch(function(e){
|
||||||
|
msg.textContent = 'Error: '+e.message;
|
||||||
|
msg.style.color = 'var(--crit-fg,#9f3a38)';
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div></div>`
|
||||||
|
}
|
||||||
@@ -314,6 +314,8 @@ func NewHandler(opts HandlerOptions) http.Handler {
|
|||||||
mux.HandleFunc("GET /api/tools/check", h.handleAPIToolsCheck)
|
mux.HandleFunc("GET /api/tools/check", h.handleAPIToolsCheck)
|
||||||
mux.HandleFunc("GET /api/tools/nvme-formats", h.handleAPINVMeFormats)
|
mux.HandleFunc("GET /api/tools/nvme-formats", h.handleAPINVMeFormats)
|
||||||
mux.HandleFunc("POST /api/tools/nvme-format/run", h.handleAPINVMeFormatRun)
|
mux.HandleFunc("POST /api/tools/nvme-format/run", h.handleAPINVMeFormatRun)
|
||||||
|
mux.HandleFunc("GET /api/tools/saa-dmi", h.handleAPISAADMIRead)
|
||||||
|
mux.HandleFunc("POST /api/tools/saa-dmi/write", h.handleAPISAADMIWrite)
|
||||||
|
|
||||||
// GPU presence / tools
|
// GPU presence / tools
|
||||||
mux.HandleFunc("GET /api/gpu/presence", h.handleAPIGPUPresence)
|
mux.HandleFunc("GET /api/gpu/presence", h.handleAPIGPUPresence)
|
||||||
|
|||||||
@@ -382,6 +382,12 @@ func executeTaskWithOptions(opts *HandlerOptions, t *Task, j *jobState, ctx cont
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
err = runNVMeFormatTask(ctx, j, t.params.Device, t.params.LBAF)
|
err = runNVMeFormatTask(ctx, j, t.params.Device, t.params.LBAF)
|
||||||
|
case "saa-dmi-write":
|
||||||
|
if len(t.params.SAADmiChanges) == 0 {
|
||||||
|
err = fmt.Errorf("no changes provided")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = runSAADMIWriteTask(ctx, j, opts.ExportDir, t.params)
|
||||||
default:
|
default:
|
||||||
j.append("ERROR: unknown target: " + t.Target)
|
j.append("ERROR: unknown target: " + t.Target)
|
||||||
j.finish("unknown target")
|
j.finish("unknown target")
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ type taskParams struct {
|
|||||||
Device string `json:"device,omitempty"` // for install
|
Device string `json:"device,omitempty"` // for install
|
||||||
LBAF int `json:"lbaf,omitempty"`
|
LBAF int `json:"lbaf,omitempty"`
|
||||||
PlatformComponents []string `json:"platform_components,omitempty"`
|
PlatformComponents []string `json:"platform_components,omitempty"`
|
||||||
|
SAADmiChanges []saaChange `json:"saa_dmi_changes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type persistedTask struct {
|
type persistedTask struct {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ NCCL_TESTS_VERSION=2.13.10
|
|||||||
NVCC_VERSION=12.8
|
NVCC_VERSION=12.8
|
||||||
CUBLAS_VERSION=13.1.1.3-1
|
CUBLAS_VERSION=13.1.1.3-1
|
||||||
CUDA_USERSPACE_VERSION=13.0.96-1
|
CUDA_USERSPACE_VERSION=13.0.96-1
|
||||||
DCGM_VERSION=4.5.3-1
|
DCGM_VERSION=4.6.0-1
|
||||||
JOHN_JUMBO_COMMIT=67fcf9fe5a
|
JOHN_JUMBO_COMMIT=67fcf9fe5a
|
||||||
ROCM_VERSION=6.3.4
|
ROCM_VERSION=6.3.4
|
||||||
ROCM_SMI_VERSION=7.4.0.60304-76~22.04
|
ROCM_SMI_VERSION=7.4.0.60304-76~22.04
|
||||||
|
|||||||
@@ -1473,7 +1473,7 @@ cp "${BUILDER_DIR}/smoketest.sh" "${OVERLAY_STAGE_DIR}/usr/local/bin/bee-smokete
|
|||||||
chmod +x "${OVERLAY_STAGE_DIR}/usr/local/bin/bee-smoketest"
|
chmod +x "${OVERLAY_STAGE_DIR}/usr/local/bin/bee-smoketest"
|
||||||
|
|
||||||
# --- vendor utilities (optional pre-fetched binaries) ---
|
# --- vendor utilities (optional pre-fetched binaries) ---
|
||||||
for tool in storcli64 sas2ircu sas3ircu arcconf ssacli; do
|
for tool in storcli64 sas2ircu sas3ircu arcconf ssacli saa; do
|
||||||
if [ -f "${VENDOR_DIR}/${tool}" ]; then
|
if [ -f "${VENDOR_DIR}/${tool}" ]; then
|
||||||
cp "${VENDOR_DIR}/${tool}" "${OVERLAY_STAGE_DIR}/usr/local/bin/${tool}"
|
cp "${VENDOR_DIR}/${tool}" "${OVERLAY_STAGE_DIR}/usr/local/bin/${tool}"
|
||||||
chmod +x "${OVERLAY_STAGE_DIR}/usr/local/bin/${tool}" || true
|
chmod +x "${OVERLAY_STAGE_DIR}/usr/local/bin/${tool}" || true
|
||||||
|
|||||||
BIN
iso/vendor/saa
vendored
Executable file
BIN
iso/vendor/saa
vendored
Executable file
Binary file not shown.
Reference in New Issue
Block a user