#!/usr/bin/env bash # nbshell — drive nixbuild.net's interactive admin shell non-interactively. # # nixbuild.net's account administration (settings, ssh-keys, tokens, billing) # lives ONLY behind `ssh shell`, and that shell is stubborn: # - `ssh -T shell` with piped stdin produces NO output (needs a PTY). # - `ssh -tt shell` is rejected: "pty not supported". # - `ssh shell ` (command as ssh args) hits the *build-runner* # channel, which needs the `run:write` permission and fails with # "Authorization failed". # The only thing that works is a real local PTY wrapping `ssh -T ... shell`, # fed one command at a time and synchronised on the `nixbuild.net> ` prompt. # `expect` provides exactly that. (The HTTP API at api.nixbuild.net is # read-only — no settings endpoints — so it cannot substitute for this.) # # Usage: # nbshell 'settings substituters --show' # nbshell 'settings substituters --add https://cache.example.org' \ # 'settings substituters --show' # printf '%s\n' 'settings usage' | nbshell - # commands on stdin # # Each argument (or each stdin line) is one shell command line, run in order # in a single session. Output is cleaned: banner and `nixbuild.net> ` prompts # stripped, CRs removed. Exit code is expect's (0 unless the session failed). # # Env: # NIXBUILD_SSH_HOST override host (default: eu.nixbuild.net / userConfig) # NBSHELL_RAW=1 do not strip banner/prompts (debug) # NBSHELL_TIMEOUT per-prompt timeout seconds (default 30) # # Note: `exit` is NOT a valid shell command — the session ends on EOF, which # this script handles via expect `close`. set -euo pipefail HOST="${NIXBUILD_SSH_HOST:-eu.nixbuild.net}" TIMEOUT="${NBSHELL_TIMEOUT:-30}" # Collect commands from argv, or from stdin if the sole arg is "-". cmds=() if [ "$#" -eq 1 ] && [ "$1" = "-" ]; then while IFS= read -r line; do [ -n "$line" ] && cmds+=("$line") done else cmds=("$@") fi if [ "${#cmds[@]}" -eq 0 ]; then echo "nbshell: no commands given" >&2 echo "usage: nbshell 'settings substituters --show' [more commands...]" >&2 exit 2 fi # Locate expect: prefer a system binary, else run it ephemerally from nixpkgs # (no profile/-env install — matches the declarative-only house rule). if command -v expect >/dev/null 2>&1; then EXPECT=(expect) elif command -v nix >/dev/null 2>&1; then EXPECT=(nix run nixpkgs#expect --) else echo "nbshell: need either 'expect' on PATH or 'nix' to fetch it" >&2 exit 3 fi script="$(mktemp "${TMPDIR:-/tmp}/nbshell.XXXXXX.exp")" trap 'rm -f "$script"' EXIT { echo "set timeout ${TIMEOUT}" echo 'log_user 1' echo 'proc wp {} {' echo ' expect {' echo ' -re {nixbuild\.net>\s*$} { return }' echo ' timeout { puts stderr "nbshell: TIMEOUT waiting for prompt"; return }' echo ' }' echo '}' # -T: do NOT request a remote PTY (nixbuild rejects that); expect itself # provides the local PTY the shell needs to render. Disable mux so we get # a clean dedicated channel. echo "spawn ssh -T -o ControlMaster=no -o ControlPath=none ${HOST} shell" echo 'wp' for c in "${cmds[@]}"; do # Escape backslashes and double-quotes for the Tcl string literal. esc=${c//\\/\\\\} esc=${esc//\"/\\\"} echo "send \"${esc}\r\"" echo 'wp' done # The remote shell has no `exit` command and won't EOF on its own; close the # PTY ourselves. `close` then a bare `expect eof` races and throws # "spawn id ... not open" — wrap both so the session always ends cleanly # with exit status 0. echo 'catch { close }' echo 'catch { expect eof }' echo 'exit 0' } >"$script" run_expect() { "${EXPECT[@]}" "$script" 2>&1; } if [ "${NBSHELL_RAW:-0}" = "1" ]; then run_expect exit "${PIPESTATUS[0]}" fi # Clean output: drop the static welcome banner, SSH PQ/MOTD warnings, the # command listing, the spawn echo, any Tcl close/eof traceback, and the # `nixbuild.net> ` prompt prefix; normalise CRs and squeeze blank lines. run_expect | tr -d '\r' | grep -vE \ 'WARNING|store now, decrypt|may need to be upgraded|openssh\.com/pq|^\*\*|^•|nixbuild\.net •|^Welcome to nixbuild|^This shell allows|^and retrieve information|^Account id:|^Storage used:|^You have no free|^Free build time left:|^Available shell commands:|^ (help|settings|usage|ssh-keys|billing|tokens|builds) |^Run .help COMMAND|^For help on subcommands|^For more documentation|^https://docs\.nixbuild\.net/configuration|^spawn ssh |^spawn id |expect: spawn id|^ *while executing|^ *invoked from within|^"(expect eof|close)"|^ *\(file ' \ | sed -E 's/^nixbuild\.net> ?//' \ | cat -s