diff --git a/src/lib/onboard/install-ollama-linux-upgrade.test.ts b/src/lib/onboard/install-ollama-linux-upgrade.test.ts index 224e87af49a..8b7f4494bc4 100644 --- a/src/lib/onboard/install-ollama-linux-upgrade.test.ts +++ b/src/lib/onboard/install-ollama-linux-upgrade.test.ts @@ -225,8 +225,10 @@ describe("installOllamaOnLinux (upgrade recovery)", () => { isUpgrade: true, }); expect(installOllamaOnLinux(opts).ok).toBe(true); - const installer = findRunShellCall(runShellImpl, "ollama.com/install.sh"); + const installer = findRunShellCall(runShellImpl, "OLLAMA_VERSION="); expect(installer).toContain(`OLLAMA_VERSION=${MIN_OLLAMA_VERSION} sh`); + expect(installer).not.toContain("curl"); + expect(installer).not.toContain("|"); }); it("restarts the daemon for an already-current binary without running the pinned installer", () => { @@ -259,8 +261,10 @@ describe("installOllamaOnLinux (upgrade recovery)", () => { runShellImpl, }); expect(installOllamaOnLinux(opts).ok).toBe(true); - const installer = findRunShellCall(runShellImpl, "ollama.com/install.sh"); - expect(installer).toContain("| sh"); + const installer = findRunShellCall(runShellImpl, "sh '"); + expect(installer).toBeDefined(); + expect(installer).not.toContain("curl"); + expect(installer).not.toContain("|"); expect(installer).not.toContain("OLLAMA_VERSION="); }); diff --git a/src/lib/onboard/install-ollama-linux.test.ts b/src/lib/onboard/install-ollama-linux.test.ts index bcbaf4908f2..2cf375c8e6e 100644 --- a/src/lib/onboard/install-ollama-linux.test.ts +++ b/src/lib/onboard/install-ollama-linux.test.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import fs from "node:fs"; + import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MIN_HERMES_OLLAMA_CONTEXT_WINDOW } from "../inference/ollama-runtime-context"; @@ -374,7 +376,155 @@ describe("installOllamaOnLinux (system)", () => { return undefined; } + function outputPathFromCurlCommand(command: readonly string[]): string { + const outputIndex = command.indexOf("--output"); + expect(outputIndex).toBeGreaterThanOrEqual(0); + const outputPath = command[outputIndex + 1]; + expect(outputPath).toBeTruthy(); + return outputPath; + } + + function configuredCurlAttempts(command: readonly string[]): number { + const retryIndex = command.indexOf("--retry"); + expect(retryIndex).toBeGreaterThanOrEqual(0); + const retries = Number(command[retryIndex + 1]); + expect(retries).toBe(3); + expect(command).toContain("--retry-all-errors"); + return retries + 1; + } + + it("retries a transient installer fetch and executes the complete file once (#9698)", () => { + let fetchAttempts = 0; + let installerPath = ""; + const runCaptureExImpl = vi.fn().mockImplementation((command: readonly string[]) => { + installerPath = outputPathFromCurlCommand(command); + const allowedAttempts = configuredCurlAttempts(command); + fetchAttempts = 2; + expect(fetchAttempts).toBeLessThanOrEqual(allowedAttempts); + fs.writeFileSync(installerPath, "#!/bin/sh\nexit 0\n"); + expect(fs.statSync(installerPath).mode & 0o777).toBe(0o600); + return { stdout: "", stderr: "", exitCode: 0, timedOut: false }; + }); + const runShellImpl = vi + .fn() + .mockReturnValue({ status: 0, stdout: "", stderr: "", error: null }); + + const result = installOllamaOnLinux( + makeOpts({ + modeOverride: "system", + runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"), + runCaptureExImpl, + runShellImpl, + }), + ); + + expect(result.ok).toBe(true); + expect(fetchAttempts).toBe(2); + expect(runCaptureExImpl).toHaveBeenCalledTimes(1); + expect(runShellImpl).toHaveBeenCalledTimes(1); + expect(String(runShellImpl.mock.calls[0]?.[0])).toContain(installerPath); + expect(fs.existsSync(installerPath)).toBe(false); + }); + + it("stops after bounded DNS retries without executing an installer (#9698)", () => { + let fetchAttempts = 0; + let installerPath = ""; + const runCaptureExImpl = vi.fn().mockImplementation((command: readonly string[]) => { + installerPath = outputPathFromCurlCommand(command); + fetchAttempts = configuredCurlAttempts(command); + return { stdout: "", stderr: "curl: (6)", exitCode: 6, timedOut: false }; + }); + const runShellImpl = vi.fn(); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code})`); + }) as never); + + try { + expect(() => + installOllamaOnLinux( + makeOpts({ + modeOverride: "system", + runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"), + runCaptureExImpl, + runShellImpl, + }), + ), + ).toThrow("process.exit(6)"); + expect(fetchAttempts).toBe(4); + expect(runShellImpl).not.toHaveBeenCalled(); + expect(fs.existsSync(installerPath)).toBe(false); + } finally { + exitSpy.mockRestore(); + } + }); + + it("never executes a partially transferred installer (#9698)", () => { + let installerPath = ""; + const runCaptureExImpl = vi.fn().mockImplementation((command: readonly string[]) => { + installerPath = outputPathFromCurlCommand(command); + fs.writeFileSync(installerPath, "#!/bin/sh\necho partial"); + return { stdout: "", stderr: "curl: (18)", exitCode: 18, timedOut: false }; + }); + const runShellImpl = vi.fn(); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code})`); + }) as never); + + try { + expect(() => + installOllamaOnLinux( + makeOpts({ + modeOverride: "system", + runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"), + runCaptureExImpl, + runShellImpl, + }), + ), + ).toThrow("process.exit(18)"); + expect(runShellImpl).not.toHaveBeenCalled(); + expect(fs.existsSync(installerPath)).toBe(false); + } finally { + exitSpy.mockRestore(); + } + }); + + it("does not retry an installer that exits nonzero (#9698)", () => { + let installerPath = ""; + const runCaptureExImpl = vi.fn().mockImplementation((command: readonly string[]) => { + installerPath = outputPathFromCurlCommand(command); + fs.writeFileSync(installerPath, "#!/bin/sh\nexit 9\n"); + return { stdout: "", stderr: "", exitCode: 0, timedOut: false }; + }); + const runShellImpl = vi + .fn() + .mockReturnValue({ status: 9, stdout: "", stderr: "", error: null }); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code})`); + }) as never); + + try { + expect(() => + installOllamaOnLinux( + makeOpts({ + modeOverride: "system", + runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"), + runCaptureExImpl, + runShellImpl, + }), + ), + ).toThrow("process.exit(9)"); + expect(runCaptureExImpl).toHaveBeenCalledTimes(1); + expect(runShellImpl).toHaveBeenCalledTimes(1); + expect(fs.existsSync(installerPath)).toBe(false); + } finally { + exitSpy.mockRestore(); + } + }); + it("runs the official install.sh and applies the systemd loopback override", () => { + const runCaptureExImpl = vi + .fn() + .mockReturnValue({ stdout: "", stderr: "", exitCode: 0, timedOut: false }); const runShellImpl = vi .fn() .mockReturnValue({ status: 0, stdout: "", stderr: "", error: null }); @@ -383,15 +533,25 @@ describe("installOllamaOnLinux (system)", () => { const opts = makeOpts({ modeOverride: "system", runCaptureImpl: vi.fn().mockReturnValue("/usr/bin/zstd"), + runCaptureExImpl, runShellImpl, ensureManagedOllamaLoopbackSystemdOverrideImpl: ensureOverride, removeUserLocalOllamaOwnershipImpl: removeOwnership, }); const result = installOllamaOnLinux(opts); expect(result).toEqual({ ok: true, mode: "system", binPath: "/usr/local/bin/ollama" }); - const installCall = findRunShellCall(runShellImpl, "ollama.com/install.sh"); + const fetchCall = runCaptureExImpl.mock.calls.find(([command]) => + Array.isArray(command) ? command.includes("https://ollama.com/install.sh") : false, + )?.[0] as readonly string[] | undefined; + expect(fetchCall).toContain("--connect-timeout"); + expect(fetchCall).toContain("--max-time"); + expect(fetchCall).toContain("--retry-max-time"); + expect(fetchCall).toContain("--proto-redir"); + expect(fetchCall).not.toContain("--insecure"); + const installCall = findRunShellCall(runShellImpl, "sh '"); expect(installCall).toBeDefined(); - expect(installCall).toContain("curl -fsSL"); + expect(installCall).not.toContain("curl"); + expect(installCall).not.toContain("|"); expect(ensureOverride).toHaveBeenCalled(); expect(removeOwnership).toHaveBeenCalledWith({ homeDir: "/home/test" }); }); diff --git a/src/lib/onboard/install-ollama-linux.ts b/src/lib/onboard/install-ollama-linux.ts index 4075baa4d20..b59ce76d6a4 100644 --- a/src/lib/onboard/install-ollama-linux.ts +++ b/src/lib/onboard/install-ollama-linux.ts @@ -36,6 +36,9 @@ const { setResolvedOllamaHost, }: typeof import("../inference/local") = require("../inference/local"); +const OFFICIAL_OLLAMA_INSTALLER_URL = "https://ollama.com/install.sh"; +const OFFICIAL_OLLAMA_INSTALLER_PROCESS_TIMEOUT_MS = 190_000; + /** * Install location modes. * @@ -124,6 +127,8 @@ function detectJetpackVariant(opts: InstallOllamaLinuxOptions): "jetpack5" | "je */ function runOfficialInstallScript(opts: InstallOllamaLinuxOptions): void { const log = opts.log ?? ((m: string) => console.log(m)); + const errorLog = opts.errorLog ?? ((m: string) => console.error(m)); + const runCaptureExImpl = opts.runCaptureExImpl ?? runCaptureEx; const runShellImpl = opts.runShellImpl ?? runShell; ensureOllamaLinuxExtractionDependencies(opts); log( @@ -134,9 +139,68 @@ function runOfficialInstallScript(opts: InstallOllamaLinuxOptions): void { if (versionPin) { log(` Requesting Ollama ${MIN_OLLAMA_VERSION} from the installer.`); } - runShellImpl(`set -o pipefail; curl -fsSL https://ollama.com/install.sh | ${versionPin}sh`, { - stdio: "inherit", - }); + + const installerDirectory = fs.mkdtempSync( + nodePath.join(os.tmpdir(), "nemoclaw-ollama-installer-"), + ); + const installerPath = nodePath.join(installerDirectory, "install.sh"); + + let failure: { exitCode: number; message: string } | null = null; + try { + fs.writeFileSync(installerPath, "", { flag: "wx", mode: 0o600 }); + const fetchResult = runCaptureExImpl( + [ + "curl", + "--fail", + "--show-error", + "--silent", + "--location", + "--proto", + "=https", + "--proto-redir", + "=https", + "--connect-timeout", + "10", + "--max-time", + "120", + "--retry", + "3", + "--retry-all-errors", + "--retry-delay", + "1", + "--retry-max-time", + "180", + "--output", + installerPath, + OFFICIAL_OLLAMA_INSTALLER_URL, + ], + { timeout: OFFICIAL_OLLAMA_INSTALLER_PROCESS_TIMEOUT_MS }, + ); + if (fetchResult.exitCode !== 0) { + failure = { + exitCode: fetchResult.exitCode ?? 1, + message: ` Ollama installer download failed after bounded retries (exit ${fetchResult.exitCode ?? "unknown"}).`, + }; + } else { + const installResult = runShellImpl(`${versionPin}sh ${shellQuote(installerPath)}`, { + ignoreError: true, + stdio: "inherit", + }); + if (installResult.error || installResult.status !== 0) { + failure = { + exitCode: installResult.status ?? 1, + message: ` Ollama installer failed (exit ${installResult.status ?? "unknown"}).`, + }; + } + } + } finally { + fs.rmSync(installerDirectory, { force: true, recursive: true }); + } + + if (failure) { + errorLog(failure.message); + process.exit(failure.exitCode); + } } /** diff --git a/test/onboard-ollama-upgrade-version-floor.test.ts b/test/onboard-ollama-upgrade-version-floor.test.ts index 5f5f7b204fd..e839c2bc3ec 100644 --- a/test/onboard-ollama-upgrade-version-floor.test.ts +++ b/test/onboard-ollama-upgrade-version-floor.test.ts @@ -176,9 +176,11 @@ describe("onboard Ollama upgrade version floor", () => { handleInstallOllamaSelection(null, "qwen3:8b", null, makeSelectionState(), menu), /Unexpected process\.exit\(1\)/, ); - const installer = commands.find((command) => command.includes("ollama.com/install.sh")); + const installer = commands.find((command) => command.includes("OLLAMA_VERSION=")); assert.ok(installer); assert.ok(installer.includes(`OLLAMA_VERSION=${MIN_OLLAMA_VERSION}`)); + assert.ok(!installer.includes("curl")); + assert.ok(!installer.includes("|")); const surfaced = errors.join("\n"); assert.ok(surfaced.includes(`did not deliver ${MIN_OLLAMA_VERSION} on this host`)); assert.ok(!surfaced.includes("systemctl restart ollama")); diff --git a/test/onboard-selection.test.ts b/test/onboard-selection.test.ts index fa063a6db0b..2cfe361b74b 100644 --- a/test/onboard-selection.test.ts +++ b/test/onboard-selection.test.ts @@ -3517,8 +3517,8 @@ reportChildScenario(async () => { const zstdPreflightIndex = commands.findIndex((command) => command.includes("apt-get install -y -qq --no-install-recommends zstd"), ); - const installerIndex = commands.findIndex((command) => - command.includes("ollama.com/install.sh"), + const installerIndex = commands.findIndex( + (command) => command.startsWith("sh '") && command.includes("/install.sh'"), ); assert.ok(zstdPreflightIndex >= 0); assert.ok(installerIndex > zstdPreflightIndex); @@ -3537,17 +3537,15 @@ reportChildScenario(async () => { value.includes("creates a system user, a systemd service, and writes to /usr/local"), ); const installerCommandIndex = events.findIndex( - ({ type, value }) => type === "command" && value.includes("ollama.com/install.sh"), + ({ type, value }) => + type === "command" && value.startsWith("sh '") && value.includes("/install.sh'"), ); assert.ok(zstdWarningIndex >= 0 && zstdWarningIndex < zstdCommandIndex); assert.ok(installerWarningIndex >= 0 && installerWarningIndex < installerCommandIndex); - assert.equal( - events.find( - ({ type, value }) => type === "command" && value.includes("ollama.com/install.sh"), - )?.stdio, - "inherit", + assert.equal(events[installerCommandIndex]?.stdio, "inherit"); + assert.ok( + commands.some((command) => command.includes("/install.sh'")), ); - assert.ok(commands.some((command) => command.includes("ollama.com/install.sh"))); assert.ok(!commands.some((command) => command.includes("brew install"))); assert.ok( commands.some((command) => command.includes("OLLAMA_HOST=127.0.0.1:11434 ollama serve")), @@ -3596,8 +3594,8 @@ runner.runCapture = (command) => { if (cmd.includes("systemctl list-unit-files ollama.service")) return "ollama.service enabled"; return ""; }; +runner.runCaptureEx = () => ({ stdout: "", stderr: "", exitCode: 0, timedOut: false }); runner.runShell = (command) => { - if (command.includes("ollama.com/install.sh")) return { status: 0 }; if (command.includes("ollama serve")) console.error("manual-start"); if (command.includes("install -D -m 0644")) return { status: 1 }; return { status: 0 }; @@ -3694,8 +3692,8 @@ const { setupNim } = require(${onboardPath}); const zstdPreflightIndex = runShellCalls.findIndex(({ command }) => command.includes("apt-get install -y -qq --no-install-recommends zstd"), ); - const installerIndex = runShellCalls.findIndex(({ command }) => - command.includes("ollama.com/install.sh"), + const installerIndex = runShellCalls.findIndex( + ({ command }) => command.startsWith("sh '") && command.includes("/install.sh'"), ); assert.ok(zstdPreflightIndex >= 0); assert.ok(installerIndex > zstdPreflightIndex); @@ -3809,7 +3807,7 @@ const { setupNim } = require(${onboardPath}); isNonInteractive: () => true, runCaptureImpl: runCapture, runShellImpl: (command) => { - installerRan ||= command.includes("ollama.com/install.sh"); + installerRan ||= command.includes("/install.sh'"); commands.push(command); return successfulRunShellResult(); }, @@ -3855,7 +3853,9 @@ const { setupNim } = require(${onboardPath}); assert.equal(prompt.mock.calls.length, 0); assert.equal(result.provider, "ollama-local"); assert.ok(notes.some((line) => line.includes("[non-interactive] Provider: ollama"))); - assert.ok(commands.some((command) => command.includes("ollama.com/install.sh"))); + assert.ok( + commands.some((command) => command.includes("/install.sh'")), + ); } finally { resetOllamaHostCache(); }