retire deploy-miner: the miner now deploys from quantus/miner #6
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
12
readme.md
12
readme.md
@@ -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 ⚠️
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user