diff --git a/scripts/prepare-dgx-station-host.sh b/scripts/prepare-dgx-station-host.sh index 57f808c1765..390e35f97b4 100755 --- a/scripts/prepare-dgx-station-host.sh +++ b/scripts/prepare-dgx-station-host.sh @@ -197,8 +197,23 @@ dgx_station_release_profile() { platform="$(dgx_station_release_value "$path" DGX_PLATFORM)" || return 1 [[ "$platform" == "DGX Server for GALAXY-GB300" ]] || return 1 - if ota_pretty="$(dgx_station_release_value "$path" DGX_OTA_PRETTY_NAME 2>/dev/null)"; then - [[ "$ota_pretty" == "DGX OS" ]] || return 1 + # DGX OS keeps its upgrade history in the DGX_OTA_* fields, so a host that has + # an OTA history is classified by the most recent OTA version it applied. + # + # A host provisioned from a full DGX OS image also carries the identity field + # DGX_OTA_PRETTY_NAME="DGX OS"; when that field is present it must read exactly + # "DGX OS". It is absent on a host that was first installed from an older base + # image (for example 7.4.1-GB300ws) and later OTA-upgraded, because an OTA + # upgrade never adds that field. In that case, fall back to the hardware + # identity and require DGX_PRETTY_NAME="NVIDIA DGX GB300WS" so that other + # release lineages that also emit DGX_OTA_* fields stay fail-closed. + if dgx_station_release_value "$path" DGX_OTA_VERSION >/dev/null 2>&1; then + if ota_pretty="$(dgx_station_release_value "$path" DGX_OTA_PRETTY_NAME 2>/dev/null)"; then + [[ "$ota_pretty" == "DGX OS" ]] || return 1 + else + pretty="$(dgx_station_release_value "$path" DGX_PRETTY_NAME)" || return 1 + [[ "$pretty" == "NVIDIA DGX GB300WS" ]] || return 1 + fi version="$(dgx_station_release_value "$path" DGX_OTA_VERSION)" || return 1 case "$version" in 7.2.0 | 7.4.0 | 7.5.0) printf '%s' supported-dgx-os ;; @@ -210,7 +225,6 @@ dgx_station_release_profile() { # No-OTA factory images are separate, exact profiles. Do not infer support # merely from a missing OTA identity: internal BaseOS and customer images # use different software stacks and qualification evidence. - dgx_station_release_value "$path" DGX_OTA_VERSION >/dev/null 2>&1 && return 1 dgx_station_release_value "$path" DGX_OTA_DATE >/dev/null 2>&1 && return 1 pretty="$(dgx_station_release_value "$path" DGX_PRETTY_NAME)" || return 1 version="$(dgx_station_release_value "$path" DGX_SWBUILD_VERSION)" || return 1 diff --git a/test/install-station-dgx-os.test.ts b/test/install-station-dgx-os.test.ts index dd3e5fcd458..da26acb6c2e 100644 --- a/test/install-station-dgx-os.test.ts +++ b/test/install-station-dgx-os.test.ts @@ -116,6 +116,36 @@ function writeNoOtaFactoryRelease( return target; } +function writeOtaUpgradedRelease( + overrides: Partial<{ pretty: string; otaVersion: string; swbuildVersion: string }> = {}, +) { + const fields = { + pretty: "NVIDIA DGX GB300WS", + otaVersion: "7.5.0", + swbuildVersion: "7.4.1-GB300ws", + ...overrides, + }; + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-station-ota-upgraded-")); + const target = path.join(dir, "dgx-release"); + fs.writeFileSync( + target, + [ + 'DGX_NAME="DGX GB300WS"', + `DGX_PRETTY_NAME="${fields.pretty}"`, + 'DGX_SWBUILD_DATE="2026-02-20-05-22-42"', + `DGX_SWBUILD_VERSION="${fields.swbuildVersion}"`, + 'DGX_COMMIT_ID="51c59a9"', + 'DGX_PLATFORM="DGX Server for GALAXY-GB300"', + 'DGX_SERIAL_NUMBER="Unknown"', + "", + `DGX_OTA_VERSION="${fields.otaVersion}"`, + 'DGX_OTA_DATE="Sun Apr 12 16:25:30 PDT 2026"', + "", + ].join("\n"), + ); + return target; +} + describe("DGX Station stock DGX OS classification", () => { it.each([ "7.2.0", @@ -349,6 +379,37 @@ dgx_station_release_state "$DGX_RELEASE" expect(result.stdout).toBe(expected); }); + it("classifies an OTA-upgraded GB300 workstation without the fresh-install marker as supported-dgx-os (#7103)", () => { + const release = writeOtaUpgradedRelease(); + const { result, output } = runSourced( + STATION_PREPARE, + ` +stat() { printf '0|0|644|256\n'; } +dgx_station_release_state "$DGX_RELEASE" +`, + { DGX_RELEASE: release }, + ); + + expect(result.status, output).toBe(0); + expect(result.stdout).toBe("supported-dgx-os"); + }); + + it.each([ + [ + "a non-workstation DGX Server identity", + writeOtaUpgradedRelease({ pretty: "NVIDIA DGX Server" }), + ], + ["an unreviewed latest OTA version", writeOtaUpgradedRelease({ otaVersion: "7.6.0" })], + ])("keeps a marker-less OTA host fail-closed with %s (#7103)", (_scenario, release) => { + const { result } = runSourced( + STATION_PREPARE, + `dgx_station_release_contents_are_supported "$DGX_RELEASE"`, + { DGX_RELEASE: release }, + ); + + expect(result.status).not.toBe(0); + }); + it("keeps the classifier self-contained when the helper is transported alone", () => { const isolated = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-station-helper-only-")); const copiedHelper = path.join(isolated, "prepare-dgx-station-host.sh");