cbcf122422
Haiku ops agent + 3 tools (nbshell expect+PTY admin-shell driver, nbapi read-only HTTP client, nb-substituters guarded cache manager) + 2 skills (nixbuild-settings, nixbuild-usage). Encodes the two-control-surface model and the path-style-substituter-URL gotcha.
113 lines
4.5 KiB
Bash
Executable File
113 lines
4.5 KiB
Bash
Executable File
#!/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 <host> shell`, and that shell is stubborn:
|
|
# - `ssh -T <host> shell` with piped stdin produces NO output (needs a PTY).
|
|
# - `ssh -tt <host> shell` is rejected: "pty not supported".
|
|
# - `ssh <host> shell <cmd>` (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
|