From 8d167b96b2da0326afd9c3e0bd5b4e050cdc4165 Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Wed, 7 Oct 2026 20:13:07 +0000 Subject: [PATCH] Release: host installer runs the agent as root (0.2.4) (#367) Brings #366 to prod. --- apps/api/install/install.sh | 51 +++++++++++++++++++++++++++++-------- build/Containerfile | 35 +++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/apps/api/install/install.sh b/apps/api/install/install.sh index 2f842a55..ee548c95 100755 --- a/apps/api/install/install.sh +++ b/apps/api/install/install.sh @@ -4,14 +4,17 @@ # This file is the source of what that URL serves, kept in the public # repository so anyone about to pipe it into a shell can read it first. # -# curl -fsSL https://api.nestri.io/install.sh | sh -s -- +# curl -fsSL https://api.nestri.io/install.sh | sudo sh -s -- # # What it does, in order: check this is 64-bit Linux, ask where box images # should live, download the host agent for this platform, verify it against the -# published SHA256SUMS, install it to ~/.local/bin, and hand over to -# the agent's own onboarding, which checks the machine, registers it with the -# token and starts the agent as a systemd user service. It never asks for sudo: the agent -# runs as the user who ran this. +# published SHA256SUMS, install it to /usr/local/bin, and hand over to the +# agent's own onboarding, which checks the machine, registers it with the token +# and starts the agent as a systemd service. +# +# It runs as root because the agent does: each box's VMM is jailed under a uid +# of its own, which only root can give it, and a box's network needs +# CAP_NET_ADMIN. The agent keeps its state in /var/lib/nestri. # # The token comes from the dashboard's Installation page. It registers one # machine, works once and lapses after an hour. @@ -21,9 +24,10 @@ set -eu API="${NESTRI_API:-https://api.nestri.io}" # Pinned, not "latest", so the script and the binary it installs are a pair # somebody chose. Bump when cutting a release; NESTRI_HOST_VERSION overrides it. -DEFAULT_VERSION="0.2.3" +DEFAULT_VERSION="0.2.4" VERSION="${NESTRI_HOST_VERSION:-$DEFAULT_VERSION}" -BIN_DIR="${NESTRI_BIN_DIR:-$HOME/.local/bin}" +BIN_DIR="${NESTRI_BIN_DIR:-/usr/local/bin}" +STATE_DIR=/var/lib/nestri say() { printf '%s\n' "$*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } @@ -37,7 +41,31 @@ case "$(uname -m)" in x86_64|amd64) target=x86_64-unknown-linux-gnu ;; *) die "no host build for $(uname -m) yet." ;; esac -[ "$(id -u)" -ne 0 ] || die "run this as the user that will run boxes, not as root." +[ "$(id -u)" -eq 0 ] || die "run this as root: pipe it into \`sudo sh -s -- \`, as the dashboard shows." + +# --- an install from before the agent ran as root --------------------------- +# Hosts used to run the agent as the user who installed it, from a state +# directory in their home and a user unit. That user is the one sudo was run by. Their state is copied, +# never moved: the machine keeps its identity and what it already downloaded, +# and the old directory stays as it was. +OLD_HOME="" +if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then + OLD_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6)" +fi +OLD_STATE="${OLD_HOME:+$OLD_HOME/.nestri}" +if [ -n "$OLD_STATE" ] && [ -f "$OLD_STATE"/onboarding.json ] && [ ! -e "$STATE_DIR" ]; then + say "Moving this host's agent from $SUDO_USER to a system service…" + if command -v systemctl >/dev/null 2>&1; then + systemctl --user -M "$SUDO_USER@" disable --now nestri-host.service >/dev/null 2>&1 || true + fi + mkdir -p "$(dirname "$STATE_DIR")" + cp -a "$OLD_STATE" "$STATE_DIR" + # The box store that install was given, unless this run names another. + if [ -z "${NESTRI_BOX_STORE:-}" ] && [ -f "$OLD_HOME/.config/nestri-host/env" ]; then + NESTRI_BOX_STORE="$(sed -n 's/^NESLET_BOX_STORE=//p' "$OLD_HOME/.config/nestri-host/env" | head -n1)" + fi + say "Copied $OLD_STATE to $STATE_DIR." +fi # --- fetch ------------------------------------------------------------------ if command -v curl >/dev/null 2>&1; then @@ -106,15 +134,16 @@ say "" # again by onboarding on the new binary. A host with nothing running is # unaffected. if command -v systemctl >/dev/null 2>&1 && - systemctl --user is-active --quiet nestri-host.service 2>/dev/null; then + systemctl is-active --quiet nestri-host.service 2>/dev/null; then say "Stopping the running host agent to upgrade it…" - systemctl --user stop nestri-host.service + systemctl stop nestri-host.service fi # --- onboard ---------------------------------------------------------------- # The token goes through the environment rather than argv, so it is not in # `ps` for the length of the run. -export NESTRI_INSTALL_TOKEN="$TOKEN" NESTRI_BOX_STORE="$BOX_STORE" NESTRI_API="$API" +export NESTRI_INSTALL_TOKEN="$TOKEN" NESTRI_BOX_STORE="$BOX_STORE" NESTRI_API="$API" \ + NESLET_STATE_DIR="$STATE_DIR" if tty_ok; then exec "$BIN_DIR/nestri-host" onboard /artifacts/nvgpu/.manifest +# ─────────────────────────────────────────────────────────── +# virtio-nvgpu's host half — shipped beside the guest's, taken out on the host +# +# `vhost-user-nvgpu` serves a box's forwarded GPU and `nvgpu-userspace` +# stages the host's NVIDIA driver for its guests. They run on the host, never +# in a guest: the host agent's onboarding moves /boot/host out of this image +# the way it moves the kernel, and turns forwarding on when it finds the +# NVIDIA driver loaded. In this image so they are always from the same +# NVGPU_REF as the guest driver and the layer above. +# +# Static (musl), because they run on whatever distribution the host is, and +# this one's glibc is newer than most of them. +# ─────────────────────────────────────────────────────────── +FROM builder AS nvgpu-host-build +ARG NVGPU_GIT=https://github.com/nestrilabs/virtio-nvgpu.git +ARG NVGPU_REF=dev +RUN rustup target add x86_64-unknown-linux-musl +RUN --mount=type=cache,target=/root/.cargo/registry \ + git init -q /build/virtio-nvgpu && cd /build/virtio-nvgpu && \ + git fetch -q --depth=1 "${NVGPU_GIT}" "${NVGPU_REF}" && git checkout -q FETCH_HEAD && \ + cargo build --release --target x86_64-unknown-linux-musl -p device --features vhost-user \ + --bin vhost-user-nvgpu --bin nvgpu-userspace && \ + for b in vhost-user-nvgpu nvgpu-userspace; do \ + install -Dm755 "target/x86_64-unknown-linux-musl/release/$b" "/artifacts/nvgpu-host/boot/host/$b" && \ + strip "/artifacts/nvgpu-host/boot/host/$b"; \ + done && \ + git rev-parse HEAD > /artifacts/nvgpu-host/boot/host/virtio-nvgpu.rev && \ + cd / && rm -rf /build/virtio-nvgpu + # ─────────────────────────────────────────────────────────── # The guest kernel — shipped inside the image it boots # @@ -473,9 +502,11 @@ COPY --from=mesa-build /artifacts/mesa / COPY --from=nestri-build /artifacts/nestri / COPY --from=nvgpu-layer-build /artifacts/nvgpu / COPY --from=umu-build /artifacts/umu / -# The kernel this image is booted with, for the host to take out. See the -# kernel-build stage. +# The kernel this image is booted with, and the host's half of GPU +# forwarding, for the host to take out. See the kernel-build and +# nvgpu-host-build stages. COPY --from=kernel-build /artifacts/kernel / +COPY --from=nvgpu-host-build /artifacts/nvgpu-host / # **No Proton here.** It is attached to a box at run time, mounted at # /nestri/compat, so this image has no Proton path for a launch to fall back to