diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 2ac077e7b9..4f0d162706 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -16,7 +16,10 @@ Install OpenShell with a single command: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` -The script detects your operating system and installs the OpenShell CLI and gateway with your native package manager. It then starts the local gateway server so you can begin creating sandboxes. +The script detects your operating system and installs the OpenShell CLI and +gateway. On Linux, the Snap path is preferred when `snapd` is available; +otherwise the script uses the native DEB or RPM package. The gateway then +starts automatically so you can begin creating sandboxes. You can also download release artifacts directly from the [OpenShell GitHub Releases](https://github.com/NVIDIA/OpenShell/releases) page. @@ -51,6 +54,12 @@ brew services restart openshell ## Linux +On distributions that ship with `snapd`, the install script uses the Snap path +described below, unless one of the following is true: +- a native (non-snap) Docker package is present (the `openshell` snap requires the `docker` snap, for now) +- `snapd` is not present +- `OPENSHELL_INSTALL_METHOD` is set in the environment to some value other than `snap` (e.g. `OPENSHELL_INSTALL_METHOD=deb`) + On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. On Debian and Ubuntu, the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. @@ -77,7 +86,12 @@ sudo loginctl enable-linger $USER ## Snap -Install the OpenShell snap from the Snap Store: +On Linux distributions that ship with `snapd`, the install script installs +OpenShell from the Snap Store. The OpenShell snap bundles the CLI, the terminal +UI, and a managed gateway daemon. `snapd` handles upgrades and rollback; the +gateway runs as a system service inside the snap. + +You can also install the snap directly: ```shell sudo snap install openshell @@ -94,19 +108,24 @@ typically `~/snap/openshell/common`. Gateway registrations live under `$SNAP_USER_COMMON/.config/openshell/gateways/` instead of `~/.config/openshell/gateways/`. -### Snap store installs - -When installing from the Snap Store, snapd automatically connects the `home`, -`network`, and `network-bind` plugs. The `docker` plug still -requires manual connection: +The OpenShell snap requires the Docker snap, which you can install with: ```shell -sudo snap connect openshell:docker docker:docker-daemon +sudo snap install docker ``` -The snap declares `default-provider: docker` on the Docker plug so snapd will -offer to install the Docker snap, but the connection itself must be made -manually. +### Snap store installs + +When installing from the Snap Store, snapd automatically connects the `docker`, +`home`, `log-observe`, `network`, `network-bind`, and `system-observe plugs. + +The `docker` interface is used to connect the `openshell` snap's `docker` plug +to the `docker` snap's `docker-daemon` slot; the `openshell` snap does not yet +work with a host-installed Docker Engine. The installer runs this and the other +interface connections for you after installing the `openshell` snap; run them +by hand if you install the snap manually. The installer is best-effort: if a +connect fails (for example because the `docker` snap is not yet running), the +snap still installs and the installer prints a warning. ### Locally built snap packages @@ -127,6 +146,26 @@ to read logs and inspect system processes. The `docker` plug requires the `docker:docker-daemon` slot from the Docker snap and does not work with system-installed Docker. +### Verify the gateway + +The snap-managed gateway service is `openshell.gateway`. Inspect it with: + +```shell +snap services openshell +snap logs -n 100 openshell.gateway +``` + +Register the gateway with the CLI: + +```shell +openshell gateway add http://127.0.0.1:17670 --local --name openshell +openshell status +``` + +The gateway listens on `http://127.0.0.1:17670` and stores its state under +`/var/snap/openshell/common/`. Override gateway settings by creating +`/var/snap/openshell/common/gateway.toml`. + ### Gateway service The gateway runs as a snap daemon with `refresh-mode: endure`, meaning snapd @@ -138,6 +177,37 @@ a snap refresh when you need the updated binary: sudo systemctl restart snap.openshell.gateway ``` +### When to choose Snap + +Use the `openshell` snap when `snapd` is available and no native Docker Engine +is installed, and you want atomic upgrades and rollback, a single self-contained +and sandboxed install, or a desktop launcher that surfaces the OpenShell +terminal UI in the application menu. + +Use the `openshell` `.deb` or `.rpm` file when `snapd` is unavailable or when +you already run Docker from a non-snap source. The installer falls back to +these methods on hosts with native Docker. + +### Force an install method + +Set `OPENSHELL_INSTALL_METHOD` to force a specific install method instead of +relying on auto-detection. Supported values: + +| Value | Install path | +|---|---| +| `deb` | Debian package via `apt`/`dpkg` | +| `rpm` | RPM package via `dnf`/`yum`/`zypper`/`rpm` | +| `snap` | Snap package via `snapd` | +| `homebrew` | Homebrew formula via `brew` (macOS only; not yet supported on Linux) | + +When the variable is unset, the script auto-detects the host package manager +as described above. If the requested method's package manager is not +installed, the installer aborts with an error. + +```shell +curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_INSTALL_METHOD=deb sh +``` + ## Kubernetes Kubernetes deployments use the OpenShell Helm chart. For step-by-step installation, refer to [Kubernetes Setup](/kubernetes/setup). For chart values and packaging details, refer to the [Helm chart README](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md). diff --git a/install.sh b/install.sh index 6cda59f8bc..c6757e87ce 100755 --- a/install.sh +++ b/install.sh @@ -51,6 +51,10 @@ OPTIONS: ENVIRONMENT VARIABLES: OPENSHELL_VERSION Release tag to install (default: latest tagged release). Set OPENSHELL_VERSION=dev to install the rolling dev build. + OPENSHELL_INSTALL_METHOD + Force a specific install method. Supported values: + deb, rpm, snap, homebrew. When unset, the script + auto-detects the host package manager. OPENSHELL_ACK_BREAKING_UPGRADE Set to 1 only after backing up and cleaning up a pre-v0.0.37 installation. @@ -60,9 +64,10 @@ NOTES: from ${GITHUB_URL}/releases/latest. Linux installs the Debian package on amd64/arm64 or the RPM packages on - x86_64/aarch64, depending on the host package manager. - macOS installs the release Homebrew formula on Apple Silicon and starts a - brew services-backed local gateway. + x86_64/aarch64, depending on the host package manager. When snapd is + available and no native Docker Engine is installed, the installer prefers + the Snap path. macOS installs the release Homebrew formula on Apple Silicon + and starts a brew services-backed local gateway. EOF } @@ -76,6 +81,14 @@ require_cmd() { fi } +has_snapd() { + has_cmd snap && systemctl is-active --quiet snapd.socket 2>/dev/null +} + +has_native_docker() { + has_cmd docker && ! snap list docker >/dev/null 2>&1 +} + download() { _url="$1" _output="$2" @@ -468,7 +481,33 @@ detect_platform() { } linux_package_method() { - if has_cmd dpkg; then + case "${OPENSHELL_INSTALL_METHOD:-}" in + "") + ;; + snap) + echo "snap" + return 0 + ;; + deb|apt) + echo "deb" + return 0 + ;; + rpm|dnf|yum|zypper) + echo "rpm" + return 0 + ;; + homebrew|brew) + echo "homebrew" + return 0 + ;; + *) + error "OPENSHELL_INSTALL_METHOD='${OPENSHELL_INSTALL_METHOD}' is not supported; use one of: deb, rpm, snap, homebrew" + ;; + esac + + if has_snapd && ! has_native_docker; then + echo "snap" + elif has_cmd dpkg; then echo "deb" elif has_cmd rpm; then echo "rpm" @@ -953,6 +992,75 @@ install_linux_rpm() { start_user_gateway } +install_linux_snap() { + require_cmd snap + set_linux_target_runtime_dir + + # Docker snap must be installed before openshell so that the + # openshell:docker -> docker:docker-daemon interface connection + # can succeed. The docker-daemon slot doesn't exist until the + # Docker snap itself is present. + if ! snap list docker >/dev/null 2>&1; then + info "docker snap not found, installing..." + as_root snap install docker + else + info "docker snap already present" + fi + + info "installing openshell snap..." + as_root snap install openshell + + info "installed openshell snap from Snap Store" + info "registering local gateway as ${TARGET_USER}..." + register_local_gateway_snap + wait_for_local_gateway_listener_snap + wait_for_local_gateway_status +} + +register_local_gateway_snap() { + _register_bin="${OPENSHELL_REGISTER_BIN:-openshell}" + + if _add_output="$($_register_bin gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell 2>&1)"; then + [ -z "$_add_output" ] || print_gateway_add_output "$_add_output" + return 0 + fi + + _add_status=$? + + case "$_add_output" in + *"already exists"*) + info "local gateway already exists; removing and re-adding it..." + remove_local_gateway_registration + "$_register_bin" gateway add "http://127.0.0.1:${LOCAL_GATEWAY_PORT}" --local --name openshell + ;; + *) + printf '%s\n' "$_add_output" >&2 + return "$_add_status" + ;; + esac +} + +wait_for_local_gateway_listener_snap() { + _timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT_SNAP:-90}" + _elapsed=0 + _last_output="" + _probe_url="http://127.0.0.1:${LOCAL_GATEWAY_PORT}/" + + info "waiting for local gateway listener to become reachable..." + while [ "$_elapsed" -lt "$_timeout" ]; do + if _last_output="$(curl -sS --max-time 2 "$_probe_url" 2>&1)"; then + info "local gateway listener is reachable" + return 0 + fi + sleep 1 + _elapsed=$((_elapsed + 1)) + done + + [ -z "$_last_output" ] || printf '%s +' "$_last_output" >&2 + error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s" +} + install_macos_homebrew() { check_macos_platform @@ -1033,20 +1141,36 @@ main() { case "$PLATFORM" in linux) - require_linux_package_glibc case "$(linux_package_method)" in + snap) + install_linux_snap + ;; deb) + require_linux_package_glibc install_linux_deb ;; rpm) + require_linux_package_glibc install_linux_rpm ;; + homebrew) + # TODO: implement install_linux_homebrew for Linuxbrew hosts + # (e.g. Universal Blue/Bazzite). Homebrew is accepted as a method + # value but not yet wired to an installer on Linux. + error "the 'homebrew' install method is not yet supported on Linux" + ;; *) error "unsupported Linux package method" ;; esac ;; darwin) + case "${OPENSHELL_INSTALL_METHOD:-}" in + ""|homebrew|brew) ;; + *) + error "only the 'homebrew' install method is supported on macOS" + ;; + esac install_macos_homebrew ;; *) diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index a1259cf0bc..d3474b1c2c 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -100,4 +100,132 @@ assert_glibc_preflight_fails \ "OpenShell Linux packages require glibc >= 2.28; detected musl or unsupported libc." \ setup_ldd_musl +assert_method_equals() { + local name=$1 + local value=$2 + local expected=$3 + + local result + if ! result="$(OPENSHELL_INSTALL_METHOD="$value" linux_package_method)" 2>"$err"; then + echo "FAIL: ${name}: expected '${expected}' but call failed" >&2 + cat "$err" >&2 || true + exit 1 + fi + + if [ "$result" != "$expected" ]; then + echo "FAIL: ${name}: expected '${expected}', got '${result}'" >&2 + exit 1 + fi +} + +assert_method_fails() { + local name=$1 + local value=$2 + local expected=$3 + + if (OPENSHELL_INSTALL_METHOD="$value" linux_package_method) >"$out" 2>"$err"; then + echo "FAIL: ${name}: expected failure for value '${value}'" >&2 + exit 1 + fi + + if ! grep -Fq "$expected" "$err"; then + echo "FAIL: ${name}: missing expected message" >&2 + echo "Expected: ${expected}" >&2 + echo "Actual:" >&2 + cat "$err" >&2 || true + exit 1 + fi +} + +assert_method_equals "method deb passes through" "deb" "deb" +assert_method_equals "method rpm passes through" "rpm" "rpm" +assert_method_equals "method snap passes through" "snap" "snap" +assert_method_equals "method homebrew passes through" "homebrew" "homebrew" + +assert_method_equals "alias apt maps to deb" "apt" "deb" +assert_method_equals "alias dnf maps to rpm" "dnf" "rpm" +assert_method_equals "alias yum maps to rpm" "yum" "rpm" +assert_method_equals "alias zypper maps to rpm" "zypper" "rpm" +assert_method_equals "alias brew maps to homebrew" "brew" "homebrew" + +assert_method_fails \ + "unknown method fails" \ + "apk" \ + "is not supported; use one of: deb, rpm, snap, homebrew" + +assert_method_fails \ + "classic method fails" \ + "classic" \ + "is not supported; use one of: deb, rpm, snap, homebrew" + +# Unset OPENSHELL_INSTALL_METHOD falls through to host detection. Shim the +# has_* helpers so the auto-detect path is deterministic regardless of the +# host running the tests. +assert_method_unset_detects() { + local name=$1 + local snapd=$2 + local native_docker=$3 + local dpkg=$4 + local rpm=$5 + local expected=$6 + + local result + if ! result="$( + unset OPENSHELL_INSTALL_METHOD + has_snapd() { [ "$snapd" = "1" ]; } + has_native_docker() { [ "$native_docker" = "1" ]; } + has_cmd() { + case "$1" in + dpkg) [ "$dpkg" = "1" ] ;; + rpm) [ "$rpm" = "1" ] ;; + *) return 1 ;; + esac + } + linux_package_method + )" 2>"$err"; then + echo "FAIL: ${name}: call failed" >&2 + cat "$err" >&2 || true + exit 1 + fi + + if [ "$result" != "$expected" ]; then + echo "FAIL: ${name}: expected '${expected}', got '${result}'" >&2 + exit 1 + fi +} + +assert_method_unset_detects \ + "unset prefers snap when snapd and no native docker" \ + 1 0 1 1 "snap" + +assert_method_unset_detects \ + "unset skips snap when native docker present" \ + 1 1 1 0 "deb" + +assert_method_unset_detects \ + "unset skips snap when snapd absent" \ + 0 0 1 0 "deb" + +assert_method_unset_detects \ + "unset falls back to rpm when no dpkg" \ + 0 0 0 1 "rpm" + +# Unset with no snapd, no dpkg, no rpm must error. +if ( + unset OPENSHELL_INSTALL_METHOD + has_snapd() { return 1; } + has_native_docker() { return 1; } + has_cmd() { return 1; } + linux_package_method +) >"$out" 2>"$err"; then + echo "FAIL: unset with no package managers should error" >&2 + exit 1 +fi + +if ! grep -Fq "Linux installs require either dpkg or rpm" "$err"; then + echo "FAIL: missing no-package-manager error" >&2 + cat "$err" >&2 || true + exit 1 +fi + echo "install.sh libc preflight tests passed"