feat(tpm): add read-only TPM validation
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -17,6 +17,8 @@ var runtimeRequiredTools = []string{
|
||||
"smartctl",
|
||||
"nvme",
|
||||
"ipmitool",
|
||||
"tpm2_getcap",
|
||||
"tpm2_pcrread",
|
||||
"dhclient",
|
||||
"mount",
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user