ci / fmt (pull_request) Successful in 23s
bench / build (pull_request) Successful in 1m19s
ci / clippy (pull_request) Successful in 2m23s
ci / doc (pull_request) Successful in 2m29s
bench / measure (pull_request) Failing after 2m23s
ci / test (pull_request) Successful in 8m30s
The power limit and 810 MHz memory lock were applied at boot as a host setting, so they outlived the miner: a host switched back to LLM inference kept the lock and decoded at a sixth of its measured speed, with nothing reporting as throttled. - quantus-miner.service applies the profile in ExecStartPre and resets to driver defaults in ExecStopPost (nvidia-power-limit.sh, installed by lair/quantus). ExecStopPost runs on stop, crash and failed start. - deploy: on a host with neuron.service enabled, ship the files but do not enable or start the miner; validate passes there without checking hashing. neuron conflicts with the miner, so starting it would stop inference behind the operator's back. - bench-on-host.sh: stopping the unit now resets the clocks, so apply the miner profile for the measurement window (and read the asserted power limit after it), restore the prior profile afterwards, and refuse to measure on a host where neuron is active. - infra-setup.sh: sudoers for the two script invocations the bench needs. Re-run it on the mining hosts before the next deploy; the preflight will name the missing path until then. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XViS3RVfGTRaRSopLN68MA
141 lines
7.1 KiB
Bash
Executable File
141 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# One-time provisioning of a mining host for the CI deploy in
|
|
# .gitea/workflows/deploy.yaml (quantus/miner#8). The miner role of
|
|
# lair/quantus's script/infra-setup.sh, moved here so the deployer and its
|
|
# sudoers grant list live in the same repo and cannot drift: the deploy
|
|
# preflights the target's `sudo -n -l` against the SUDO block below.
|
|
#
|
|
# Convention: ~/git/architecture/deployment-gitea-actions.md §1–§2. Run from a
|
|
# workstation with admin (sudo) ssh access to the targets — NOT the gitea_ci
|
|
# account. Idempotent; re-running is a no-op. Skips unreachable hosts.
|
|
#
|
|
# ./deploy/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub
|
|
# ./deploy/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub --miner-hosts benjy.hanzalova.internal
|
|
#
|
|
# The runner keypair is NOT generated here. It already exists at
|
|
# ~/.ssh/id_gitea_ci and is shared by every project's RSYNC_SSH_KEY secret and
|
|
# every host's gitea_ci authorized_keys — regenerating it would silently break
|
|
# every other deploy on the fleet.
|
|
#
|
|
# Re-run this whenever the deploy gains a new file to ship or a new privileged
|
|
# command; the preflight fails up front naming the missing paths rather than
|
|
# dying partway through an rsync with "sudo: a password is required".
|
|
set -euo pipefail
|
|
|
|
ADMIN_USER="${ADMIN_USER:-$USER}"
|
|
# Must agree with the deploy matrix in .gitea/workflows/deploy.yaml.
|
|
MINER_HOSTS="${MINER_HOSTS-benjy.hanzalova.internal quadbrat.hanzalova.internal beast.hanzalova.internal}"
|
|
PUBKEY=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--pubkey) PUBKEY="$2"; shift 2 ;;
|
|
--miner-hosts) MINER_HOSTS="$2"; shift 2 ;;
|
|
--admin) ADMIN_USER="$2"; shift 2 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
[ -n "$PUBKEY" ] && [ -r "$PUBKEY" ] || { echo "--pubkey <file> is required and must be readable" >&2; exit 2; }
|
|
|
|
provision_miner() {
|
|
local host="$1"
|
|
echo "== ${host} (miner) =="
|
|
if ! ssh -o ConnectTimeout=8 -o BatchMode=yes "${ADMIN_USER}@${host}" true; then
|
|
echo " ! unreachable as ${ADMIN_USER} — skipping" >&2
|
|
return 1
|
|
fi
|
|
|
|
# All privileged work in one remote `sudo bash`. The runner pubkey is the only
|
|
# dynamic value, passed as $1 (single line, no quoting hazard). The sudoers
|
|
# `\=` are the required escapes — visudo rejects a bare `=` in a command arg.
|
|
# The `*` in an rsync line matches rsync's --server arg vector; the trailing
|
|
# literal destination is what actually bounds the rule.
|
|
ssh "${ADMIN_USER}@${host}" "sudo bash -seu -- '$(cat "$PUBKEY")'" <<'REMOTE'
|
|
PUBKEY="$1"
|
|
|
|
# /bin/bash, NOT nologin. The deploy runs `ssh gitea_ci@host <command>`, and
|
|
# a nologin shell refuses that with "This account is currently not
|
|
# available": the key authenticates, then the command cannot run.
|
|
if ! getent passwd gitea_ci >/dev/null; then
|
|
useradd --system --create-home --home-dir /var/lib/gitea_ci \
|
|
--shell /bin/bash gitea_ci
|
|
echo " + created gitea_ci"
|
|
else
|
|
echo " = gitea_ci already present"
|
|
fi
|
|
cur=$(getent passwd gitea_ci | cut -d: -f7)
|
|
if [ "$cur" != /bin/bash ]; then
|
|
usermod -s /bin/bash gitea_ci
|
|
echo " ~ gitea_ci shell was ${cur} — set to /bin/bash so ssh commands can run"
|
|
fi
|
|
|
|
install -d -o gitea_ci -g gitea_ci -m 0700 /var/lib/gitea_ci/.ssh
|
|
ak=/var/lib/gitea_ci/.ssh/authorized_keys
|
|
touch "$ak"
|
|
grep -qxF "$PUBKEY" "$ak" || printf '%s\n' "$PUBKEY" >> "$ak"
|
|
chown gitea_ci:gitea_ci "$ak"
|
|
chmod 0600 "$ak"
|
|
usermod -aG systemd-journal gitea_ci
|
|
|
|
# The GPU must be present and enumerable before a miner deploy is worth
|
|
# attempting; failing here beats a green deploy that mines nothing.
|
|
if ! command -v nvidia-smi >/dev/null; then
|
|
echo " ! nvidia-smi not found — this host has no usable NVIDIA driver" >&2
|
|
exit 1
|
|
fi
|
|
nvidia-smi --query-gpu=name --format=csv,noheader | sed 's/^/ = gpu: /'
|
|
|
|
tmp=/etc/sudoers.d/.quantus-miner_gitea_ci.tmp
|
|
cat > "$tmp" <<'SUDO'
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /usr/local/bin/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/systemd/system/quantus-miner.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/sysusers.d/quantus-miner.conf
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-miner/miner.env
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-miner/miner-auth-token
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/quantus-miner/miner-tls-cert-sha256
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * /etc/firewalld/services/quantus-miner-metrics.xml
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --get-default-zone
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --reload
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --permanent --zone\=* --add-rich-rule\=*
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --zone\=* --add-rich-rule\=*
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/firewall-cmd --zone\=* --query-rich-rule\=*
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemd-sysusers
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o root -g quantus-miner -m 0750 /etc/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -d -o quantus-miner -g quantus-miner -m 0750 /var/lib/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R /usr/local/bin/quantus-miner /etc/quantus-miner /var/lib/quantus-miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl enable quantus-miner.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl restart quantus-miner.service
|
|
# stop/start (not just restart) so the benchmark harness (bench.yaml) can pause
|
|
# mining for a measurement window and resume it afterwards.
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl stop quantus-miner.service
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/systemctl start quantus-miner.service
|
|
# The benchmark measures under the miner's GPU profile, which stopping the unit
|
|
# resets (its ExecStopPost). bench-on-host.sh re-applies it for the window and
|
|
# restores whichever profile the host was in afterwards.
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/local/bin/nvidia-power-limit.sh miner
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/local/bin/nvidia-power-limit.sh default
|
|
# Rollback: the deploy keeps the previous binary as .prev (cp) and restores it
|
|
# with install when validate fails; cp back would hit "Text file busy" on the
|
|
# running binary, install unlinks the destination first. Deploying from main means a bad commit reaches production;
|
|
# this is what makes that survivable.
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/cp -p /usr/local/bin/quantus-miner /usr/local/bin/quantus-miner.prev
|
|
gitea_ci ALL=(root) NOPASSWD: /usr/bin/install -m 0755 /usr/local/bin/quantus-miner.prev /usr/local/bin/quantus-miner
|
|
SUDO
|
|
chmod 0440 "$tmp"
|
|
visudo -cf "$tmp"
|
|
mv "$tmp" /etc/sudoers.d/quantus-miner_gitea_ci
|
|
echo " = sudoers quantus-miner_gitea_ci installed and visudo-verified"
|
|
REMOTE
|
|
}
|
|
|
|
rc=0
|
|
for h in $MINER_HOSTS; do
|
|
provision_miner "$h" || rc=1
|
|
done
|
|
echo
|
|
echo "done. remaining operator steps:"
|
|
echo " 1. confirm RSYNC_SSH_KEY is set in the repo's Actions secrets"
|
|
echo " 2. push to main, or run the deploy workflow manually (mode: validate first)"
|
|
exit $rc
|