From e7a0ca9931a140f27a09a96fb5da941e52dbad53 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Fri, 8 May 2026 17:57:40 -0700 Subject: [PATCH 1/2] fix(install): support ostree rpm hosts Closes #1266 Install RPM release payloads into user-local paths on ostree hosts so immutable deployments do not require package layering or reboot before starting the gateway service. Signed-off-by: Drew Newberry --- docs/about/installation.mdx | 4 +- docs/get-started/quickstart.mdx | 2 +- install.sh | 247 +++++++++++++++++++++++++++----- scripts/test-install-ostree.sh | 137 ++++++++++++++++++ tasks/test.toml | 7 +- 5 files changed, 361 insertions(+), 36 deletions(-) create mode 100755 scripts/test-install-ostree.sh diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 25bf96480..5ba9e5151 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -16,7 +16,7 @@ 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 mutable Linux hosts and macOS, it uses the native package manager. On RPM-based ostree hosts, it installs the gateway into user-local paths so the immutable system deployment does not need package layering or a reboot. It then starts the local gateway server 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 +51,8 @@ brew services restart openshell 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 RPM-based ostree systems such as Fedora Silverblue, Fedora CoreOS, Bazzite, Aurora, and Bluefin, the install script extracts the RPM payloads into user-local paths instead of mutating `/usr`. The CLI and gateway are installed under `~/.local/bin`, helper scripts under `~/.local/libexec/openshell`, and the user service under `~/.config/systemd/user/openshell-gateway.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. The Debian user service listens on `https://127.0.0.1:17670` and generates a local mTLS bundle before the gateway starts. The CLI reads the client bundle from `~/.config/openshell/gateways/openshell/mtls/`. diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index fcfbc945b..bf8bed6b0 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -28,7 +28,7 @@ Run the install script: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` -The install script uses Homebrew, RPM, or a Debian package based on your machine. It starts the local gateway server after installation. +The install script uses Homebrew, RPM, or a Debian package based on your machine. On RPM-based ostree hosts, it uses a user-local install path instead of system package layering. It starts the local gateway server after installation. If you prefer [uv](https://docs.astral.sh/uv/): diff --git a/install.sh b/install.sh index 98d2d5d77..366912248 100755 --- a/install.sh +++ b/install.sh @@ -5,6 +5,8 @@ # Install OpenShell from a GitHub release. # # Linux installs either the Debian or RPM packages from the selected release. +# RPM-based ostree systems use a user-local install path so immutable /usr +# deployments do not require package layering or a reboot. # Apple Silicon macOS installs the generated Homebrew formula, so Homebrew owns # the binary layout and launchd service lifecycle. # @@ -54,7 +56,8 @@ 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. + x86_64/aarch64, depending on the host package manager. RPM-based ostree + hosts install into user-local paths instead of layering system packages. macOS installs the release Homebrew formula on Apple Silicon and starts a brew services-backed local gateway. EOF @@ -216,8 +219,18 @@ detect_platform() { esac } +is_ostree_system() { + [ "${OPENSHELL_TEST_OSTREE_BOOTED:-}" = "1" ] || [ -e /run/ostree-booted ] +} + linux_package_method() { - if has_cmd dpkg; then + if is_ostree_system; then + if has_cmd rpm; then + echo "rpm-ostree" + else + error "ostree Linux installs require rpm-compatible release assets" + fi + elif has_cmd dpkg; then echo "deb" elif has_cmd rpm; then echo "rpm" @@ -402,6 +415,175 @@ install_rpm_packages() { fi } +extract_rpm_payload() { + _rpm_path="$1" + _extract_dir="$2" + + require_cmd rpm2cpio + require_cmd cpio + + mkdir -p "$_extract_dir" + ( + cd "$_extract_dir" + rpm2cpio "$_rpm_path" | cpio -idm --quiet + ) +} + +supervisor_image_tag() { + case "$RELEASE_TAG" in + dev) + echo "dev" + ;; + *) + echo "latest" + ;; + esac +} + +write_ostree_gateway_unit() { + _unit_file="$1" + _gateway_bin="$2" + _init_pki="$3" + _init_gateway_env="$4" + _supervisor_tag="$(supervisor_image_tag)" + + cat >"$_unit_file" <&2 + if [ -n "${2:-}" ]; then + printf ' %s\n' "$2" >&2 + fi +} + +assert_eq() { + _actual="$1" + _expected="$2" + _label="$3" + + if [ "$_actual" = "$_expected" ]; then + pass "$_label" + else + fail "$_label" "expected '${_expected}', got '${_actual}'" + fi +} + +assert_contains() { + _file="$1" + _pattern="$2" + _label="$3" + + if grep -qF "$_pattern" "$_file"; then + pass "$_label" + else + fail "$_label" "expected '${_pattern}' in ${_file}" + fi +} + +make_fake_cmd() { + _dir="$1" + _name="$2" + + cat >"${_dir}/${_name}" <<'EOF' +#!/bin/sh +exit 0 +EOF + chmod 0755 "${_dir}/${_name}" +} + +test_ostree_detection_prefers_rpm_ostree() { + printf 'TEST: ostree detection routes to rpm-ostree install path\n' + + _tmpdir="$(mktemp -d)" + make_fake_cmd "$_tmpdir" rpm + make_fake_cmd "$_tmpdir" dpkg + + OPENSHELL_TEST_OSTREE_BOOTED=1 + PATH="${_tmpdir}:${ORIG_PATH}" + export OPENSHELL_TEST_OSTREE_BOOTED PATH + + _method="$(linux_package_method)" + assert_eq "$_method" "rpm-ostree" "ostree host uses rpm-ostree method" + + unset OPENSHELL_TEST_OSTREE_BOOTED + PATH="$ORIG_PATH" + export PATH + rm -rf "$_tmpdir" +} + +test_ostree_unit_uses_user_local_paths() { + printf 'TEST: generated ostree unit uses user-local paths and local gateway port\n' + + _tmpdir="$(mktemp -d)" + _unit="${_tmpdir}/openshell-gateway.service" + TARGET_HOME="${_tmpdir}/home" + RELEASE_TAG=dev + export TARGET_HOME RELEASE_TAG + + write_ostree_gateway_unit \ + "$_unit" \ + "${TARGET_HOME}/.local/bin/openshell-gateway" \ + "${TARGET_HOME}/.local/libexec/openshell/init-pki.sh" \ + "${TARGET_HOME}/.local/libexec/openshell/init-gateway-env.sh" + + assert_contains "$_unit" "ExecStart=${TARGET_HOME}/.local/bin/openshell-gateway" "ExecStart points to user-local gateway" + assert_contains "$_unit" "ExecStartPre=${TARGET_HOME}/.local/libexec/openshell/init-pki.sh %S/openshell/tls" "PKI helper points to user-local libexec" + assert_contains "$_unit" "ExecStartPre=${TARGET_HOME}/.local/libexec/openshell/init-gateway-env.sh %E/openshell/gateway.env" "env helper points to user-local libexec" + assert_contains "$_unit" "Environment=OPENSHELL_SERVER_PORT=17670" "unit uses installer gateway port" + assert_contains "$_unit" "Environment=OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/nvidia/openshell/supervisor:dev" "dev release uses dev supervisor image" + + rm -rf "$_tmpdir" +} + +test_stable_release_uses_latest_supervisor() { + printf 'TEST: stable release unit uses latest supervisor image\n' + + _tmpdir="$(mktemp -d)" + _unit="${_tmpdir}/openshell-gateway.service" + RELEASE_TAG=v0.0.37 + export RELEASE_TAG + + write_ostree_gateway_unit \ + "$_unit" \ + "${_tmpdir}/openshell-gateway" \ + "${_tmpdir}/init-pki.sh" \ + "${_tmpdir}/init-gateway-env.sh" + + assert_contains "$_unit" "Environment=OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/nvidia/openshell/supervisor:latest" "stable release uses latest supervisor image" + + rm -rf "$_tmpdir" +} + +printf '=== install.sh ostree tests ===\n\n' + +test_ostree_detection_prefers_rpm_ostree +echo "" +test_ostree_unit_uses_user_local_paths +echo "" +test_stable_release_uses_latest_supervisor + +printf '\n=== Results: %d passed, %d failed ===\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/tasks/test.toml b/tasks/test.toml index bf5741c72..ee695a0ce 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -5,7 +5,7 @@ [test] description = "Run all tests (Rust + Python)" -depends = ["test:rust", "test:python"] +depends = ["test:rust", "test:python", "test:install"] [e2e] description = "Run all end-to-end tests (Rust + Python)" @@ -27,6 +27,11 @@ env = { UV_NO_SYNC = "1" } run = "uv run pytest python/" hide = true +["test:install"] +description = "Run installer shell tests" +run = "scripts/test-install-ostree.sh" +hide = true + ["e2e:rust"] description = "Run Rust CLI e2e tests against a Docker-backed gateway" run = [ From 12727cb7523a2cd10fb9e134141c06ef0b347fd3 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Sat, 9 May 2026 11:38:33 -0700 Subject: [PATCH 2/2] fix(install): reuse rpm unit for ostree installs Stage the systemd user unit from the extracted RPM payload and patch only the user-local relocation and local gateway port fields. Remove the standalone installer shell test script and task hook. Signed-off-by: Drew Newberry --- install.sh | 112 ++++++++++++--------------- scripts/test-install-ostree.sh | 137 --------------------------------- tasks/test.toml | 7 +- 3 files changed, 52 insertions(+), 204 deletions(-) delete mode 100755 scripts/test-install-ostree.sh diff --git a/install.sh b/install.sh index 366912248..5ab986e61 100755 --- a/install.sh +++ b/install.sh @@ -220,7 +220,7 @@ detect_platform() { } is_ostree_system() { - [ "${OPENSHELL_TEST_OSTREE_BOOTED:-}" = "1" ] || [ -e /run/ostree-booted ] + [ -e /run/ostree-booted ] } linux_package_method() { @@ -429,63 +429,54 @@ extract_rpm_payload() { ) } -supervisor_image_tag() { - case "$RELEASE_TAG" in - dev) - echo "dev" - ;; - *) - echo "latest" - ;; - esac -} +stage_ostree_gateway_unit() { + _src_unit="$1" + _dst_unit="$2" + _gateway_bin="$3" + _init_pki="$4" + _init_gateway_env="$5" + + [ -e "$_src_unit" ] || error "expected systemd user unit not found in RPM payload: ${_src_unit}" + # Reuse the packaged RPM user unit and patch only the relocation details that + # differ for the ostree user-local install path. + if ! awk \ + -v gateway_bin="$_gateway_bin" \ + -v init_pki="$_init_pki" \ + -v init_gateway_env="$_init_gateway_env" \ + -v local_port="$LOCAL_GATEWAY_PORT" ' + /^Environment=OPENSHELL_SERVER_PORT=/ || + /^Environment=OPENSHELL_SSH_GATEWAY_HOST=/ || + /^Environment=OPENSHELL_SSH_GATEWAY_PORT=/ { + next + } -write_ostree_gateway_unit() { - _unit_file="$1" - _gateway_bin="$2" - _init_pki="$3" - _init_gateway_env="$4" - _supervisor_tag="$(supervisor_image_tag)" - - cat >"$_unit_file" <"$_dst_unit"; then + error "failed to stage ostree systemd unit from RPM payload: ${_src_unit}" + fi } install_ostree_file() { @@ -533,7 +524,8 @@ install_ostree_payload_files() { install_ostree_file "${_gateway_root}/usr/libexec/openshell/init-gateway-env.sh" "${_local_libexec}/init-gateway-env.sh" 0755 _staged_unit="${_tmpdir}/openshell-gateway.service" - write_ostree_gateway_unit \ + stage_ostree_gateway_unit \ + "${_gateway_root}/usr/lib/systemd/user/openshell-gateway.service" \ "$_staged_unit" \ "${_local_bin}/openshell-gateway" \ "${_local_libexec}/init-pki.sh" \ @@ -920,6 +912,4 @@ main() { esac } -if [ "${OPENSHELL_INSTALL_SOURCE_ONLY:-}" != "1" ]; then - main "$@" -fi +main "$@" diff --git a/scripts/test-install-ostree.sh b/scripts/test-install-ostree.sh deleted file mode 100755 index aa1bd22c5..000000000 --- a/scripts/test-install-ostree.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/bin/sh -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -set -eu - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -ORIG_PATH="${PATH}" - -OPENSHELL_INSTALL_SOURCE_ONLY=1 -export OPENSHELL_INSTALL_SOURCE_ONLY -. "${ROOT}/install.sh" - -PASS=0 -FAIL=0 - -pass() { - PASS=$((PASS + 1)) - printf ' PASS: %s\n' "$1" -} - -fail() { - FAIL=$((FAIL + 1)) - printf ' FAIL: %s\n' "$1" >&2 - if [ -n "${2:-}" ]; then - printf ' %s\n' "$2" >&2 - fi -} - -assert_eq() { - _actual="$1" - _expected="$2" - _label="$3" - - if [ "$_actual" = "$_expected" ]; then - pass "$_label" - else - fail "$_label" "expected '${_expected}', got '${_actual}'" - fi -} - -assert_contains() { - _file="$1" - _pattern="$2" - _label="$3" - - if grep -qF "$_pattern" "$_file"; then - pass "$_label" - else - fail "$_label" "expected '${_pattern}' in ${_file}" - fi -} - -make_fake_cmd() { - _dir="$1" - _name="$2" - - cat >"${_dir}/${_name}" <<'EOF' -#!/bin/sh -exit 0 -EOF - chmod 0755 "${_dir}/${_name}" -} - -test_ostree_detection_prefers_rpm_ostree() { - printf 'TEST: ostree detection routes to rpm-ostree install path\n' - - _tmpdir="$(mktemp -d)" - make_fake_cmd "$_tmpdir" rpm - make_fake_cmd "$_tmpdir" dpkg - - OPENSHELL_TEST_OSTREE_BOOTED=1 - PATH="${_tmpdir}:${ORIG_PATH}" - export OPENSHELL_TEST_OSTREE_BOOTED PATH - - _method="$(linux_package_method)" - assert_eq "$_method" "rpm-ostree" "ostree host uses rpm-ostree method" - - unset OPENSHELL_TEST_OSTREE_BOOTED - PATH="$ORIG_PATH" - export PATH - rm -rf "$_tmpdir" -} - -test_ostree_unit_uses_user_local_paths() { - printf 'TEST: generated ostree unit uses user-local paths and local gateway port\n' - - _tmpdir="$(mktemp -d)" - _unit="${_tmpdir}/openshell-gateway.service" - TARGET_HOME="${_tmpdir}/home" - RELEASE_TAG=dev - export TARGET_HOME RELEASE_TAG - - write_ostree_gateway_unit \ - "$_unit" \ - "${TARGET_HOME}/.local/bin/openshell-gateway" \ - "${TARGET_HOME}/.local/libexec/openshell/init-pki.sh" \ - "${TARGET_HOME}/.local/libexec/openshell/init-gateway-env.sh" - - assert_contains "$_unit" "ExecStart=${TARGET_HOME}/.local/bin/openshell-gateway" "ExecStart points to user-local gateway" - assert_contains "$_unit" "ExecStartPre=${TARGET_HOME}/.local/libexec/openshell/init-pki.sh %S/openshell/tls" "PKI helper points to user-local libexec" - assert_contains "$_unit" "ExecStartPre=${TARGET_HOME}/.local/libexec/openshell/init-gateway-env.sh %E/openshell/gateway.env" "env helper points to user-local libexec" - assert_contains "$_unit" "Environment=OPENSHELL_SERVER_PORT=17670" "unit uses installer gateway port" - assert_contains "$_unit" "Environment=OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/nvidia/openshell/supervisor:dev" "dev release uses dev supervisor image" - - rm -rf "$_tmpdir" -} - -test_stable_release_uses_latest_supervisor() { - printf 'TEST: stable release unit uses latest supervisor image\n' - - _tmpdir="$(mktemp -d)" - _unit="${_tmpdir}/openshell-gateway.service" - RELEASE_TAG=v0.0.37 - export RELEASE_TAG - - write_ostree_gateway_unit \ - "$_unit" \ - "${_tmpdir}/openshell-gateway" \ - "${_tmpdir}/init-pki.sh" \ - "${_tmpdir}/init-gateway-env.sh" - - assert_contains "$_unit" "Environment=OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/nvidia/openshell/supervisor:latest" "stable release uses latest supervisor image" - - rm -rf "$_tmpdir" -} - -printf '=== install.sh ostree tests ===\n\n' - -test_ostree_detection_prefers_rpm_ostree -echo "" -test_ostree_unit_uses_user_local_paths -echo "" -test_stable_release_uses_latest_supervisor - -printf '\n=== Results: %d passed, %d failed ===\n' "$PASS" "$FAIL" -[ "$FAIL" -eq 0 ] diff --git a/tasks/test.toml b/tasks/test.toml index ee695a0ce..bf5741c72 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -5,7 +5,7 @@ [test] description = "Run all tests (Rust + Python)" -depends = ["test:rust", "test:python", "test:install"] +depends = ["test:rust", "test:python"] [e2e] description = "Run all end-to-end tests (Rust + Python)" @@ -27,11 +27,6 @@ env = { UV_NO_SYNC = "1" } run = "uv run pytest python/" hide = true -["test:install"] -description = "Run installer shell tests" -run = "scripts/test-install-ostree.sh" -hide = true - ["e2e:rust"] description = "Run Rust CLI e2e tests against a Docker-backed gateway" run = [