feat(tpm): add read-only TPM validation

This commit is contained in:
Mikhail Chusavitin
2026-08-26 17:13:50 +03:00
parent 9e466b8a70
commit af95216c6f
27 changed files with 434 additions and 10 deletions
+1
View File
@@ -138,6 +138,7 @@ type satRunner interface {
ResetNvidiaGPU(index int) (string, error)
RunMemoryAcceptancePack(ctx context.Context, baseDir string, sizeMB, passes int, logFunc func(string)) (string, error)
RunStorageAcceptancePack(ctx context.Context, baseDir string, extended bool, logFunc func(string)) (string, error)
RunTPMValidationPack(ctx context.Context, baseDir string, logFunc func(string)) (string, error)
RunNvidiaConfigCheckPack(ctx context.Context, baseDir string, logFunc func(string)) (string, error)
RunPCIeLinkCheckPack(ctx context.Context, baseDir string, logFunc func(string)) (string, error)
RunNvidiaPCIeBandwidthPack(ctx context.Context, baseDir string, gpuIndices []int, logFunc func(string)) (string, error)
+7
View File
@@ -258,6 +258,13 @@ func (a *App) RunStorageAcceptancePackResult(baseDir string) (ActionResult, erro
return ActionResult{Title: "Storage SAT", Body: satResultBody(path)}, err
}
func (a *App) RunTPMValidationPackCtx(ctx context.Context, baseDir string, logFunc func(string)) (string, error) {
if strings.TrimSpace(baseDir) == "" {
baseDir = DefaultSATBaseDir
}
return a.sat.RunTPMValidationPack(ctx, baseDir, logFunc)
}
func (a *App) RunNvidiaConfigCheckPackCtx(ctx context.Context, baseDir string, logFunc func(string)) (string, error) {
if strings.TrimSpace(baseDir) == "" {
baseDir = DefaultSATBaseDir
+4
View File
@@ -283,6 +283,10 @@ func (f fakeSAT) RunStorageAcceptancePack(_ context.Context, baseDir string, _ b
return f.runStorageFn(baseDir)
}
func (f fakeSAT) RunTPMValidationPack(_ context.Context, _ string, _ func(string)) (string, error) {
return "", nil
}
func (f fakeSAT) RunNvidiaConfigCheckPack(_ context.Context, baseDir string, _ func(string)) (string, error) {
return "", nil
}
+1 -1
View File
@@ -169,7 +169,7 @@ reading here can be normal driver power management, not a fault),
speed here, the idle Gen1 reading was power saving — if it stays at Gen1
under load, that's a real link/riser/slot degradation), `pcie-aer-sysfs.txt`, `kernel-aer-nvidia.txt` (AER/NVRM/Xid-filtered dmesg), `lspci-video-vv.txt`, `systemctl-nvidia-units.txt`. AMD systems get `rocm-smi*.txt` here instead. |
| `network/` | `ethtool-{info,link,module}.txt` (per-NIC), `mstflint-query.txt` (Mellanox/NVIDIA NICs). |
| `platform/` | `dmidecode-type{0,1,2}.txt` (BIOS/System/Baseboard), `ipmitool-{fru,sdr,sensor,sel,sel-time}.txt` (BMC), `sensors.json`, `lspci-{nn,vmm,vvv}.txt`. |
| `platform/` | `dmidecode-type{0,1,2}.txt` (BIOS/System/Baseboard), `ipmitool-{fru,sdr,sensor,sel,sel-time}.txt` (BMC), `tpm-{properties-fixed,pcr-banks,pcr-values,test-result}.txt` (read-only TPM state), `sensors.json`, `lspci-{nn,vmm,vvv}.txt`. |
## `status/` in detail
+1
View File
@@ -52,6 +52,7 @@ func Run(_ runtimeenv.Mode) schema.HardwareIngestRequest {
snap.PowerSupplies = enrichPSUsWithTelemetry(snap.PowerSupplies, sensorDoc)
snap.Sensors = mergeIPMISensors(buildSensorsFromDoc(sensorDoc), collectIPMISensors())
snap.EventLogs = append(collectIPMISEL(), collectDmesgErrors()...)
snap.PlatformConfig = collectTPMPlatformConfig()
finalizeSnapshot(&snap, collectedAt)
// remaining collectors added in steps 1.8 1.10
+129
View File
@@ -0,0 +1,129 @@
package collector
import (
"encoding/binary"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
var (
tpmGlob = filepath.Glob
tpmStat = os.Stat
tpmRun = func(name string, args ...string) ([]byte, error) {
return exec.Command(name, args...).Output()
}
)
// collectTPMPlatformConfig records TPM presence and read-only identity data.
// It never provisions the TPM, changes ownership, writes NV storage, or runs
// TPM2_SelfTest.
func collectTPMPlatformConfig() *json.RawMessage {
config := map[string]any{"TpmPresent": false, "TpmEnabled": false}
devices, _ := tpmGlob("/sys/class/tpm/tpm*")
if len(devices) == 0 {
return rawPlatformConfig(config)
}
config["TpmPresent"] = true
config["TpmEnabled"] = true
config["TpmDevice"] = filepath.Base(devices[0])
for _, path := range []string{"/dev/tpmrm0", "/dev/tpm0"} {
if _, err := tpmStat(path); err == nil {
config["TpmInterface"] = path
break
}
}
out, err := tpmRun("tpm2_getcap", "properties-fixed")
if err != nil {
return rawPlatformConfig(config)
}
properties := parseTPMProperties(string(out))
if value := tpmPropertyValue(properties, "TPM2_PT_FAMILY_INDICATOR"); value != "" {
config["TpmVersion"] = value
}
if value := tpmManufacturer(properties["TPM2_PT_MANUFACTURER"]); value != "" {
config["TpmManufacturer"] = value
}
firmware1 := tpmPropertyRaw(properties, "TPM2_PT_FIRMWARE_VERSION_1")
firmware2 := tpmPropertyRaw(properties, "TPM2_PT_FIRMWARE_VERSION_2")
if firmware1 != "" || firmware2 != "" {
config["TpmFirmwareVersion"] = strings.Trim(strings.Join([]string{firmware1, firmware2}, "/"), "/")
}
return rawPlatformConfig(config)
}
type tpmProperty struct {
raw string
value string
}
func parseTPMProperties(input string) map[string]tpmProperty {
properties := make(map[string]tpmProperty)
current := ""
for _, line := range strings.Split(input, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "TPM2_PT_") && strings.HasSuffix(trimmed, ":") {
current = strings.TrimSuffix(trimmed, ":")
continue
}
if current == "" {
continue
}
property := properties[current]
switch {
case strings.HasPrefix(trimmed, "raw:"):
property.raw = strings.TrimSpace(strings.TrimPrefix(trimmed, "raw:"))
case strings.HasPrefix(trimmed, "value:"):
property.value = strings.Trim(strings.TrimSpace(strings.TrimPrefix(trimmed, "value:")), `"`)
default:
continue
}
properties[current] = property
}
return properties
}
func tpmPropertyValue(properties map[string]tpmProperty, name string) string {
property := properties[name]
if property.value != "" {
return property.value
}
return property.raw
}
func tpmPropertyRaw(properties map[string]tpmProperty, name string) string {
return properties[name].raw
}
func tpmManufacturer(property tpmProperty) string {
if property.value != "" {
return property.value
}
raw := strings.TrimPrefix(property.raw, "0x")
value, err := strconv.ParseUint(raw, 16, 32)
if err != nil {
return ""
}
bytes := make([]byte, 4)
binary.BigEndian.PutUint32(bytes, uint32(value))
for _, b := range bytes {
if b != 0 && (b < 0x20 || b > 0x7e) {
return property.raw
}
}
return strings.TrimRight(string(bytes), "\x00 ")
}
func rawPlatformConfig(config map[string]any) *json.RawMessage {
raw, err := json.Marshal(config)
if err != nil {
return nil
}
message := json.RawMessage(raw)
return &message
}
+106
View File
@@ -0,0 +1,106 @@
package collector
import (
"encoding/json"
"errors"
"os"
"reflect"
"testing"
)
func TestParseTPMProperties(t *testing.T) {
properties := parseTPMProperties(`TPM2_PT_FAMILY_INDICATOR:
raw: 0x322E3000
value: "2.0"
TPM2_PT_MANUFACTURER:
raw: 0x49465800
value: "IFX"
TPM2_PT_FIRMWARE_VERSION_1:
raw: 0x0007003F
TPM2_PT_FIRMWARE_VERSION_2:
raw: 0x00100023
`)
if got := tpmPropertyValue(properties, "TPM2_PT_FAMILY_INDICATOR"); got != "2.0" {
t.Fatalf("family indicator = %q, want 2.0", got)
}
if got := tpmManufacturer(properties["TPM2_PT_MANUFACTURER"]); got != "IFX" {
t.Fatalf("manufacturer = %q, want IFX", got)
}
if got := tpmPropertyRaw(properties, "TPM2_PT_FIRMWARE_VERSION_1"); got != "0x0007003F" {
t.Fatalf("firmware version 1 = %q", got)
}
}
func TestTPMManufacturerDecodesRawVendorID(t *testing.T) {
if got := tpmManufacturer(tpmProperty{raw: "0x49465800"}); got != "IFX" {
t.Fatalf("manufacturer = %q, want IFX", got)
}
}
func TestCollectTPMPlatformConfig(t *testing.T) {
originalGlob, originalStat, originalRun := tpmGlob, tpmStat, tpmRun
t.Cleanup(func() {
tpmGlob, tpmStat, tpmRun = originalGlob, originalStat, originalRun
})
tpmGlob = func(string) ([]string, error) { return []string{"/sys/class/tpm/tpm0"}, nil }
tpmStat = func(path string) (os.FileInfo, error) {
if path == "/dev/tpmrm0" {
return nil, nil
}
return nil, os.ErrNotExist
}
tpmRun = func(name string, args ...string) ([]byte, error) {
if name != "tpm2_getcap" || !reflect.DeepEqual(args, []string{"properties-fixed"}) {
t.Fatalf("unexpected command: %s %v", name, args)
}
return []byte(`TPM2_PT_FAMILY_INDICATOR:
value: "2.0"
TPM2_PT_MANUFACTURER:
raw: 0x49465800
TPM2_PT_FIRMWARE_VERSION_1:
raw: 0x0007003F
TPM2_PT_FIRMWARE_VERSION_2:
raw: 0x00100023
`), nil
}
got := decodeTPMConfig(t, collectTPMPlatformConfig())
want := map[string]any{
"TpmPresent": true,
"TpmEnabled": true,
"TpmDevice": "tpm0",
"TpmInterface": "/dev/tpmrm0",
"TpmVersion": "2.0",
"TpmManufacturer": "IFX",
"TpmFirmwareVersion": "0x0007003F/0x00100023",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("config = %#v, want %#v", got, want)
}
}
func TestCollectTPMPlatformConfigAbsent(t *testing.T) {
originalGlob := tpmGlob
t.Cleanup(func() { tpmGlob = originalGlob })
tpmGlob = func(string) ([]string, error) { return nil, errors.New("not found") }
got := decodeTPMConfig(t, collectTPMPlatformConfig())
want := map[string]any{"TpmPresent": false, "TpmEnabled": false}
if !reflect.DeepEqual(got, want) {
t.Fatalf("config = %#v, want %#v", got, want)
}
}
func decodeTPMConfig(t *testing.T, raw *json.RawMessage) map[string]any {
t.Helper()
if raw == nil {
t.Fatal("platform config is nil")
}
var config map[string]any
if err := json.Unmarshal(*raw, &config); err != nil {
t.Fatalf("unmarshal config: %v", err)
}
return config
}
+2
View File
@@ -17,6 +17,8 @@ var runtimeRequiredTools = []string{
"smartctl",
"nvme",
"ipmitool",
"tpm2_getcap",
"tpm2_pcrread",
"dhclient",
"mount",
}
+3
View File
@@ -50,6 +50,9 @@ const (
// RAM: memtester 512 MB / 1 pass (extrapolated from validate timing, linear with size).
SATEstimatedMemoryStressSec = 140
// TPM capabilities, PCR values, and existing self-test result queries.
SATEstimatedTPMValidateSec = 5
// NVIDIA dcgmi diag Level 2 (medium), all GPUs simultaneously.
SATEstimatedNvidiaGPUValidateSec = 85
// NVIDIA dcgmi diag Level 3 (targeted stress), all GPUs simultaneously.
+4
View File
@@ -29,6 +29,10 @@ var techDumpFixedCommands = []struct {
{Name: "ipmitool", Args: []string{"sensor"}, File: "ipmitool-sensor.txt"},
{Name: "ipmitool", Args: []string{"sel", "list"}, File: "ipmitool-sel.txt"},
{Name: "ipmitool", Args: []string{"sel", "time", "get"}, File: "ipmitool-sel-time.txt"},
{Name: "tpm2_getcap", Args: []string{"properties-fixed"}, File: "tpm-properties-fixed.txt"},
{Name: "tpm2_getcap", Args: []string{"pcrs"}, File: "tpm-pcr-banks.txt"},
{Name: "tpm2_pcrread", File: "tpm-pcr-values.txt"},
{Name: "tpm2_gettestresult", File: "tpm-test-result.txt"},
{Name: "nvme", Args: []string{"list", "-o", "json"}, File: "nvme-list.json"},
{Name: "storcli64", Args: []string{"/call/eall/sall", "show", "all", "J"}, File: "storcli64-drives.json"},
// storcli2 (Tri-Mode controllers, e.g. SAS3808-iMR/9500 series) needs an
+19
View File
@@ -0,0 +1,19 @@
package platform
import "context"
// RunTPMValidationPack verifies TPM 2.0 communication using read-only
// commands. It deliberately excludes SelfTest, provisioning, NV writes, PCR
// changes, key creation, and ownership operations.
func (s *System) RunTPMValidationPack(ctx context.Context, baseDir string, logFunc func(string)) (string, error) {
return runAcceptancePackCtx(ctx, baseDir, "tpm", tpmValidationJobs(), logFunc)
}
func tpmValidationJobs() []satJob {
return []satJob{
{name: "01-properties-fixed.log", cmd: []string{"tpm2_getcap", "properties-fixed"}},
{name: "02-pcr-banks.log", cmd: []string{"tpm2_getcap", "pcrs"}},
{name: "03-pcr-values.log", cmd: []string{"tpm2_pcrread"}},
{name: "04-test-result.log", cmd: []string{"tpm2_gettestresult"}},
}
}
+33
View File
@@ -0,0 +1,33 @@
package platform
import (
"reflect"
"strings"
"testing"
)
func TestTPMValidationJobsAreReadOnly(t *testing.T) {
t.Parallel()
jobs := tpmValidationJobs()
want := [][]string{
{"tpm2_getcap", "properties-fixed"},
{"tpm2_getcap", "pcrs"},
{"tpm2_pcrread"},
{"tpm2_gettestresult"},
}
if len(jobs) != len(want) {
t.Fatalf("jobs=%d want %d", len(jobs), len(want))
}
for index, job := range jobs {
if !reflect.DeepEqual(job.cmd, want[index]) {
t.Fatalf("jobs[%d].cmd=%v want %v", index, job.cmd, want[index])
}
joined := strings.ToLower(strings.Join(job.cmd, " "))
for _, forbidden := range []string{"selftest", "clear", "changeauth", "nvwrite", "pcrextend", "create"} {
if strings.Contains(joined, forbidden) {
t.Fatalf("job %q contains state-changing command %q", joined, forbidden)
}
}
}
}
+1
View File
@@ -1542,6 +1542,7 @@ func (h *handler) handleAPISystemTimeSync(w http.ResponseWriter, r *http.Request
var standardTools = []string{
"dmidecode", "smartctl", "nvme", "lspci", "ipmitool",
"tpm2_getcap", "tpm2_pcrread", "tpm2_gettestresult",
"nvidia-smi", "dcgmi", "nv-hostengine", "memtester", "stress-ng", "nvtop",
"mstflint", "saa",
}
+44 -4
View File
@@ -22,6 +22,7 @@ type validateInventory struct {
CPU string
Memory string
Storage string
TPM string
NVIDIA string
AMD string
NvidiaGPUCount int
@@ -42,6 +43,7 @@ func validateTotalValidateSec(n int) int {
}
total := platform.SATEstimatedCPUValidateSec +
platform.SATEstimatedMemoryValidateSec +
platform.SATEstimatedTPMValidateSec +
platform.SATEstimatedNvidiaInterconnectSec +
platform.SATEstimatedNvidiaBandwidthSec
if n > 0 {
@@ -147,6 +149,12 @@ func renderValidateMode(opts HandlerOptions, stressDefault bool) string {
`<code>lsblk</code>; NVMe: <code>nvme id-ctrl</code>, <code>nvme smart-log</code>, <code>nvme device-self-test -s 1</code>; SATA/SAS: <code>smartctl -H -A</code>, <code>smartctl -t short</code>`,
`~2 min per device (NVMe short self-test; SATA/SAS short self-test — duration device-dependent).`,
)) +
renderSATCard("tpm", "TPM", "runSAT('tpm')", "", renderValidateCardBody(
inv.TPM,
`Checks TPM 2.0 communication and reports its fixed properties, allocated PCR banks, current PCR values, and the result of self-tests already performed by the TPM. It does not start a new self-test or change TPM state.`,
`<code>tpm2_getcap properties-fixed</code>, <code>tpm2_getcap pcrs</code>, <code>tpm2_pcrread</code>, <code>tpm2_gettestresult</code>`,
`Seconds - read-only queries; no ownership, NV, PCR, or key changes.`,
)) +
`</div>
<div style="height:1px;background:var(--border);margin:16px 0"></div>
<div class="card" style="margin-bottom:16px">
@@ -211,7 +219,7 @@ func renderValidateMode(opts HandlerOptions, stressDefault bool) string {
let satES = null;
` + satStressModeJS + `
function satLabels() {
return {nvidia:'Validate GPU', 'nvidia-targeted-stress':'NVIDIA Targeted Stress (dcgmi diag targeted_stress)', 'nvidia-targeted-power':'NVIDIA Targeted Power (dcgmi diag targeted_power)', 'nvidia-pulse':'NVIDIA PSU Pulse Test (dcgmi diag pulse_test)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL all_reduce_perf)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Validate Memory', storage:'Validate Storage', cpu:'Validate CPU', amd:'Validate AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth'};
return {nvidia:'Validate GPU', 'nvidia-targeted-stress':'NVIDIA Targeted Stress (dcgmi diag targeted_stress)', 'nvidia-targeted-power':'NVIDIA Targeted Power (dcgmi diag targeted_power)', 'nvidia-pulse':'NVIDIA PSU Pulse Test (dcgmi diag pulse_test)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL all_reduce_perf)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Validate Memory', storage:'Validate Storage', tpm:'Validate TPM (read-only)', cpu:'Validate CPU', amd:'Validate AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth'};
}
let satNvidiaGPUsPromise = null;
function loadSatNvidiaGPUs() {
@@ -416,7 +424,7 @@ function runAllSAT() {
const status = document.getElementById('sat-all-status');
status.textContent = 'Enqueuing...';
const stressOnlyTargets = ['nvidia-targeted-stress', 'nvidia-targeted-power', 'nvidia-pulse'];
const baseTargets = ['nvidia','nvidia-targeted-stress','nvidia-targeted-power','nvidia-pulse','nvidia-interconnect','nvidia-bandwidth','memory','storage','cpu'].concat(selectedAMDValidateTargets());
const baseTargets = ['nvidia','nvidia-targeted-stress','nvidia-targeted-power','nvidia-pulse','nvidia-interconnect','nvidia-bandwidth','memory','storage','tpm','cpu'].concat(selectedAMDValidateTargets());
const activeTargets = baseTargets.filter(target => {
if (stressOnlyTargets.indexOf(target) >= 0 && !satStressMode()) return false;
const btn = document.getElementById('sat-btn-' + target);
@@ -498,6 +506,7 @@ func loadValidateInventory(opts HandlerOptions) validateInventory {
CPU: unknown,
Memory: unknown,
Storage: unknown,
TPM: unknown,
NVIDIA: unknown,
AMD: unknown,
}
@@ -561,6 +570,7 @@ func loadValidateInventory(opts HandlerOptions) validateInventory {
out.CPU = formatValidateDeviceSummary(cpuTotal, cpuCounts, "CPU")
out.Memory = formatValidateDeviceSummary(memTotal, memCounts, "module")
out.Storage = formatValidateDeviceSummary(storageTotal, storageCounts, "device")
out.TPM = formatValidateTPMSummary(snap.Hardware.PlatformConfig)
out.NVIDIA = formatValidateDeviceSummary(nvidiaTotal, nvidiaCounts, "GPU")
out.AMD = formatValidateDeviceSummary(amdTotal, amdCounts, "GPU")
out.NvidiaGPUCount = nvidiaTotal
@@ -568,6 +578,30 @@ func loadValidateInventory(opts HandlerOptions) validateInventory {
return out
}
func formatValidateTPMSummary(raw *json.RawMessage) string {
if raw == nil {
return "TPM presence was not collected."
}
var config map[string]any
if err := json.Unmarshal(*raw, &config); err != nil {
return "TPM presence was not collected."
}
present, ok := config["TpmPresent"].(bool)
if !ok {
return "TPM presence was not collected."
}
if !present {
return "No TPM detected."
}
parts := []string{"TPM detected"}
for _, key := range []string{"TpmVersion", "TpmManufacturer", "TpmFirmwareVersion", "TpmInterface"} {
if value, ok := config[key].(string); ok && strings.TrimSpace(value) != "" {
parts = append(parts, html.EscapeString(value))
}
}
return strings.Join(parts, " / ")
}
func renderValidateCardBody(devices, description, commands, settings string) string {
return `<div class="validate-card-section"><div style="font-size:13px;color:var(--muted)">` + devices + `</div></div>` +
`<div class="validate-card-section"><div style="font-size:13px">` + description + `</div></div>` +
@@ -665,6 +699,12 @@ func renderCheck(opts HandlerOptions) string {
`<code>lsblk</code>; NVMe: <code>nvme id-ctrl</code>, <code>nvme smart-log</code>; SATA/SAS: <code>smartctl -H -A</code>`,
`Seconds — instantaneous device query, no wear counters incremented.`,
)) +
renderSATCard("tpm", "TPM", "runSAT('tpm')", "", renderValidateCardBody(
inv.TPM,
`Checks TPM 2.0 communication and reads fixed properties, allocated PCR banks, current PCR values, and the result of self-tests already performed by the TPM. It never starts a new self-test.`,
`<code>tpm2_getcap properties-fixed</code>, <code>tpm2_getcap pcrs</code>, <code>tpm2_pcrread</code>, <code>tpm2_gettestresult</code>`,
`Seconds - read-only queries; no ownership, NV, PCR, or key changes.`,
)) +
renderSATCard("nvidia-config", "GPU Config & NVLink", "runSAT('nvidia-config')", "", renderValidateCardBody(
inv.NVIDIA,
`Checks GPU configuration and NVLink topology that DCGM diag does not cover: ECC/MIG/power-limit drift from factory default, NVLink-bonded pair link count and error counters, and (informational) NVIDIA Confidential Computing readiness (CPU TEE support + GPU firmware CC capability). Read-only — changes nothing.`,
@@ -744,7 +784,7 @@ func renderCheck(opts HandlerOptions) string {
<script>
let satES = null;
function satLabels() {
return {nvidia:'Check GPU (DCGM L2)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Check Memory', storage:'Check Storage', cpu:'Check CPU', amd:'Check AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth', 'nvidia-config':'Check GPU Config & NVLink', 'pcie-link':'PCIe Link Check', 'nvidia-pcie-bandwidth':'GPU PCIe Bandwidth Check'};
return {nvidia:'Check GPU (DCGM L2)', 'nvidia-interconnect':'NVIDIA Interconnect (NCCL)', 'nvidia-bandwidth':'NVIDIA Bandwidth (NVBandwidth)', memory:'Check Memory', storage:'Check Storage', tpm:'Check TPM (read-only)', cpu:'Check CPU', amd:'Check AMD GPU', 'amd-mem':'AMD GPU MEM Integrity', 'amd-bandwidth':'AMD GPU MEM Bandwidth', 'nvidia-config':'Check GPU Config & NVLink', 'pcie-link':'PCIe Link Check', 'nvidia-pcie-bandwidth':'GPU PCIe Bandwidth Check'};
}
let satNvidiaGPUsPromise = null;
function loadSatNvidiaGPUs() {
@@ -880,7 +920,7 @@ function runAllCheckSAT() {
status.textContent = 'Enqueuing...';
const nvidiaIndices = satSelectedGPUIndices();
const nvidiaAllTargets = ['nvidia', 'nvidia-interconnect', 'nvidia-bandwidth', 'nvidia-pcie-bandwidth'];
const baseTargets = ['cpu', 'memory', 'storage', 'nvidia-config', 'pcie-link'];
const baseTargets = ['cpu', 'memory', 'storage', 'tpm', 'nvidia-config', 'pcie-link'];
const amdTargets = selectedAMDValidateTargets();
const expanded = [];
baseTargets.forEach(t => expanded.push({target: t}));
@@ -0,0 +1,49 @@
package webui
import (
"encoding/json"
"strings"
"testing"
)
func TestFormatValidateTPMSummary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
want string
}{
{name: "absent", raw: `{"TpmPresent":false}`, want: "No TPM detected."},
{name: "present", raw: `{"TpmPresent":true,"TpmVersion":"2.0","TpmManufacturer":"IFX","TpmFirmwareVersion":"7.63","TpmInterface":"/dev/tpmrm0"}`, want: "TPM detected / 2.0 / IFX / 7.63 / /dev/tpmrm0"},
{name: "legacy snapshot", raw: `{}`, want: "TPM presence was not collected."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
raw := json.RawMessage(tt.raw)
if got := formatValidateTPMSummary(&raw); got != tt.want {
t.Fatalf("summary=%q want %q", got, tt.want)
}
})
}
}
func TestRenderCheckIncludesReadOnlyTPMValidation(t *testing.T) {
t.Parallel()
page := renderCheck(HandlerOptions{})
for _, want := range []string{
`id="sat-btn-tpm"`,
`tpm2_getcap properties-fixed`,
`tpm2_pcrread`,
`tpm2_gettestresult`,
`'storage', 'tpm', 'nvidia-config'`,
} {
if !strings.Contains(page, want) {
t.Fatalf("check page does not contain %q", want)
}
}
if strings.Contains(page, "tpm2_selftest") {
t.Fatal("check page must not offer tpm2_selftest")
}
}
+1
View File
@@ -264,6 +264,7 @@ func NewHandler(opts HandlerOptions) http.Handler {
mux.HandleFunc("POST /api/sat/nvidia-stress/run", h.handleAPISATRun("nvidia-stress"))
mux.HandleFunc("POST /api/sat/memory/run", h.handleAPISATRun("memory"))
mux.HandleFunc("POST /api/sat/storage/run", h.handleAPISATRun("storage"))
mux.HandleFunc("POST /api/sat/tpm/run", h.handleAPISATRun("tpm"))
mux.HandleFunc("POST /api/sat/nvidia-config/run", h.handleAPISATRun("nvidia-config"))
mux.HandleFunc("POST /api/sat/pcie-link/run", h.handleAPISATRun("pcie-link"))
mux.HandleFunc("POST /api/sat/nvidia-pcie-bandwidth/run", h.handleAPISATRun("nvidia-pcie-bandwidth"))
+6
View File
@@ -288,6 +288,12 @@ func executeTaskWithOptions(opts *HandlerOptions, t *Task, j *jobState, ctx cont
break
}
archive, err = runStorageAcceptancePackCtx(a, ctx, "", t.params.StressMode, j.append)
case "tpm":
if a == nil {
err = fmt.Errorf("app not configured")
break
}
archive, err = runTPMValidationPackCtx(a, ctx, "", j.append)
case "nvidia-config":
if a == nil {
err = fmt.Errorf("app not configured")
+4
View File
@@ -45,6 +45,7 @@ var taskNames = map[string]string{
"nvidia-stress": "NVIDIA GPU Stress",
"memory": "Memory SAT",
"storage": "Storage SAT",
"tpm": "TPM Read-only Check",
"nvidia-config": "GPU Config & NVLink Check",
"pcie-link": "PCIe Link Check (forced retrain)",
"nvidia-pcie-bandwidth": "NVIDIA GPU PCIe Bandwidth Check",
@@ -316,6 +317,9 @@ var (
runStorageAcceptancePackCtx = func(a *app.App, ctx context.Context, baseDir string, extended bool, logFunc func(string)) (string, error) {
return a.RunStorageAcceptancePackCtx(ctx, baseDir, extended, logFunc)
}
runTPMValidationPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) {
return a.RunTPMValidationPackCtx(ctx, baseDir, logFunc)
}
runNvidiaConfigCheckPackCtx = func(a *app.App, ctx context.Context, baseDir string, logFunc func(string)) (string, error) {
return a.RunNvidiaConfigCheckPackCtx(ctx, baseDir, logFunc)
}
+1 -1
Submodule bible updated: 1977730d93...d2600f1279
+5
View File
@@ -23,6 +23,11 @@ Generic engineering rules live in `bible/rules/patterns/`.
- `lscpu`
- `sensors`
- `stress-ng`
- TPM check (read-only)
- `tpm2_getcap properties-fixed`
- `tpm2_getcap pcrs`
- `tpm2_pcrread`
- `tpm2_gettestresult` (reads the existing result; does not start `TPM2_SelfTest`)
- Memory check
- `free`
- `timeout <timeout_sec> memtester`
+3 -1
View File
@@ -149,7 +149,8 @@ Current validation state:
5. pcie collector (lspci -vmm -D, /sys/bus/pci/devices/)
6. psu collector (ipmitool fru + sdr — silent if no /dev/ipmi0)
7. nvidia enrichment (nvidia-smi — skipped if binary absent or driver not loaded)
8. output JSON → /var/log/bee-audit.json
8. TPM inventory (sysfs presence + `tpm2_getcap properties-fixed`; no state changes)
9. output JSON → /var/log/bee-audit.json
```
Every collector returns `nil, nil` on tool-not-found. Errors are logged, never fatal.
@@ -159,6 +160,7 @@ Acceptance flows:
- NVIDIA GPU burn-in can use either `bee-gpu-burn` or `bee-john-gpu-stress` (John the Ripper jumbo via OpenCL)
- `bee sat memory``memtester` archive
- `bee sat storage` → SMART/NVMe diagnostic archive and short self-test trigger where supported
- `bee` TPM Validate → read-only capabilities, PCR values, and existing self-test result; never starts `TPM2_SelfTest`
- SAT `summary.txt` now includes `overall_status` and per-job `*_status` values (`OK`, `FAILED`, `UNSUPPORTED`)
- `bee-gpu-burn` should prefer cuBLASLt GEMM load over the old integer/PTX burn path:
- Ampere: `fp16` + `fp32`/TF32 tensor-core load
+1 -1
View File
@@ -18,7 +18,7 @@ Fills gaps where Redfish/logpile is blind:
## In scope
- Read-only hardware inventory: board, CPU, memory, storage, PCIe, PSU, GPU, NIC, RAID
- Read-only hardware inventory: board, CPU, memory, storage, PCIe, PSU, GPU, NIC, RAID, TPM
- Machine-readable health summary derived from collector verdicts
- Operator-triggered acceptance tests for NVIDIA, memory, and storage
- NVIDIA SAT includes diagnostic collection plus a lightweight in-image GPU stress step via `bee-gpu-burn`
@@ -763,6 +763,11 @@ PSU без `serial_number` игнорируется.
}
```
`bee` emits the following TPM keys in this object: `TpmPresent`, `TpmEnabled`,
`TpmDevice`, `TpmInterface`, `TpmVersion`, `TpmManufacturer`, and
`TpmFirmwareVersion`. Detection and identity collection are read-only; absent
optional identity fields are omitted.
---
## Обработка статусов компонентов
+1
View File
@@ -18,6 +18,7 @@ Tests on the **Validate** page are purely diagnostic. They:
| Storage | `smartctl -a`, `nvme smart-log` — reads SMART data only |
| CPU | `stress-ng` for a bounded duration; CPU-only, no I/O |
| AMD GPU | `rocm-smi --showallinfo`, `dmidecode` — read-only queries |
| TPM | `tpm2_getcap`, `tpm2_pcrread`, `tpm2_gettestresult` — read-only queries; does not start `TPM2_SelfTest` |
## Burn Tests (hardware wear)
@@ -2,6 +2,7 @@
dmidecode
smartmontools
nvme-cli
tpm2-tools
pciutils
rsync
ipmitool
+1 -1
View File
@@ -43,7 +43,7 @@ info "nvidia modules flavor: ${NVIDIA_MODULES_FLAVOR}"
# --- PATH & binaries ---
echo "-- PATH & binaries --"
for tool in dmidecode smartctl nvme ipmitool lspci bee; do
for tool in dmidecode smartctl nvme ipmitool lspci tpm2_getcap tpm2_pcrread tpm2_gettestresult bee; do
if p=$(PATH="/usr/local/bin:/usr/sbin:/sbin:$PATH" command -v "$tool" 2>/dev/null); then
ok "$tool found: $p"
else