deploy: build on push to main and land the binary on the mining hosts
ci / fmt (pull_request) Successful in 24s
ci / clippy (pull_request) Successful in 1m38s
ci / doc (pull_request) Successful in 2m0s
ci / test (pull_request) Successful in 7m47s

quantus/miner#8. The miner is now deployed from this repo, not from
lair/quantus's release-pinned workflow (lair/quantus#4 retires that half).

- .gitea/workflows/deploy.yaml: build on cuda-13.0 (Fedora 43 like the
  hosts, and the runner with nvcc for #3) with the commit embedded, then per
  host in the matrix: sudoers preflight against deploy/infra-setup.sh,
  credentials copied from the node, miner.env rendered with the node's mesh
  IP resolved on the miner host, checksum-gated push of binary and unit,
  scoped firewalld rule for the exporter, restart only on change. Validate
  asserts the unit is active, --version carries this commit, the miner sees
  the matrix's device count, and miner_hashes_total advances within 120 s.
  If validate fails after a restart, the previous binary is restored.
- deploy/: the unit, sysusers, firewalld service and env template moved
  verbatim from lair/quantus asset/, plus infra-setup.sh with the miner role
  (and two new cp grants for the rollback). Applied to benjy and quadbrat.
- Matrix: benjy and quadbrat; beast present but commented out per the
  operating policy in #1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
This commit is contained in:
2026-09-03 13:28:52 +03:00
co-authored by Claude Fable 5.1
parent ad846af984
commit b9e1d21ec9
7 changed files with 598 additions and 0 deletions
+339
View File
@@ -0,0 +1,339 @@
---
# Deploy — or validate — the miner on the fleet's mining hosts (quantus/miner#8).
#
# Every push to main builds the binary and lands it on each host in the matrix,
# validated, with rollback. This workflow is the source of infra truth for the
# miner: hosts and per-host settings live in the `deploy` matrix and nowhere
# else. The node it attaches to is deployed by quantus/chain; the fleet's
# monitoring, GPU power limits and nvidia metrics stay in lair/quantus.
#
# Build on cuda-13.0: it is Fedora 43 like the hosts (the `rust` image is
# Fedora 44 and its binaries fail on the hosts with GLIBC_2.43 not found), and
# it is the only runner with nvcc for the CUDA engine (#3). Deploy on
# fedora-43: ssh + rsync are on every runner (gitea-runners.md §3).
name: deploy
on:
push:
branches: [main]
paths-ignore:
- "**.md"
- .gitea/workflows/ci.yml
- .gitea/workflows/bench.yaml
workflow_dispatch:
inputs:
mode:
description: "deploy (apply) or validate (check only, no changes)"
required: false
default: deploy
type: choice
options: [deploy, validate]
# Never half-apply two deploys at once. (Not relied on for correctness of a
# single host: the restart is checksum-gated and the binary is rsynced atomically.)
concurrency:
group: deploy-miner
cancel-in-progress: false
env:
# The chain spec: the miner credential path on the node host derives from it.
# Must agree with quantus/chain's deploy. Planck is a testnet; mainnet is
# expected 2026-09-09.
CHAIN: planck
MINER_LINK_PORT: "9833"
MINER_METRICS_PORT: "9900"
# Fleet Prometheus host; the miner's exporter is unauthenticated and bound to
# 0.0.0.0, so this is the only host allowed to reach it.
METRICS_HOST: golgafrinchans.kosherinata.internal
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: cuda-13.0
steps:
- uses: actions/checkout@v4
- name: build quantus-miner
env:
# Embedded by crates/miner-cli/build.rs into --version; validate below
# asserts the host runs exactly this commit.
MINER_BUILD_SHA: ${{ github.sha }}
run: |
set -euo pipefail
cargo build --release --locked -p miner-cli
./target/release/quantus-miner --version
- uses: actions/upload-artifact@v3
with:
name: quantus-miner
path: target/release/quantus-miner
deploy:
runs-on: fedora-43
needs: build
strategy:
fail-fast: false # one host's failure must not abort the other
matrix:
include:
# `node` is the host whose QUIC control channel this miner attaches
# to, and whose reward address therefore receives what it earns. The
# node's deploy (quantus/chain) must list this host in its `miners`
# so the firewalld rich rule admits it. lair/quantus's SCRAPE_MINERS
# must list it so Prometheus scrapes it.
#
# Policy until the mainnet call (quantus/miner#1): benjy and quadbrat
# mine testnet continuously; beast (2x RTX 5090) serves inference and
# does not mine. Enabling it is uncommenting the entry.
- host: benjy.hanzalova.internal
node: bob.hanzalova.internal
gpu_devices: "1" # 1x RTX 4090 (sm_89); reference card for #2
- host: quadbrat.hanzalova.internal
node: bob.hanzalova.internal
gpu_devices: "1" # 1x RTX 3060 (sm_86)
# - host: beast.hanzalova.internal
# node: bob.hanzalova.internal
# gpu_devices: "2" # 2x RTX 5090 (sm_120)
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with: { name: quantus-miner, path: _bin }
- name: write ssh key
run: |
set -euo pipefail
install -d -m 0700 ~/.ssh
printf '%s\n' "${{ secrets.RSYNC_SSH_KEY }}" > ~/.ssh/id_gitea_ci
chmod 0600 ~/.ssh/id_gitea_ci
- name: reachability
run: |
set -euo pipefail
ssh -i ~/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new \
gitea_ci@${{ matrix.host }} hostname -f
- name: preflight — sudoers covers this deploy
run: |
set -euo pipefail
SSHOPTS="-i $HOME/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new"
# Every path deploy/infra-setup.sh grants must already be permitted on
# the host. Compare up front, from the script itself so the two cannot
# drift, and name the missing paths instead of failing forty lines into
# an rsync with "sudo: a password is required".
sed -n "/quantus-miner_gitea_ci.tmp/,/^SUDO$/p" deploy/infra-setup.sh \
| grep '^gitea_ci ALL=' | grep -oE '(/etc|/usr|/var)/[^ ]*' | sort -u > expected-paths.txt
ssh $SSHOPTS gitea_ci@${{ matrix.host }} 'sudo -n -l' \
| grep -oE '(/etc|/usr|/var)/[^ ]*' | sort -u > permitted-paths.txt
comm -23 expected-paths.txt permitted-paths.txt > missing-paths.txt
if [ -s missing-paths.txt ]; then
echo "the miner sudoers on ${{ matrix.host }} is out of date." >&2
echo "not permitted, but this deploy needs them:" >&2
sed 's/^/ /' missing-paths.txt >&2
echo "" >&2
echo "run: ./deploy/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub" >&2
exit 1
fi
echo "sudoers covers all $(wc -l < expected-paths.txt) paths this deploy needs"
- name: deploy miner
id: deploy
if: ${{ github.event.inputs.mode != 'validate' }}
env:
GPU_DEVICES: ${{ matrix.gpu_devices }}
run: |
set -euo pipefail
SSHOPTS="-i $HOME/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new"
nrun() { ssh $SSHOPTS gitea_ci@"${{ matrix.node }}" "$@"; }
run() { ssh $SSHOPTS gitea_ci@"${{ matrix.host }}" "$@"; }
# -c (checksum), not rsync's default size+mtime quick check: the
# artifact is freshly built every run so mtimes always differ, and the
# default heuristic would report a change on every deploy. Restart is
# gated on an itemised content difference only.
RESTART=0
push() {
local out
out=$(rsync -e "ssh $SSHOPTS" --rsync-path='sudo rsync' -ic "$@")
if [ -n "$out" ]; then
RESTART=1
printf '%s\n' "$out" | sed 's/^/ changed: /'
fi
}
# 1. service account + dirs
push --mkpath --chmod=F0644 \
deploy/quantus-miner.sysusers.conf \
gitea_ci@"${{ matrix.host }}":/etc/sysusers.d/quantus-miner.conf
run sudo systemd-sysusers
run sudo install -d -o root -g quantus-miner -m 0750 /etc/quantus-miner
run sudo install -d -o quantus-miner -g quantus-miner -m 0750 /var/lib/quantus-miner
# 2. The miner's credentials are GENERATED BY THE NODE on first start
# and regenerate if the node's base-path is ever wiped. Copying them
# on every deploy is what makes that self-healing instead of a
# silent auth failure. They pass through the runner in memory,
# never the workspace.
umask 077
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
nrun sudo cat /var/lib/quantus-node/chains/${{ env.CHAIN }}/miner-auth-token \
> "$tmp/miner-auth-token"
nrun sudo cat /var/lib/quantus-node/chains/${{ env.CHAIN }}/miner-tls-cert-sha256 \
> "$tmp/miner-tls-cert-sha256"
test -s "$tmp/miner-auth-token" || { echo "node auth token empty — has ${{ matrix.node }} started?" >&2; exit 1; }
test -s "$tmp/miner-tls-cert-sha256" || { echo "node TLS pin empty — has ${{ matrix.node }} started?" >&2; exit 1; }
push --chown=root:quantus-miner --chmod=F0640 \
"$tmp/miner-auth-token" gitea_ci@"${{ matrix.host }}":/etc/quantus-miner/miner-auth-token
push --chown=root:quantus-miner --chmod=F0640 \
"$tmp/miner-tls-cert-sha256" gitea_ci@"${{ matrix.host }}":/etc/quantus-miner/miner-tls-cert-sha256
# 3. non-secret runtime config.
# --node-addr parses as a Rust SocketAddr: an IP and a port, with NO
# DNS resolution. Resolve on the MINER host — it is the one
# dialling — and keep the 10.x literal out of the repo.
node_addrs=$(run "getent ahostsv4 ${{ matrix.node }}")
node_ip=$(awk '{print $1; exit}' <<<"$node_addrs")
case "$node_ip" in
10.*) echo "node address: ${{ matrix.node }} -> ${node_ip}" ;;
*) echo "refusing to point the miner at non-mesh address '${node_ip}'" >&2; exit 1 ;;
esac
export NODE_ADDR="${node_ip}:${{ env.MINER_LINK_PORT }}"
python3 - <<'PY'
import os, pathlib
t = pathlib.Path("deploy/miner.env.tmpl").read_text()
t = t.replace("{{QUANTUS_NODE_ADDR}}", os.environ["NODE_ADDR"])
t = t.replace("{{QUANTUS_GPU_DEVICES}}", os.environ["GPU_DEVICES"])
pathlib.Path("miner.env").write_text(t)
PY
push --chown=root:quantus-miner --chmod=F0640 \
miner.env gitea_ci@"${{ matrix.host }}":/etc/quantus-miner/miner.env
# 4. binary + unit. Keep the running binary as .prev first so a failed
# validate can put it back (rollback step below).
if run test -x /usr/local/bin/quantus-miner; then
run sudo cp -p /usr/local/bin/quantus-miner /usr/local/bin/quantus-miner.prev
echo "previous binary kept: $(run /usr/local/bin/quantus-miner.prev --version)"
fi
push --chmod=F0755 _bin/quantus-miner gitea_ci@"${{ matrix.host }}":/usr/local/bin/quantus-miner
push --chmod=F0644 deploy/quantus-miner.service \
gitea_ci@"${{ matrix.host }}":/etc/systemd/system/quantus-miner.service
run sudo restorecon -R /usr/local/bin/quantus-miner /etc/quantus-miner /var/lib/quantus-miner
# 5. firewalld for the exporter, scoped to the scrape host. The miner
# binds metrics on 0.0.0.0 unconditionally.
rsync -e "ssh $SSHOPTS" --rsync-path='sudo rsync' -ic --mkpath --chmod=F0644 \
deploy/quantus-miner-metrics.xml \
gitea_ci@"${{ matrix.host }}":/etc/firewalld/services/quantus-miner-metrics.xml \
| sed 's/^/ changed: /'
run sudo firewall-cmd --reload
zone=$(run sudo firewall-cmd --get-default-zone)
metrics_addrs=$(run "getent ahostsv4 ${{ env.METRICS_HOST }}")
metrics_ip=$(awk '{print $1; exit}' <<<"$metrics_addrs")
case "$metrics_ip" in
10.*) echo "scrape source: ${metrics_ip}" ;;
*) echo "refusing to expose metrics to non-mesh address '${metrics_ip}'" >&2; exit 1 ;;
esac
mrich="rule family=ipv4 source address=${metrics_ip}/32 service name=quantus-miner-metrics accept"
if run "sudo firewall-cmd --zone=$zone --query-rich-rule='$mrich'"; then
echo "firewalld: metrics rich rule already present in ${zone}"
else
run "sudo firewall-cmd --permanent --zone=$zone --add-rich-rule='$mrich'"
run "sudo firewall-cmd --zone=$zone --add-rich-rule='$mrich'"
fi
run sudo systemctl enable quantus-miner.service # idempotent
if [ "$RESTART" = 1 ]; then
echo "changes applied — restarting"
run sudo systemctl daemon-reload
run sudo systemctl restart quantus-miner.service
elif run systemctl is-active --quiet quantus-miner.service; then
echo "nothing changed and the miner is running — left alone"
else
echo "nothing changed but the miner is down — starting it"
run sudo systemctl restart quantus-miner.service
fi
echo "restarted=$RESTART" >> "$GITHUB_OUTPUT"
- name: validate miner
id: validate
run: |
set -euo pipefail
SSHOPTS="-i $HOME/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new"
run() { ssh $SSHOPTS gitea_ci@"${{ matrix.host }}" "$@"; }
fail=0
echo "--- unit (${{ matrix.host }} -> ${{ matrix.node }}) ---"
run systemctl is-active quantus-miner.service
echo "--- version ---"
got=$(run /usr/local/bin/quantus-miner --version)
echo "installed: ${got}"
case "$got" in
*"${{ github.sha }}"*) echo "binary is this commit" ;;
*)
if [ "${{ github.event.inputs.mode }}" = validate ]; then
echo "note: installed binary is not this commit (validate mode; not a failure)"
else
echo "installed binary does NOT carry commit ${{ github.sha }}" >&2; fail=1
fi ;;
esac
echo "--- gpu ---"
# An `active` miner that found no adapter still looks healthy to
# systemd; assert the GPU is enumerated and that the miner sees the
# number of devices the matrix says it should.
run "nvidia-smi --query-gpu=name,power.draw,utilization.gpu --format=csv,noheader"
echo "--- hashing ---"
# The counter is the only honest evidence this process is doing work
# rather than idling on a failed connection. After a restart the miner
# must reconnect and receive a job, so poll rather than sample twice.
# Here-strings, not pipes: a pipe whose reader exits early SIGPIPEs the
# writer and pipefail turns that into exit 141.
deadline=$((SECONDS + 120))
m=$(run "curl -fsS http://127.0.0.1:${{ env.MINER_METRICS_PORT }}/metrics")
h1=$(awk '/^miner_hashes_total/{print $2; exit}' <<<"$m")
devs=$(awk '/^miner_gpu_devices/{print $2; exit}' <<<"$m")
if [ "${devs%.*}" != "${{ matrix.gpu_devices }}" ]; then
echo "miner_gpu_devices is ${devs:-?}, matrix says ${{ matrix.gpu_devices }}" >&2; fail=1
else
echo " ok miner_gpu_devices ${devs%.*}"
fi
ok=0
while [ $SECONDS -lt $deadline ]; do
sleep 10
m=$(run "curl -fsS http://127.0.0.1:${{ env.MINER_METRICS_PORT }}/metrics")
h2=$(awk '/^miner_hashes_total/{print $2; exit}' <<<"$m")
if [ -n "${h1:-}" ] && [ -n "${h2:-}" ] && [ "${h2%.*}" -gt "${h1%.*}" ]; then
echo " ok miner_hashes_total ${h1%.*} -> ${h2%.*}"
ok=1; break
fi
done
if [ "$ok" != 1 ]; then
echo " miner_hashes_total did not advance within 120s (${h1:-?} -> ${h2:-?})" >&2
fail=1
fi
exit $fail
- name: rollback
# Only when this run replaced the binary and validate then failed.
if: ${{ failure() && steps.deploy.outputs.restarted == '1' }}
run: |
set -euo pipefail
SSHOPTS="-i $HOME/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new"
run() { ssh $SSHOPTS gitea_ci@"${{ matrix.host }}" "$@"; }
if run test -x /usr/local/bin/quantus-miner.prev; then
echo "validate failed after a restart — restoring the previous binary"
run sudo cp -p /usr/local/bin/quantus-miner.prev /usr/local/bin/quantus-miner
run sudo restorecon -R /usr/local/bin/quantus-miner /etc/quantus-miner /var/lib/quantus-miner
run sudo systemctl restart quantus-miner.service
echo "restored: $(run /usr/local/bin/quantus-miner --version)"
else
echo "no previous binary to restore" >&2
fi
- name: journal
if: always()
run: |
ssh -i ~/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new \
gitea_ci@${{ matrix.host }} journalctl -u quantus-miner.service -n 60 --no-pager
+42
View File
@@ -0,0 +1,42 @@
# deploy
Everything the CI deploy of the miner ships or needs (`.gitea/workflows/deploy.yaml`).
Moved from `lair/quantus` on 2026-09-03 so that one repo owns the deployed miner;
the fleet's monitoring, GPU power limits and nvidia metrics stay there.
| File | Lands at |
| --- | --- |
| `quantus-miner.service` | `/etc/systemd/system/quantus-miner.service` |
| `quantus-miner.sysusers.conf` | `/etc/sysusers.d/quantus-miner.conf` |
| `quantus-miner-metrics.xml` | `/etc/firewalld/services/quantus-miner-metrics.xml` |
| `miner.env.tmpl` | rendered to `/etc/quantus-miner/miner.env` |
| `infra-setup.sh` | operator-run once per host; installs the `gitea_ci` sudoers |
The miner's credentials are generated by the node and copied host-to-host by the
deploy on every run. Nothing secret lives here.
## Traps, all previously hit
- **Adding a file or privileged command means re-running `infra-setup.sh`.** The
deploy preflights the host's `sudo -n -l` against the script's SUDO block and
names the missing paths, instead of failing partway with `sudo: a password is required`.
- **Deploys must be no-ops when nothing changed.** `push()` uses `rsync -ic`
(checksum). The artifact is rebuilt every run so mtimes always differ. Restart
only on an itemised content change.
- **`--node-addr` takes an IP, not a name.** It parses as a Rust `SocketAddr`
with no DNS. The deploy resolves the node on the miner host and renders the
literal; the `10.x` never enters the repo.
- **Pipes in validate steps.** The shell is `bash -e -o pipefail`; a reader that
exits early (`awk ... exit`, `grep -q`, `head`) SIGPIPEs the writer, exit 141.
Capture into a variable and parse with a here-string.
- **ssh argument quoting.** `run()` is `ssh ... "$@"` and the remote shell
re-splits; any argument with a space is passed as one pre-quoted string.
- **`PrivateDevices=false`** in the unit is deliberate: the miner needs `/dev/nvidia*`.
- **`systemctl is-active` is not evidence of mining.** A miner that found no
adapter, or never got a job, is `active`. Validate asserts `miner_hashes_total`
advances and `miner_gpu_devices` matches the matrix.
- **Binaries must be built on `cuda-13.0`** (Fedora 43, like the hosts). The
`rust` runner is Fedora 44; its binaries fail with `GLIBC_2.43 not found`.
- **A benchmark (`bench.yaml`) stops the miner on its host.** A deploy landing
on the same host during a measurement would find the unit down and start it
mid-window. Both are serialised only by not running them together.
+134
View File
@@ -0,0 +1,134 @@
#!/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}"
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
# Rollback: the deploy keeps the previous binary as .prev and restores it when
# validate fails. 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/cp -p /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
+6
View File
@@ -0,0 +1,6 @@
# Rendered by .gitea/workflows/deploy.yaml (quantus/miner) and rsynced to
# /etc/quantus-miner/miner.env (0640 root:quantus-miner). Not secret — the
# miner's actual credentials are the auth token and TLS pin, which are copied
# from the node host as separate 0640 files.
QUANTUS_NODE_ADDR={{QUANTUS_NODE_ADDR}}
QUANTUS_GPU_DEVICES={{QUANTUS_GPU_DEVICES}}
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>quantus-miner-metrics</short>
<description>Quantus miner Prometheus exporter. The miner binds this on
0.0.0.0 unconditionally (crates/metrics: SocketAddr from [0,0,0,0]) — there is
no loopback option — so the firewalld rich rule scoped to the scrape host is
the only thing bounding who can reach it.</description>
<port protocol="tcp" port="9900"/>
</service>
+64
View File
@@ -0,0 +1,64 @@
# Quantus external miner. Runs on the GPU host and connects OUT to the node's
# QUIC control channel; it listens on nothing but its loopback metrics port, so
# it ships no firewalld service of its own.
#
# Hardened per ~/git/architecture/generic.md §8, with one deliberate relaxation:
#
# PrivateDevices=false
# The miner needs /dev/nvidia*; PrivateDevices=true hides them and wgpu
# finds no adapter, silently falling back to nothing.
#
# Poseidon2-over-Goldilocks is compute-bound in registers, not memory-bound, so
# there is no tuning knob here worth more than the GPU power limit — which is a
# host-level concern (nvidia-smi -pl), not a unit-file one.
#
# --cpu-workers 0 is deliberate: a current-gen desktop CPU contributes ~4 MH/s
# next to a 4090's ~183 MH/s, for a couple hundred watts. Measured, not assumed.
[Unit]
Description=Quantus external miner (GPU)
Documentation=https://github.com/Quantus-Network/quantus-miner
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=quantus-miner
Group=quantus-miner
Environment=RUST_LOG=info
WorkingDirectory=/var/lib/quantus-miner
ExecStart=/usr/local/bin/quantus-miner serve \
--node-addr ${QUANTUS_NODE_ADDR} \
--auth-token-file /etc/quantus-miner/miner-auth-token \
--tls-cert-sha256-file /etc/quantus-miner/miner-tls-cert-sha256 \
--gpu-devices ${QUANTUS_GPU_DEVICES} \
--cpu-workers 0 \
--metrics-port 9900
EnvironmentFile=/etc/quantus-miner/miner.env
# The node may be down, still syncing, or mid-redeploy; reconnecting is normal
# operation, not an error condition.
Restart=always
RestartSec=15s
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=false
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
SystemCallArchitectures=native
MemoryDenyWriteExecute=false
ReadWritePaths=/var/lib/quantus-miner
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target
+4
View File
@@ -0,0 +1,4 @@
#Type Name ID GECOS Home directory Shell
u quantus-miner - "Quantus miner service account" /var/lib/quantus-miner /usr/sbin/nologin
m quantus-miner video
m quantus-miner render