diff --git a/audit/internal/app/app.go b/audit/internal/app/app.go index e470812..a63b105 100644 --- a/audit/internal/app/app.go +++ b/audit/internal/app/app.go @@ -137,6 +137,12 @@ type satRunner interface { RunConfidentialComputingCheckPack(ctx context.Context, baseDir string, logFunc func(string)) (string, error) RunCPUAcceptancePack(ctx context.Context, baseDir string, durationSec int, logFunc func(string)) (string, error) ListNvidiaGPUs() ([]platform.NvidiaGPU, error) + ListNvidiaGPUSettings() ([]platform.NvidiaGPUSetting, error) + SetNvidiaGPUECC(index int, enabled bool) (string, error) + SetNvidiaGPUMIG(index int, enabled bool) (string, error) + SetNvidiaGPUCCMode(index int, enabled bool) (string, error) + SetNvidiaGPUPowerLimit(index int, watts float64) (string, error) + ResetNvidiaGPUDefaults() (string, error) DetectGPUVendor() string ListAMDGPUs() ([]platform.AMDGPUInfo, error) RunAMDAcceptancePack(ctx context.Context, baseDir string, logFunc func(string)) (string, error) diff --git a/audit/internal/app/app_packs.go b/audit/internal/app/app_packs.go index 0d255f3..13c4410 100644 --- a/audit/internal/app/app_packs.go +++ b/audit/internal/app/app_packs.go @@ -39,6 +39,47 @@ func (a *App) ResetNvidiaGPU(index int) (ActionResult, error) { return ActionResult{Title: fmt.Sprintf("Reset NVIDIA GPU %d", index), Body: strings.TrimSpace(out)}, err } +func (a *App) ListNvidiaGPUSettings() ([]platform.NvidiaGPUSetting, error) { + return a.sat.ListNvidiaGPUSettings() +} + +func (a *App) SetNvidiaGPUECC(index int, enabled bool) (ActionResult, error) { + out, err := a.sat.SetNvidiaGPUECC(index, enabled) + title := fmt.Sprintf("Enable ECC on GPU %d", index) + if !enabled { + title = fmt.Sprintf("Disable ECC on GPU %d", index) + } + return ActionResult{Title: title, Body: strings.TrimSpace(out)}, err +} + +func (a *App) SetNvidiaGPUMIG(index int, enabled bool) (ActionResult, error) { + out, err := a.sat.SetNvidiaGPUMIG(index, enabled) + title := fmt.Sprintf("Enable MIG on GPU %d", index) + if !enabled { + title = fmt.Sprintf("Disable MIG on GPU %d", index) + } + return ActionResult{Title: title, Body: strings.TrimSpace(out)}, err +} + +func (a *App) SetNvidiaGPUCCMode(index int, enabled bool) (ActionResult, error) { + out, err := a.sat.SetNvidiaGPUCCMode(index, enabled) + title := fmt.Sprintf("Enable Confidential Computing on GPU %d", index) + if !enabled { + title = fmt.Sprintf("Disable Confidential Computing on GPU %d", index) + } + return ActionResult{Title: title, Body: strings.TrimSpace(out)}, err +} + +func (a *App) SetNvidiaGPUPowerLimit(index int, watts float64) (ActionResult, error) { + out, err := a.sat.SetNvidiaGPUPowerLimit(index, watts) + return ActionResult{Title: fmt.Sprintf("Set power limit on GPU %d", index), Body: strings.TrimSpace(out)}, err +} + +func (a *App) ResetNvidiaGPUDefaults() (ActionResult, error) { + out, err := a.sat.ResetNvidiaGPUDefaults() + return ActionResult{Title: "Reset all GPUs to factory defaults", Body: strings.TrimSpace(out)}, err +} + func (a *App) RunNvidiaAcceptancePackWithOptions(ctx context.Context, baseDir string, diagLevel int, gpuIndices []int, logFunc func(string)) (ActionResult, error) { if strings.TrimSpace(baseDir) == "" { baseDir = DefaultSATBaseDir diff --git a/audit/internal/app/app_test.go b/audit/internal/app/app_test.go index 363beaa..7dabdba 100644 --- a/audit/internal/app/app_test.go +++ b/audit/internal/app/app_test.go @@ -235,6 +235,30 @@ func (f fakeSAT) ResetNvidiaGPU(index int) (string, error) { return "", nil } +func (f fakeSAT) ListNvidiaGPUSettings() ([]platform.NvidiaGPUSetting, error) { + return nil, nil +} + +func (f fakeSAT) SetNvidiaGPUECC(index int, enabled bool) (string, error) { + return "", nil +} + +func (f fakeSAT) SetNvidiaGPUMIG(index int, enabled bool) (string, error) { + return "", nil +} + +func (f fakeSAT) SetNvidiaGPUCCMode(index int, enabled bool) (string, error) { + return "", nil +} + +func (f fakeSAT) SetNvidiaGPUPowerLimit(index int, watts float64) (string, error) { + return "", nil +} + +func (f fakeSAT) ResetNvidiaGPUDefaults() (string, error) { + return "", nil +} + func (f fakeSAT) RunMemoryAcceptancePack(_ context.Context, baseDir string, _, _ int, _ func(string)) (string, error) { return f.runMemoryFn(baseDir) } diff --git a/audit/internal/platform/nvidia_settings.go b/audit/internal/platform/nvidia_settings.go new file mode 100644 index 0000000..391dbcf --- /dev/null +++ b/audit/internal/platform/nvidia_settings.go @@ -0,0 +1,183 @@ +package platform + +import ( + "fmt" + "strconv" + "strings" +) + +// NvidiaGPUSetting reports the persistent GPU settings that nvidia-smi +// exposes: ECC mode, MIG mode, and Confidential Computing mode all persist +// via the GPU's inforom/firmware and survive a reboot, but only take effect +// after a GPU reset (nvidia-smi -r) or host reboot (hence separate +// current/pending fields for ECC and MIG). Power limit does NOT persist +// across reboot/driver reload — it is re-applied here on demand, same as +// nvidia-smi's own behavior. +type NvidiaGPUSetting struct { + Index int `json:"index"` + Name string `json:"name"` + ECCCurrent string `json:"ecc_current"` // "Enabled" / "Disabled" / "N/A" + ECCPending string `json:"ecc_pending"` // differs from current until a GPU reset/reboot + MIGCurrent string `json:"mig_current"` // "Enabled" / "Disabled" / "N/A" (N/A = not MIG-capable) + MIGPending string `json:"mig_pending"` + CCState string `json:"cc_state"` // "ON" / "OFF" / "" (unsupported or query failed) + PowerLimitW float64 `json:"power_limit_w"` + PowerMinLimitW float64 `json:"power_min_limit_w"` + PowerMaxLimitW float64 `json:"power_max_limit_w"` + PowerDefaultLimitW float64 `json:"power_default_limit_w"` +} + +// ListNvidiaGPUSettings reports current ECC, MIG, Confidential Computing and +// power-limit configuration per GPU. +func (s *System) ListNvidiaGPUSettings() ([]NvidiaGPUSetting, error) { + out, err := satExecCommand("nvidia-smi", + "--query-gpu=index,name,ecc.mode.current,ecc.mode.pending,mig.mode.current,mig.mode.pending,power.limit,power.min_limit,power.max_limit,power.default_limit", + "--format=csv,noheader,nounits").Output() + if err != nil { + return nil, fmt.Errorf("nvidia-smi: %w", err) + } + var settings []NvidiaGPUSetting + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.Split(line, ",") + if len(parts) < 10 { + continue + } + idx, err := strconv.Atoi(strings.TrimSpace(parts[0])) + if err != nil { + continue + } + g := NvidiaGPUSetting{ + Index: idx, + Name: strings.TrimSpace(parts[1]), + ECCCurrent: strings.TrimSpace(parts[2]), + ECCPending: strings.TrimSpace(parts[3]), + MIGCurrent: strings.TrimSpace(parts[4]), + MIGPending: strings.TrimSpace(parts[5]), + PowerLimitW: parseNvidiaFloat(parts[6]), + PowerMinLimitW: parseNvidiaFloat(parts[7]), + PowerMaxLimitW: parseNvidiaFloat(parts[8]), + PowerDefaultLimitW: parseNvidiaFloat(parts[9]), + } + g.CCState = s.queryNvidiaGPUCCState(idx) + settings = append(settings, g) + } + return settings, nil +} + +// queryNvidiaGPUCCState reads the per-GPU Confidential Computing state via +// `nvidia-smi conf-compute -i -q`, reusing the same field-name +// convention as RunConfidentialComputingCheckPack's parseConfComputeFields. +// Returns "" if the driver/GPU doesn't support conf-compute at all. +func (s *System) queryNvidiaGPUCCState(index int) string { + out, err := satExecCommand("nvidia-smi", "conf-compute", "-i", strconv.Itoa(index), "-q").Output() + if err != nil { + return "" + } + return parseConfComputeFields(out)["CC State"] +} + +func parseNvidiaFloat(v string) float64 { + f, err := strconv.ParseFloat(strings.TrimSpace(v), 64) + if err != nil { + return 0 + } + return f +} + +// SetNvidiaGPUECC sets pending ECC mode for one GPU. Takes effect after the +// GPU is reset (nvidia-smi -r) or the host is rebooted. +func (s *System) SetNvidiaGPUECC(index int, enabled bool) (string, error) { + val := "0" + if enabled { + val = "1" + } + out, err := satExecCommand("nvidia-smi", "-i", strconv.Itoa(index), "-e", val).CombinedOutput() + return string(out), err +} + +// SetNvidiaGPUMIG sets pending MIG (Multi-Instance GPU) mode for one GPU. +// Persists via inforom but takes effect only after a GPU reset (nvidia-smi +// -r) or host reboot. Only supported on MIG-capable GPUs (Ampere/Hopper +// data-center parts); fails harmlessly on others. +func (s *System) SetNvidiaGPUMIG(index int, enabled bool) (string, error) { + val := "0" + if enabled { + val = "1" + } + out, err := satExecCommand("nvidia-smi", "-i", strconv.Itoa(index), "-mig", val).CombinedOutput() + return string(out), err +} + +// SetNvidiaGPUCCMode sets Confidential Computing mode for one GPU +// (nvidia-smi conf-compute -scc). Persists via inforom, reversible, but +// takes effect only after a GPU reset (nvidia-smi -r) or host reboot. +func (s *System) SetNvidiaGPUCCMode(index int, enabled bool) (string, error) { + val := "0" + if enabled { + val = "1" + } + out, err := satExecCommand("nvidia-smi", "conf-compute", "-i", strconv.Itoa(index), "-scc", val).CombinedOutput() + return string(out), err +} + +// SetNvidiaGPUPowerLimit sets the power limit (watts) for one GPU. Does not +// persist across reboot/driver reload — callers that want a durable change +// must re-apply it (e.g. at boot), same as nvidia-smi's own semantics. +func (s *System) SetNvidiaGPUPowerLimit(index int, watts float64) (string, error) { + out, err := satExecCommand("nvidia-smi", "-i", strconv.Itoa(index), "-pl", strconv.FormatFloat(watts, 'f', 0, 64)).CombinedOutput() + return string(out), err +} + +// ResetNvidiaGPUDefaults restores factory-default settings on every visible +// NVIDIA GPU that has drifted from them: power limit back to +// power.default_limit (as reported by nvidia-smi), ECC re-enabled (NVIDIA +// data-center GPUs ship with ECC on; nvidia-smi has no queryable "ECC +// default" field, so this is the documented factory default, not a value +// read back from hardware), and MIG/Confidential-Computing mode disabled +// (both ship off from the factory). GPUs already at defaults are left +// alone. +func (s *System) ResetNvidiaGPUDefaults() (string, error) { + settings, err := s.ListNvidiaGPUSettings() + if err != nil { + return "", err + } + var out strings.Builder + for _, g := range settings { + if g.PowerDefaultLimitW > 0 && g.PowerLimitW != g.PowerDefaultLimitW { + res, err := s.SetNvidiaGPUPowerLimit(g.Index, g.PowerDefaultLimitW) + out.WriteString(res) + if err != nil { + fmt.Fprintf(&out, "GPU %d: power limit reset failed: %v\n", g.Index, err) + } + } + if strings.EqualFold(g.ECCCurrent, "disabled") { + res, err := s.SetNvidiaGPUECC(g.Index, true) + out.WriteString(res) + if err != nil { + fmt.Fprintf(&out, "GPU %d: ECC re-enable failed: %v\n", g.Index, err) + } + } + if strings.EqualFold(g.MIGCurrent, "enabled") { + res, err := s.SetNvidiaGPUMIG(g.Index, false) + out.WriteString(res) + if err != nil { + fmt.Fprintf(&out, "GPU %d: MIG disable failed: %v\n", g.Index, err) + } + } + if strings.EqualFold(g.CCState, "on") { + res, err := s.SetNvidiaGPUCCMode(g.Index, false) + out.WriteString(res) + if err != nil { + fmt.Fprintf(&out, "GPU %d: CC mode disable failed: %v\n", g.Index, err) + } + } + } + if out.Len() == 0 { + out.WriteString("All GPUs already at factory-default settings.\n") + } + return out.String(), nil +} diff --git a/audit/internal/platform/nvidia_settings_test.go b/audit/internal/platform/nvidia_settings_test.go new file mode 100644 index 0000000..bb4263b --- /dev/null +++ b/audit/internal/platform/nvidia_settings_test.go @@ -0,0 +1,223 @@ +package platform + +import ( + "os/exec" + "strings" + "testing" +) + +// fakeNvidiaSMI stubs satExecCommand for nvidia-smi calls made by the +// GPU-settings code: the CSV query (index,...,power.default_limit), the +// per-GPU "conf-compute -q" read, and write commands (-e/-mig/-pl/-scc). +// ccStateByIndex maps GPU index -> CC State text ("ON"/"OFF"); a missing +// entry simulates conf-compute being unsupported (query fails). +func fakeNvidiaSMI(t *testing.T, queryOut string, ccStateByIndex map[int]string, writeCalls *[]string) { + t.Helper() + old := satExecCommand + satExecCommand = func(name string, args ...string) *exec.Cmd { + if name != "nvidia-smi" { + return exec.Command(name, args...) + } + if len(args) > 0 && strings.HasPrefix(args[0], "--query-gpu=") { + return exec.Command("printf", "%s", queryOut) + } + if len(args) > 0 && args[0] == "conf-compute" { + // args: conf-compute -i -q + idx := -1 + for i, a := range args { + if a == "-i" && i+1 < len(args) { + var n int + fscanInt(args[i+1], &n) + idx = n + } + } + if len(args) > 0 && args[len(args)-1] == "-q" { + state, ok := ccStateByIndex[idx] + if !ok { + return exec.Command("false") + } + return exec.Command("printf", "CC State : %s\\n", state) + } + if writeCalls != nil { + *writeCalls = append(*writeCalls, strings.Join(args, " ")) + } + return exec.Command("true") + } + if writeCalls != nil { + *writeCalls = append(*writeCalls, strings.Join(args, " ")) + } + return exec.Command("true") + } + t.Cleanup(func() { satExecCommand = old }) +} + +func fscanInt(s string, out *int) { + n := 0 + neg := false + for i, c := range s { + if i == 0 && c == '-' { + neg = true + continue + } + if c < '0' || c > '9' { + return + } + n = n*10 + int(c-'0') + } + if neg { + n = -n + } + *out = n +} + +func TestListNvidiaGPUSettingsParsesCSV(t *testing.T) { + fakeNvidiaSMI(t, + "0, H100 80GB HBM3, Enabled, Enabled, Disabled, Disabled, 350.00, 100.00, 700.00, 700.00\n"+ + "1, H100 80GB HBM3, Disabled, Enabled, N/A, N/A, 300.00, 100.00, 700.00, 700.00\n", + map[int]string{0: "OFF", 1: "OFF"}, nil) + + s := &System{} + got, err := s.ListNvidiaGPUSettings() + if err != nil { + t.Fatalf("err=%v", err) + } + if len(got) != 2 { + t.Fatalf("len=%d want 2 (%#v)", len(got), got) + } + if got[0].ECCCurrent != "Enabled" || got[0].PowerLimitW != 350 || got[0].PowerDefaultLimitW != 700 { + t.Fatalf("gpu0=%#v", got[0]) + } + if got[0].MIGCurrent != "Disabled" || got[0].CCState != "OFF" { + t.Fatalf("gpu0 mig/cc=%#v", got[0]) + } + if got[1].ECCCurrent != "Disabled" || got[1].ECCPending != "Enabled" || got[1].MIGCurrent != "N/A" { + t.Fatalf("gpu1=%#v", got[1]) + } +} + +func TestListNvidiaGPUSettingsCCUnsupportedLeavesEmptyState(t *testing.T) { + fakeNvidiaSMI(t, + "0, H100, Enabled, Enabled, Disabled, Disabled, 350.00, 100.00, 700.00, 700.00\n", + map[int]string{}, nil) + + s := &System{} + got, err := s.ListNvidiaGPUSettings() + if err != nil { + t.Fatalf("err=%v", err) + } + if len(got) != 1 || got[0].CCState != "" { + t.Fatalf("got=%#v want empty CCState when conf-compute query fails", got) + } +} + +func TestSetNvidiaGPUECCPassesCorrectFlag(t *testing.T) { + var calls []string + fakeNvidiaSMI(t, "", nil, &calls) + + s := &System{} + if _, err := s.SetNvidiaGPUECC(2, true); err != nil { + t.Fatalf("err=%v", err) + } + if len(calls) != 1 || calls[0] != "-i 2 -e 1" { + t.Fatalf("calls=%v want [-i 2 -e 1]", calls) + } + + if _, err := s.SetNvidiaGPUECC(0, false); err != nil { + t.Fatalf("err=%v", err) + } + if calls[1] != "-i 0 -e 0" { + t.Fatalf("calls[1]=%q want -i 0 -e 0", calls[1]) + } +} + +func TestSetNvidiaGPUMIGPassesCorrectFlag(t *testing.T) { + var calls []string + fakeNvidiaSMI(t, "", nil, &calls) + + s := &System{} + if _, err := s.SetNvidiaGPUMIG(3, true); err != nil { + t.Fatalf("err=%v", err) + } + if len(calls) != 1 || calls[0] != "-i 3 -mig 1" { + t.Fatalf("calls=%v want [-i 3 -mig 1]", calls) + } +} + +func TestSetNvidiaGPUCCModePassesCorrectFlag(t *testing.T) { + var calls []string + fakeNvidiaSMI(t, "", nil, &calls) + + s := &System{} + if _, err := s.SetNvidiaGPUCCMode(1, false); err != nil { + t.Fatalf("err=%v", err) + } + if len(calls) != 1 || calls[0] != "conf-compute -i 1 -scc 0" { + t.Fatalf("calls=%v want [conf-compute -i 1 -scc 0]", calls) + } +} + +func TestSetNvidiaGPUPowerLimitFormatsWatts(t *testing.T) { + var calls []string + fakeNvidiaSMI(t, "", nil, &calls) + + s := &System{} + if _, err := s.SetNvidiaGPUPowerLimit(1, 450); err != nil { + t.Fatalf("err=%v", err) + } + if len(calls) != 1 || calls[0] != "-i 1 -pl 450" { + t.Fatalf("calls=%v want [-i 1 -pl 450]", calls) + } +} + +func TestResetNvidiaGPUDefaultsOnlyTouchesDriftedGPUs(t *testing.T) { + var calls []string + fakeNvidiaSMI(t, + "0, H100, Enabled, Enabled, Enabled, Enabled, 350.00, 100.00, 700.00, 700.00\n"+ + "1, H100, Disabled, Disabled, Disabled, Disabled, 700.00, 100.00, 700.00, 700.00\n", + map[int]string{0: "ON", 1: "OFF"}, &calls) + + s := &System{} + out, err := s.ResetNvidiaGPUDefaults() + if err != nil { + t.Fatalf("err=%v", err) + } + if out == "" { + t.Fatalf("expected non-empty output") + } + // GPU0: power limit drifted (350 vs 700) -> reset; MIG enabled -> disable; CC ON -> disable. + // GPU1: power limit already default, MIG already disabled, CC already off; + // only ECC disabled -> re-enable. + want := []string{ + "-i 0 -pl 700", + "-i 0 -mig 0", + "conf-compute -i 0 -scc 0", + "-i 1 -e 1", + } + if len(calls) != len(want) { + t.Fatalf("calls=%v want %v", calls, want) + } + for i := range want { + if calls[i] != want[i] { + t.Fatalf("calls[%d]=%q want %q (all calls: %v)", i, calls[i], want[i], calls) + } + } +} + +func TestResetNvidiaGPUDefaultsNoOpWhenAlreadyDefault(t *testing.T) { + var calls []string + fakeNvidiaSMI(t, + "0, H100, Enabled, Enabled, Disabled, Disabled, 700.00, 100.00, 700.00, 700.00\n", + map[int]string{0: "OFF"}, &calls) + + s := &System{} + out, err := s.ResetNvidiaGPUDefaults() + if err != nil { + t.Fatalf("err=%v", err) + } + if len(calls) != 0 { + t.Fatalf("calls=%v want no calls (already at factory defaults)", calls) + } + if !strings.Contains(out, "already at factory-default") { + t.Fatalf("out=%q want no-op message", out) + } +} diff --git a/audit/internal/webui/api.go b/audit/internal/webui/api.go index 88e1888..146f62a 100644 --- a/audit/internal/webui/api.go +++ b/audit/internal/webui/api.go @@ -1169,6 +1169,125 @@ func (h *handler) handleAPIGNVIDIAReset(w http.ResponseWriter, r *http.Request) writeJSON(w, map[string]string{"status": status, "output": result.Body}) } +// ── GPU settings (ECC / power limit) ────────────────────────────────────────── + +func (h *handler) handleAPIGNVIDIAGPUSettings(w http.ResponseWriter, _ *http.Request) { + if h.opts.App == nil { + writeError(w, http.StatusServiceUnavailable, "app not configured") + return + } + settings, err := h.opts.App.ListNvidiaGPUSettings() + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + if settings == nil { + settings = []platform.NvidiaGPUSetting{} + } + writeJSON(w, settings) +} + +func (h *handler) handleAPIGNVIDIASetECC(w http.ResponseWriter, r *http.Request) { + if h.opts.App == nil { + writeError(w, http.StatusServiceUnavailable, "app not configured") + return + } + var req struct { + Index int `json:"index"` + Enabled bool `json:"enabled"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + result, err := h.opts.App.SetNvidiaGPUECC(req.Index, req.Enabled) + status := "ok" + if err != nil { + status = "error" + } + writeJSON(w, map[string]string{"status": status, "output": result.Body}) +} + +func (h *handler) handleAPIGNVIDIASetMIG(w http.ResponseWriter, r *http.Request) { + if h.opts.App == nil { + writeError(w, http.StatusServiceUnavailable, "app not configured") + return + } + var req struct { + Index int `json:"index"` + Enabled bool `json:"enabled"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + result, err := h.opts.App.SetNvidiaGPUMIG(req.Index, req.Enabled) + status := "ok" + if err != nil { + status = "error" + } + writeJSON(w, map[string]string{"status": status, "output": result.Body}) +} + +func (h *handler) handleAPIGNVIDIASetCCMode(w http.ResponseWriter, r *http.Request) { + if h.opts.App == nil { + writeError(w, http.StatusServiceUnavailable, "app not configured") + return + } + var req struct { + Index int `json:"index"` + Enabled bool `json:"enabled"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + result, err := h.opts.App.SetNvidiaGPUCCMode(req.Index, req.Enabled) + status := "ok" + if err != nil { + status = "error" + } + writeJSON(w, map[string]string{"status": status, "output": result.Body}) +} + +func (h *handler) handleAPIGNVIDIASetPowerLimit(w http.ResponseWriter, r *http.Request) { + if h.opts.App == nil { + writeError(w, http.StatusServiceUnavailable, "app not configured") + return + } + var req struct { + Index int `json:"index"` + Watts float64 `json:"watts"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + if req.Watts <= 0 { + writeError(w, http.StatusBadRequest, "watts must be > 0") + return + } + result, err := h.opts.App.SetNvidiaGPUPowerLimit(req.Index, req.Watts) + status := "ok" + if err != nil { + status = "error" + } + writeJSON(w, map[string]string{"status": status, "output": result.Body}) +} + +func (h *handler) handleAPIGNVIDIAResetDefaults(w http.ResponseWriter, _ *http.Request) { + if h.opts.App == nil { + writeError(w, http.StatusServiceUnavailable, "app not configured") + return + } + result, err := h.opts.App.ResetNvidiaGPUDefaults() + status := "ok" + if err != nil { + status = "error" + } + writeJSON(w, map[string]string{"status": status, "output": result.Body}) +} + func (h *handler) handleAPIGPUPresence(w http.ResponseWriter, r *http.Request) { if h.opts.App == nil { writeError(w, http.StatusServiceUnavailable, "app not configured") diff --git a/audit/internal/webui/page_export_tools.go b/audit/internal/webui/page_export_tools.go index be6964e..c997298 100644 --- a/audit/internal/webui/page_export_tools.go +++ b/audit/internal/webui/page_export_tools.go @@ -404,11 +404,170 @@ loadNvidiaSelfHeal(); func renderTools() string { return renderNVMeFormatCard() + ` +` + renderGPUSettingsCard() + ` + ` + renderFRUEditorCard() + ` ` + renderRAIDMgmtCard() } +func renderGPUSettingsCard() string { + return `
GPU Settings
+ + +
+

ECC, MIG, and Confidential Computing modes persist across reboot but only take effect after a GPU reset (or host reboot). Power limit does NOT persist across reboot — it is reapplied here on demand.

+
Loading GPU settings...
+
+ +
+` +} + func renderFRUEditorCard() string { return `
FRU / Elabel

Reads and edits hardware identity fields from all available sources. Each field shows its source method.

diff --git a/audit/internal/webui/server.go b/audit/internal/webui/server.go index af7a597..1ef3cd2 100644 --- a/audit/internal/webui/server.go +++ b/audit/internal/webui/server.go @@ -331,6 +331,12 @@ func NewHandler(opts HandlerOptions) http.Handler { mux.HandleFunc("GET /api/gpu/nvidia", h.handleAPIGNVIDIAGPUs) mux.HandleFunc("GET /api/gpu/nvidia-status", h.handleAPIGNVIDIAGPUStatuses) mux.HandleFunc("POST /api/gpu/nvidia-reset", h.handleAPIGNVIDIAReset) + mux.HandleFunc("GET /api/gpu/nvidia-settings", h.handleAPIGNVIDIAGPUSettings) + mux.HandleFunc("POST /api/gpu/nvidia-ecc", h.handleAPIGNVIDIASetECC) + mux.HandleFunc("POST /api/gpu/nvidia-mig", h.handleAPIGNVIDIASetMIG) + mux.HandleFunc("POST /api/gpu/nvidia-cc", h.handleAPIGNVIDIASetCCMode) + mux.HandleFunc("POST /api/gpu/nvidia-power-limit", h.handleAPIGNVIDIASetPowerLimit) + mux.HandleFunc("POST /api/gpu/nvidia-reset-defaults", h.handleAPIGNVIDIAResetDefaults) mux.HandleFunc("GET /api/gpu/tools", h.handleAPIGPUTools) // System