retire deploy-miner: the miner now deploys from quantus/miner #6

Merged
grenade merged 1 commits from retire/deploy-miner into main 2026-09-03 10:45:22 +00:00
8 changed files with 23 additions and 424 deletions

View File

@@ -1,13 +1,14 @@
name: deploy
# Deploy — or validate — Quantus Planck nodes and their external GPU miners.
# Deploy — or validate — Quantus Planck nodes, and the fleet monitoring for
# nodes and miners. The miners themselves deploy from quantus/miner.
#
# Topology (the workflow is the source of infra truth,
# ~/git/architecture/deployment-gitea-actions.md — hosts belong here, and only
# here; see readme.md "Scope"):
#
# quantus-node consensus, P2P, rewards
# quantus-node consensus, P2P, rewards (deployed here)
# ▲ QUIC/9833 (mesh only, rich-rule scoped to that node's miners)
# quantus-miner GPU search
# quantus-miner GPU search (deployed by quantus/miner)
#
# The node and the miner are separate processes on separate hosts by design: the
# node is disk+network bound and belongs on an always-on box, the miner is pure
@@ -37,9 +38,6 @@ on:
node_version:
description: quantus-node version (overrides the pin)
required: false
miner_version:
description: quantus-miner version (overrides the pin)
required: false
concurrency: # never half-apply two deploys at once
group: deploy
@@ -54,7 +52,9 @@ env:
# Pinned; bump deliberately to upgrade. An unattended upgrade of a validator
# is how you find out at 3am that a release changed a consensus rule.
NODE_VERSION: "0.11.1"
MINER_VERSION: "4.0.2"
# The miner is deployed by quantus/miner's own workflow, following its main
# branch (quantus/miner#8, lair/quantus#4). This repo no longer ships it;
# SCRAPE_MINERS below must still agree with that repo's deploy matrix.
# Ports — port-allocations.md §5 registry. These are upstream protocol
# defaults rather than derived numbers; see readme.md "Ports".
P2P_PORT: "30333"
@@ -74,7 +74,7 @@ env:
PROM_PORT: "26559"
GRAFANA_PORT: "28767"
# Scrape targets, rendered into the Prometheus scrape config. These must agree
# with the deploy-node / deploy-miner matrices below; `validate metrics`
# with the deploy-node matrix below and quantus/miner's deploy matrix; `validate metrics`
# asserts Prometheus actually has every target UP, so drift fails loudly rather
# than silently monitoring nothing.
SCRAPE_NODES: bob.hanzalova.internal
@@ -87,16 +87,13 @@ jobs:
runs-on: infra
outputs:
node_version: ${{ steps.v.outputs.node }}
miner_version: ${{ steps.v.outputs.miner }}
steps:
- id: v
run: |
set -euo pipefail
n="${{ github.event.inputs.node_version || env.NODE_VERSION }}"
m="${{ github.event.inputs.miner_version || env.MINER_VERSION }}"
echo "node=${n}" >> "$GITHUB_OUTPUT"
echo "miner=${m}" >> "$GITHUB_OUTPUT"
echo "quantus-node ${n} / quantus-miner ${m}"
echo "quantus-node ${n}"
- name: download quantus-node
run: |
@@ -109,16 +106,6 @@ jobs:
install -D -m 0755 "$(find . -name quantus-node -type f | head -1)" _bin/quantus-node
_bin/quantus-node --version
- name: download quantus-miner
run: |
set -euo pipefail
v="${{ steps.v.outputs.miner }}"
curl -fSL --retry 5 --retry-delay 5 --retry-all-errors --connect-timeout 15 \
-o _bin/quantus-miner \
"https://github.com/Quantus-Network/quantus-miner/releases/download/v${v}/quantus-miner-linux-x86_64"
chmod 0755 _bin/quantus-miner
_bin/quantus-miner --version
- uses: actions/upload-artifact@v3
with:
name: quantus-bin
@@ -589,232 +576,9 @@ jobs:
ssh -i ~/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new \
gitea_ci@${{ matrix.host }} journalctl -u quantus-node.service -n 80 --no-pager
deploy-miner:
runs-on: infra
needs: [fetch, deploy-node]
strategy:
fail-fast: false
matrix:
include:
# `node` is the host whose QUIC control channel this miner attaches
# to, and whose inner_hash therefore receives what it earns. A miner
# holds no reward configuration of its own — the mining protocol
# carries no payout address at all.
- host: benjy.hanzalova.internal
node: bob.hanzalova.internal
gpu_devices: "1" # 1× RTX 4090, measured 183 MH/s @ ~450 W
- host: quadbrat.hanzalova.internal
node: bob.hanzalova.internal
gpu_devices: "1" # 1× RTX 3060, hashrate not yet measured
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with: { name: quantus-bin, 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 infra-setup.sh grants for this role must already be
# permitted on the host. Adding an asset here without re-running
# infra-setup produces a bare "sudo: a password is required" forty
# lines into the deploy, after some files have already landed. Compare
# up front instead, from infra-setup.sh itself so the two cannot drift.
sed -n "/quantus-miner_gitea_ci.tmp/,/^SUDO$/p" script/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: ./script/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
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 }}" "$@"; }
# See the node job for why -c rather than rsync's default quick check.
# A rotated auth token or TLS pin shows up here as a content change and
# correctly forces a restart.
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 \
asset/systemd/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 — rather than once in infra-setup.sh — 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 ("invalid socket address syntax" on a hostname).
# Resolve on the MINER host — it is the one dialling, so its own view
# of the node's address is the correct one — and keep the 10.x
# literal out of the repo, same as the firewall rich rule.
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("asset/config/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
push --chmod=F0755 _bin/quantus-miner gitea_ci@"${{ matrix.host }}":/usr/local/bin/quantus-miner
push --chmod=F0644 asset/systemd/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. The miner binds metrics on 0.0.0.0
# unconditionally — there is no loopback option — so without this
# rule the port is simply closed, and with an unscoped one it would
# be open to the whole mesh.
rsync -e "ssh $SSHOPTS" --rsync-path='sudo rsync' -ic --mkpath --chmod=F0644 \
asset/firewalld/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
- name: validate miner
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
*"${{ needs.fetch.outputs.miner_version }}"*) echo "version matches pin" ;;
*) echo "version does NOT match pin ${{ needs.fetch.outputs.miner_version }}" >&2; fail=1 ;;
esac
echo "--- gpu ---"
# An `active` miner that found no adapter still looks healthy to
# systemd; assert the GPU is actually enumerated and busy.
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.
# Here-strings, not pipes — see the node's sync check for why.
m1=$(run "curl -fsS http://127.0.0.1:${{ env.MINER_METRICS_PORT }}/metrics")
h1=$(awk '/^miner_hashes_total/{print $2; exit}' <<<"$m1")
sleep 20
m2=$(run "curl -fsS http://127.0.0.1:${{ env.MINER_METRICS_PORT }}/metrics")
h2=$(awk '/^miner_hashes_total/{print $2; exit}' <<<"$m2")
echo " miner_hashes_total ${h1:-?} -> ${h2:-?}"
if [ -n "${h1:-}" ] && [ -n "${h2:-}" ] && [ "${h2%.*}" -gt "${h1%.*}" ]; then
echo " ok hash counter advancing"
else
echo " WARN counter not advancing — ${{ matrix.node }} may still be syncing," >&2
echo " which is expected and not itself a deploy failure." >&2
fi
exit $fail
- 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 80 --no-pager
deploy-metrics:
runs-on: infra
needs: [deploy-node, deploy-miner]
needs: [deploy-node]
# Monitoring assets only. Deliberately last and deliberately separate: a
# Grafana dashboard failing to provision must never be able to leave a
# half-deployed validator behind it.

