diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 8e0f0396fa2..be320ad8fa8 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -10,7 +10,6 @@ const { prompt, ensureApiKey, getCredential } = require("./credentials"); const registry = require("./registry"); const nim = require("./nim"); const policies = require("./policies"); -const { checkCgroupConfig } = require("./preflight"); const HOST_GATEWAY_URL = "http://host.openshell.internal"; const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1"; @@ -69,27 +68,6 @@ async function preflight() { } console.log(` ✓ openshell CLI: ${runCapture("openshell --version 2>/dev/null || echo unknown", { ignoreError: true })}`); - // cgroup v2 + Docker cgroupns - const cgroup = checkCgroupConfig(); - if (!cgroup.ok) { - console.error(""); - console.error(" !! cgroup v2 detected but Docker is not configured for cgroupns=host."); - console.error(" OpenShell's gateway runs k3s inside Docker, which will fail with:"); - console.error(""); - console.error(" openat2 /sys/fs/cgroup/kubepods/pids.max: no such file or directory"); - console.error(""); - console.error(" To fix, run:"); - console.error(""); - console.error(" nemoclaw setup-spark"); - console.error(""); - console.error(" This adds \"default-cgroupns-mode\": \"host\" to /etc/docker/daemon.json"); - console.error(" (preserving any existing settings) and restarts Docker."); - console.error(""); - console.error(` Detail: ${cgroup.reason}`); - process.exit(1); - } - console.log(" ✓ cgroup configuration OK"); - // GPU const gpu = nim.detectGpu(); if (gpu && gpu.type === "nvidia") { diff --git a/bin/lib/preflight.js b/bin/lib/preflight.js deleted file mode 100644 index dee1400277d..00000000000 --- a/bin/lib/preflight.js +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// Preflight checks for NemoClaw onboarding. - -const fs = require("fs"); -const { runCapture } = require("./runner"); - -const DEFAULT_DAEMON_JSON = "/etc/docker/daemon.json"; - -/** - * Detect if the host uses cgroup v2. - * - * Returns true when /sys/fs/cgroup is mounted as cgroup2fs. - * stat -fc is GNU coreutils (Linux). On macOS/BSD this command does not - * exist, so the platform guard ensures we never rely on command failure - * as the non-Linux fallback path. - * - * NOTE: Docker Desktop for Linux runs its own VM and manages its own - * daemon.json. This check targets native Docker Engine installs, which - * is what ships on DGX Spark and WSL2. - */ -function isCgroupV2() { - if (process.platform !== "linux") return false; - const fstype = runCapture("stat -fc %T /sys/fs/cgroup 2>/dev/null", { ignoreError: true }); - return fstype === "cgroup2fs"; -} - -/** - * Read and parse /etc/docker/daemon.json. - * - * Returns the parsed object, or null if the file doesn't exist or isn't - * valid JSON. - */ -function readDaemonJson(daemonPath) { - const p = daemonPath || DEFAULT_DAEMON_JSON; - try { - const raw = fs.readFileSync(p, "utf-8"); - return JSON.parse(raw); - } catch { - return null; - } -} - -/** - * Check whether Docker is configured for cgroupns=host. - * - * On cgroup v2 systems, OpenShell's gateway starts k3s inside a Docker - * container. k3s needs the host cgroup namespace to manage cgroup - * hierarchies. Without "default-cgroupns-mode": "host" in daemon.json, - * kubelet fails with: - * - * openat2 /sys/fs/cgroup/kubepods/pids.max: no such file or directory - * - * Returns an object: - * { ok: true } -- no issue (cgroup v1, or already configured) - * { ok: false, reason: string } -- needs fix - */ -function checkCgroupConfig(opts) { - const cgroupV2 = opts && typeof opts.cgroupV2 === "boolean" ? opts.cgroupV2 : isCgroupV2(); - if (!cgroupV2) { - return { ok: true }; - } - - const daemonPath = (opts && opts.daemonPath) || DEFAULT_DAEMON_JSON; - const config = readDaemonJson(daemonPath); - - if (config && config["default-cgroupns-mode"] === "host") { - return { ok: true }; - } - - return { - ok: false, - reason: config - ? `${daemonPath} exists but "default-cgroupns-mode" is not set to "host"` - : `${daemonPath} does not exist or is not valid JSON`, - }; -} - -module.exports = { isCgroupV2, readDaemonJson, checkCgroupConfig }; diff --git a/test/preflight.test.js b/test/preflight.test.js deleted file mode 100644 index 1700c284f42..00000000000 --- a/test/preflight.test.js +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); -const fs = require("fs"); -const os = require("os"); -const path = require("path"); - -const { isCgroupV2, readDaemonJson, checkCgroupConfig } = require("../bin/lib/preflight"); - -// Helper: create a temp daemon.json with given content and return its path. -function writeTempDaemon(content) { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-preflight-")); - const p = path.join(dir, "daemon.json"); - fs.writeFileSync(p, content, "utf-8"); - return p; -} - -describe("isCgroupV2", () => { - it("returns a boolean", () => { - assert.equal(typeof isCgroupV2(), "boolean"); - }); - - it("returns false on non-linux platforms", () => { - // On Linux this still returns a boolean (true or false depending on - // actual cgroup version). On macOS/other it always returns false. - // Either way the function must not throw. - const result = isCgroupV2(); - if (process.platform !== "linux") { - assert.equal(result, false); - } - }); -}); - -describe("readDaemonJson", () => { - it("parses valid JSON", () => { - const p = writeTempDaemon('{ "default-cgroupns-mode": "host" }'); - const result = readDaemonJson(p); - assert.deepEqual(result, { "default-cgroupns-mode": "host" }); - }); - - it("returns null for invalid JSON", () => { - const p = writeTempDaemon("not json at all"); - assert.equal(readDaemonJson(p), null); - }); - - it("returns null for missing file", () => { - assert.equal(readDaemonJson("/tmp/nonexistent-daemon-" + Date.now() + ".json"), null); - }); -}); - -describe("checkCgroupConfig", () => { - it("runs without arguments (uses live detection)", () => { - const result = checkCgroupConfig(); - assert.equal(typeof result.ok, "boolean"); - }); - - it("returns ok when cgroup v1 (skips daemon.json check)", () => { - const result = checkCgroupConfig({ cgroupV2: false }); - assert.deepEqual(result, { ok: true }); - }); - - it("returns ok when cgroup v2 and daemon.json has cgroupns=host", () => { - const p = writeTempDaemon('{ "default-cgroupns-mode": "host" }'); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.deepEqual(result, { ok: true }); - }); - - it("fails when cgroup v2 and daemon.json missing", () => { - const p = "/tmp/nonexistent-daemon-" + Date.now() + ".json"; - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("does not exist")); - }); - - it("fails when cgroup v2 and daemon.json has no cgroupns key", () => { - const p = writeTempDaemon('{ "storage-driver": "overlay2" }'); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("not set to")); - }); - - it("fails when cgroup v2 and cgroupns mode is wrong value", () => { - const p = writeTempDaemon('{ "default-cgroupns-mode": "private" }'); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("not set to")); - }); - - it("fails when cgroup v2 and daemon.json is invalid JSON", () => { - const p = writeTempDaemon("oops"); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("not valid JSON")); - }); - - it("passes with extra keys alongside cgroupns=host", () => { - const p = writeTempDaemon(JSON.stringify({ - "storage-driver": "overlay2", - "default-cgroupns-mode": "host", - "log-driver": "json-file", - })); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.deepEqual(result, { ok: true }); - }); -});