View File

@@ -217,8 +217,8 @@ does not arbitrate. This is an open decision, not an oversight.
## Keep the fleet's shape in the workflow
Host assignments, hardware inventories, site or geographic names, ISPs and
addressing belong in the `deploy-node` / `deploy-miner` matrices in
`.gitea/workflows/deploy.yaml` — the single source of infra truth — and nowhere
else. Do not restate them in `readme.md`, `CLAUDE.md`, `doc/`, or unit-file
addressing belong in the `deploy-node` matrix in `.gitea/workflows/deploy.yaml`
(and, for the miners, the `deploy` matrix in quantus/miner) — the source of infra
truth — and nowhere else. Do not restate them in `readme.md`, `CLAUDE.md`, `doc/`, or unit-file
comments, even as helpful context. Prose should describe roles and measured
hardware classes, never which box is where.

View File

@@ -1,6 +0,0 @@
# Rendered by .gitea/workflows/deploy.yaml 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}}

View File

@@ -1,9 +0,0 @@
<?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>

View File

@@ -1,64 +0,0 @@
# 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

View File

@@ -1,4 +0,0 @@
#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

View File

@@ -10,8 +10,10 @@ Two components:
| `quantus-node` | consensus, P2P, reward address. Disk + network bound. |
| `quantus-miner` | Poseidon2-over-Goldilocks search. Pure GPU compute. |
**Scope:** which hosts run what lives in the `deploy-node` / `deploy-miner`
matrices in `.gitea/workflows/deploy.yaml` and nowhere else — that is the
**Scope:** which hosts run what lives in the `deploy-node` matrix in
`.gitea/workflows/deploy.yaml` and the `deploy` matrix in quantus/miner's
`.gitea/workflows/deploy.yaml` (the miner deploys from its own repo since
2026-09-03, lair/quantus#4), and nowhere else — that is the
convention (`deployment-gitea-actions.md`) and it keeps the fleet's shape in one
place instead of scattered through prose. Don't restate host assignments,
hardware inventories, sites or addressing in this file.
@@ -149,7 +151,7 @@ so that switching is a config change rather than a scramble:
| To switch | Change |
| --- | --- |
| Chain | `CHAIN:` in the workflow `env:` — the unit reads it from config, and the miner credential path `<base-path>/chains/<chain>/` follows from the same value |
| Versions | `NODE_VERSION` / `MINER_VERSION` pins |
| Versions | `NODE_VERSION` pin here; the miner follows quantus/miner `main` |
| Reward address | `inner_hash` per node in the matrix |
Still to decide before launch day, none of which the deploy can settle for you:
@@ -358,8 +360,8 @@ untouched deployment is still healthy.
no GPU adapter, and a node that is authoring nothing because no miner is
attached, both look perfectly healthy to systemd. Hence the counter check.
Upgrades are deliberate — bump `NODE_VERSION` / `MINER_VERSION` in the workflow.
There is no auto-update on a validator on purpose.
Node upgrades are deliberate — bump `NODE_VERSION` in the workflow. There is no
auto-update on a validator on purpose. The miner follows quantus/miner `main`.
## Known conflict: GPU contention with helexa ⚠️

View File

@@ -6,8 +6,8 @@
# node (--node-hosts) quantus-node binary + unit + config + firewalld,
# and read access to the two miner credential files
# the node generates -> deploy.yaml deploy-node
# miner (--miner-hosts) quantus-miner binary + unit + credentials
# -> deploy.yaml deploy-miner
# (the miner role moved to quantus/miner deploy/infra-setup.sh on 2026-09-03;
# that repo deploys the miner and owns its gitea_ci sudoers)
# metrics (--metrics-hosts) the fleet Prometheus/Grafana host: a scrape
# config drop-in and a provisioned dashboard
# -> deploy.yaml deploy-metrics
@@ -35,7 +35,7 @@
#
# ./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub
# ./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub --node-hosts "" \
# --miner-hosts benjy.hanzalova.internal
# --gpu-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
@@ -48,7 +48,6 @@ ADMIN_USER="${ADMIN_USER:-$USER}"
# this role", which is how you provision only the other one. With `:-` the empty
# value would silently fall back to the full default list.
NODE_HOSTS="${NODE_HOSTS-bob.hanzalova.internal}"
MINER_HOSTS="${MINER_HOSTS-benjy.hanzalova.internal quadbrat.hanzalova.internal}"
METRICS_HOSTS="${METRICS_HOSTS-golgafrinchans.kosherinata.internal}"
# The site reverse proxy that fronts quantus.internal. Empty to skip the edge role.
EDGE_HOSTS="${EDGE_HOSTS-hanzalova.internal}"
@@ -77,7 +76,6 @@ while [[ $# -gt 0 ]]; do
case "$1" in
--pubkey) PUBKEY="$2"; shift 2 ;;
--node-hosts) NODE_HOSTS="$2"; shift 2 ;;
--miner-hosts) MINER_HOSTS="$2"; shift 2 ;;
--metrics-hosts) METRICS_HOSTS="$2"; shift 2 ;;
--edge-hosts) EDGE_HOSTS="$2"; shift 2 ;;
--gpu-hosts) GPU_HOSTS="$2"; shift 2 ;;
@@ -194,85 +192,6 @@ SUDO
REMOTE
}
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
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. Every
# pre-existing gitea_ci on this fleet has bash, so a nologin one is a bug —
# repair it rather than leaving a host that can never be deployed to.
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 in quantus/miner#2 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
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
}
# The fleet Prometheus/Grafana host. Two of the things this does are edits to
# SHARED config that other projects also rely on, which is exactly why they live
# here (operator-run, one-time) and not in the deploy workflow:
@@ -553,9 +472,6 @@ for h in $NODE_HOSTS; do
provision_node "$h" || { rc=1; continue; }
ensure_node_key "$h" || rc=1
done
for h in $MINER_HOSTS; do
provision_miner "$h" || rc=1
done
for h in $METRICS_HOSTS; do
provision_metrics "$h" || rc=1
done