bitcoin-knots: convert to a role, de-Uptime-Kuma the health check
892-line playbook becomes 40 lines plus a role with install/build/configure/
service/healthcheck phases, five templates and one handler. bitcoin_knots_vars.yml
is deleted; its content is the role's defaults.
Verified after a real run: bitcoind still active since 2026-08-19 (NO restart),
chain at 966844 blocks / 875 GB, DATUM config intact, dbcache still 200, health
check timer firing again. changed=3, all health-check. vipy changed=0.
⚠ THE BIG ONE: the playbook would have deleted the mining integration.
bitcoin.conf on the node carries a section that was hand-added and was missing
from the template entirely:
blockmaxsize=3985000
blockmaxweight=3985000
blocknotify=killall -USR1 datum_gateway
maxmempool=1000
blockreconstructionextratxn=1000000
blocknotify is how datum_gateway learns a new block landed. Running the old
playbook would have stripped all of it and solo mining would have carried on
against a stale template - a silent failure that costs money rather than raising
an error. Also dbcache 200 -> 3528 (hand-tuned down; the calculation wants 90% of
RAM) and logging moved off the file. All now reconciled, dbcache behind
bitcoin_dbcache_mb_override.
bitcoin-knots and datum-gateway are ONE SYSTEM. Noted in the README.
AND MY OWN FIX MADE IT MORE DANGEROUS. The `Restart bitcoind` handler was guarded
by uptime_kuma_enabled, so it had been inert: bitcoin.conf and the systemd unit
both notify it and neither could restart anything - a config change applied to
disk, reported success, and never took effect. Ungating that is right, but it
converts "wrong config sitting inertly on disk" into "node restarted onto a
config that breaks mining". The ungating had to land WITH the template
reconciliation, not before it.
It also raises the bar permanently: any residual template/live difference now
restarts a Bitcoin node on every run. Four rounds of --check --diff to reach
changed=0 - the DATUM section, an explanatory comment that was rendering into the
deployed config (now a {# #} Jinja comment), a "# Pruning (optional)" comment the
live file had, and one trailing blank line.
The build path is 32 tasks all guarded by `not bitcoind_binary_exists.stat.exists`,
so a converged host skips the 30-60 minute compile and both `state: absent`
deletions. Those target /opt/bitcoin-knots/{source,bitcoin-<version>}; the chain
is in /mnt/knots_data and is never touched. Signature-verification tasks copied
verbatim.
The health check timer had last fired 2026-08-09 while reporting active/enabled -
same OnBootSec + OnUnitActiveSec dead chain as fulcrum. The role runs the check
once after enabling to supply the reference the timer schedules from.
Ownership parity checked mechanically against `git show HEAD:`, keyed by TASK
NAME rather than path - keying by path gave a false positive, because
bitcoin_knots_source_dir is created with ownership and later removed with
state: absent, so whichever task comes last wins and that differs between one
file and five. 13/13 match.
bitcoin_p2p_port and the tailscale hostname moved to services_config.yml for the
socket-proxy play on the edge host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e83191c029
commit
d26dc78b3c
19 changed files with 1163 additions and 1250 deletions
85
ansible/roles/bitcoin_knots/README.md
Normal file
85
ansible/roles/bitcoin_knots/README.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# `bitcoin_knots`
|
||||
|
||||
Builds Bitcoin Knots from source with PGP + SHA256 verification of the release
|
||||
tarball, runs it as a full node on `knots-box`, and keeps a health check on a
|
||||
systemd timer. The second play in the calling playbook publishes the P2P port
|
||||
from the edge host via `socket_proxy`.
|
||||
|
||||
Converted from `deploy_bitcoin_knots_playbook.yml` (892 lines) under Plan 6. The
|
||||
playbook is now 40 lines.
|
||||
|
||||
## The build is guarded; the chain is never touched
|
||||
|
||||
`build.yml` is 32 tasks, every one carrying
|
||||
`when: not bitcoind_binary_exists.stat.exists`. On a host that already has the
|
||||
binary the whole download / verify / 30-60 minute compile skips — **including the
|
||||
two `state: absent` deletions**, which target `/opt/bitcoin-knots/source` and the
|
||||
extracted build directory.
|
||||
|
||||
The chain lives elsewhere and nothing here touches it:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `bitcoin_knots_dir` | `/opt/bitcoin-knots` — build tree, safe to delete |
|
||||
| `bitcoin_data_dir` | `/var/lib/bitcoin` — config, logs, wallets |
|
||||
| `bitcoin_large_data_dir` | `/mnt/knots_data` — **~875 GB of blockchain** |
|
||||
|
||||
The signature-verification tasks are the security control of this role. They are
|
||||
copied verbatim; do not "simplify" them.
|
||||
|
||||
## ⚠ This node is half of the mining setup
|
||||
|
||||
`bitcoin.conf` carries a DATUM Gateway section that was hand-added on the node
|
||||
and was **missing from the playbook's template**:
|
||||
|
||||
```ini
|
||||
blockmaxsize=3985000
|
||||
blockmaxweight=3985000
|
||||
blocknotify=killall -USR1 datum_gateway
|
||||
maxmempool=1000
|
||||
blockreconstructionextratxn=1000000
|
||||
```
|
||||
|
||||
`blocknotify` is how `datum_gateway` learns a new block landed. Running the old
|
||||
playbook would have deleted all of it, and solo mining would have carried on
|
||||
grinding against a stale template — a silent failure that costs money rather
|
||||
than raising an error. The template now carries it behind
|
||||
`bitcoin_datum_gateway_enabled`.
|
||||
|
||||
**bitcoin-knots and datum-gateway are one system, not two services.** Changing
|
||||
either config means thinking about both.
|
||||
|
||||
## The restart handler, and why exactness matters now
|
||||
|
||||
The hand-written `Restart bitcoind` handler carried
|
||||
`when: uptime_kuma_enabled | default(false)`, so it had been inert since the
|
||||
decommissioning: `bitcoin.conf` and the systemd unit both notify it and neither
|
||||
could restart anything. A config change applied to disk, reported success, and
|
||||
never took effect.
|
||||
|
||||
It is ungated here — which raises the bar for the template. **Any** residual
|
||||
difference between the template and the live file, down to a trailing newline,
|
||||
means the task reports `changed` and restarts a Bitcoin node on every run. It
|
||||
took four rounds of `--check --diff` to reach `changed=0`: the DATUM section, an
|
||||
explanatory comment that was rendering into the deployed file (now a `{# #}`
|
||||
Jinja comment), a `# Pruning (optional)` comment the live file had, and one
|
||||
trailing blank line.
|
||||
|
||||
## `dbcache`
|
||||
|
||||
Computed as 90% of RAM unless `bitcoin_dbcache_mb_override` is set. The live node
|
||||
was hand-tuned to **200 MB**; the calculation produces 3528. As with fulcrum,
|
||||
`set_fact` outranks role defaults, so the *calculation* honours the override — a
|
||||
value pinned only in `defaults/` is silently ignored.
|
||||
|
||||
## Monitoring: one variable, no product knowledge
|
||||
|
||||
The check tests bitcoind's RPC and records the answer in its exit code, which
|
||||
systemd keeps: `systemctl is-failed bitcoin-knots-healthcheck.service`. Set
|
||||
`healthcheck_push_url` to report anywhere that accepts an HTTP ping.
|
||||
|
||||
The timer had last fired **2026-08-09** while still reporting `active` and
|
||||
`enabled` — the same `OnBootSec` + `OnUnitActiveSec` dead chain as fulcrum, where
|
||||
nothing re-arms it if the service does not run in a given boot. The role runs the
|
||||
check once after enabling, which both smoke-tests it and supplies the reference
|
||||
the timer schedules from.
|
||||
71
ansible/roles/bitcoin_knots/defaults/main.yml
Normal file
71
ansible/roles/bitcoin_knots/defaults/main.yml
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Bitcoin Knots Configuration Variables
|
||||
|
||||
# Version - REQUIRED: Specify exact version/tag to build
|
||||
bitcoin_knots_version: "v29.2.knots20251110" # Must specify exact version/tag
|
||||
bitcoin_knots_version_short: "29.2.knots20251110" # Version without 'v' prefix (for tarball URLs)
|
||||
|
||||
# Directories
|
||||
bitcoin_knots_dir: /opt/bitcoin-knots
|
||||
bitcoin_knots_source_dir: "{{ bitcoin_knots_dir }}/source"
|
||||
bitcoin_data_dir: /var/lib/bitcoin # Standard location for config, logs, wallets
|
||||
bitcoin_large_data_dir: /mnt/knots_data # Custom location for blockchain data (blocks, chainstate)
|
||||
bitcoin_conf_dir: /etc/bitcoin
|
||||
|
||||
# Network
|
||||
bitcoin_rpc_port: 8332
|
||||
# Shared with the socket-proxy play on the edge host, so it lives in
|
||||
# services_config.yml rather than only here.
|
||||
bitcoin_p2p_port: "{{ service_settings.bitcoin.p2p_port }}"
|
||||
bitcoin_rpc_bind: "0.0.0.0"
|
||||
|
||||
# Build options
|
||||
bitcoin_build_jobs: 4 # Parallel build jobs (-j flag), adjust based on CPU cores
|
||||
bitcoin_build_prefix: /usr/local
|
||||
|
||||
# Configuration options
|
||||
bitcoin_enable_txindex: true # Set to true if transaction index needed (REQUIRED for Electrum servers like Electrs/ElectrumX)
|
||||
bitcoin_max_connections: 125
|
||||
# dbcache will be calculated as 90% of host RAM automatically in playbook
|
||||
|
||||
# ZMQ Configuration
|
||||
bitcoin_zmq_enabled: true
|
||||
bitcoin_zmq_bind: "tcp://0.0.0.0"
|
||||
bitcoin_zmq_port_rawblock: 28332
|
||||
bitcoin_zmq_port_rawtx: 28333
|
||||
bitcoin_zmq_port_hashblock: 28334
|
||||
bitcoin_zmq_port_hashtx: 28335
|
||||
|
||||
# Service user
|
||||
bitcoin_user: bitcoin
|
||||
bitcoin_group: bitcoin
|
||||
|
||||
# --- Health check ----------------------------------------------------------
|
||||
# Checks bitcoind RPC and records the answer in its exit code, which systemd
|
||||
# keeps: `systemctl is-failed bitcoin-knots-healthcheck.service`.
|
||||
#
|
||||
# WHERE TO REPORT HEALTH — the one place to plug in monitoring. Empty means
|
||||
# check, exit honestly, report nowhere. Any endpoint accepting an HTTP ping
|
||||
# works; nothing here is specific to a monitoring product.
|
||||
healthcheck_push_url: ""
|
||||
|
||||
# --- Logging ----------------------------------------------------------------
|
||||
# The live node logs to a file. Set to "" to use printtoconsole=1 (journald).
|
||||
bitcoin_logfile: "{{ bitcoin_data_dir }}/debug.log"
|
||||
|
||||
# --- dbcache ----------------------------------------------------------------
|
||||
# Computed as 90% of RAM unless this is set. The live node was hand-tuned to
|
||||
# 200 MB; the calculation would have produced 3528. As with fulcrum, note that
|
||||
# set_fact outranks role defaults, so the CALCULATION has to honour this - a
|
||||
# value pinned only in defaults/ is silently ignored.
|
||||
bitcoin_dbcache_mb_override: 200
|
||||
|
||||
# --- DATUM Gateway ----------------------------------------------------------
|
||||
# This node feeds block templates to datum_gateway on knots-box. These settings
|
||||
# were hand-added to bitcoin.conf and were missing from the template, so a
|
||||
# playbook run would have removed them and broken the mining setup.
|
||||
bitcoin_datum_gateway_enabled: true
|
||||
bitcoin_blockmaxsize: 3985000
|
||||
bitcoin_blockmaxweight: 3985000
|
||||
bitcoin_blocknotify: "killall -USR1 datum_gateway"
|
||||
bitcoin_maxmempool: 1000
|
||||
bitcoin_blockreconstructionextratxn: 1000000
|
||||
14
ansible/roles/bitcoin_knots/handlers/main.yml
Normal file
14
ansible/roles/bitcoin_knots/handlers/main.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
# Ungated on purpose. The hand-written handler carried
|
||||
# when: uptime_kuma_enabled | default(false)
|
||||
# so it has been inert since the decommissioning. Two tasks notify it —
|
||||
# bitcoin.conf and the systemd unit — and neither could actually restart
|
||||
# bitcoind. A configuration change to a Bitcoin node therefore applied to disk,
|
||||
# reported success, and silently never took effect.
|
||||
#
|
||||
# Restarting bitcoind re-opens the chainstate; it does not reindex.
|
||||
- name: Restart bitcoind
|
||||
systemd:
|
||||
name: bitcoind
|
||||
state: restarted
|
||||
daemon_reload: yes
|
||||
222
ansible/roles/bitcoin_knots/tasks/build.yml
Normal file
222
ansible/roles/bitcoin_knots/tasks/build.yml
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
---
|
||||
# Every task here is guarded by `when: not bitcoind_binary_exists.stat.exists`,
|
||||
# so on a host that already has the binary the whole download / verify / build
|
||||
# sequence skips — including the two `state: absent` deletions, which target
|
||||
# /opt/bitcoin-knots/{source,bitcoin-<version>} and never the chain data in
|
||||
# /mnt/knots_data.
|
||||
- name: Check if bitcoind binary already exists
|
||||
stat:
|
||||
path: "{{ bitcoin_build_prefix }}/bin/bitcoind"
|
||||
register: bitcoind_binary_exists
|
||||
changed_when: false
|
||||
|
||||
- name: Install gnupg for signature verification
|
||||
apt:
|
||||
name: gnupg
|
||||
state: present
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Import Luke Dashjr's Bitcoin Knots signing key
|
||||
command: gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys 90C8019E36C2E964
|
||||
register: key_import
|
||||
changed_when: "'already in secret keyring' not in key_import.stdout and 'already in public keyring' not in key_import.stdout"
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
failed_when: key_import.rc != 0
|
||||
|
||||
- name: Display imported key fingerprint
|
||||
command: gpg --fingerprint 90C8019E36C2E964
|
||||
register: key_fingerprint
|
||||
changed_when: false
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Download SHA256SUMS file
|
||||
get_url:
|
||||
url: "https://bitcoinknots.org/files/{{ bitcoin_version_major }}.x/{{ bitcoin_knots_version_short }}/SHA256SUMS"
|
||||
dest: "/tmp/bitcoin-knots-{{ bitcoin_knots_version_short }}-SHA256SUMS"
|
||||
mode: '0644'
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Download SHA256SUMS.asc signature file
|
||||
get_url:
|
||||
url: "https://bitcoinknots.org/files/{{ bitcoin_version_major }}.x/{{ bitcoin_knots_version_short }}/SHA256SUMS.asc"
|
||||
dest: "/tmp/bitcoin-knots-{{ bitcoin_knots_version_short }}-SHA256SUMS.asc"
|
||||
mode: '0644'
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Verify PGP signature on SHA256SUMS file
|
||||
command: gpg --verify /tmp/bitcoin-knots-{{ bitcoin_knots_version_short }}-SHA256SUMS.asc /tmp/bitcoin-knots-{{ bitcoin_knots_version_short }}-SHA256SUMS
|
||||
register: sha256sums_verification
|
||||
changed_when: false
|
||||
failed_when: false # Don't fail here - check for 'Good signature' in next task
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
|
||||
- name: Display SHA256SUMS verification result
|
||||
debug:
|
||||
msg: "{{ sha256sums_verification.stdout_lines + sha256sums_verification.stderr_lines }}"
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Fail if SHA256SUMS signature verification failed
|
||||
fail:
|
||||
msg: "SHA256SUMS signature verification failed. Aborting build."
|
||||
when: not bitcoind_binary_exists.stat.exists and ('Good signature' not in sha256sums_verification.stdout and 'Good signature' not in sha256sums_verification.stderr)
|
||||
|
||||
- name: Remove any existing tarball to force fresh download
|
||||
file:
|
||||
path: /tmp/bitcoin-{{ bitcoin_knots_version_short }}.tar.gz
|
||||
state: absent
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Download Bitcoin Knots source tarball
|
||||
get_url:
|
||||
url: "{{ bitcoin_source_tarball_url }}"
|
||||
dest: "/tmp/bitcoin-{{ bitcoin_knots_version_short }}.tar.gz"
|
||||
mode: '0644'
|
||||
validate_certs: yes
|
||||
force: yes
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Calculate SHA256 checksum of downloaded tarball
|
||||
command: sha256sum /tmp/bitcoin-{{ bitcoin_knots_version_short }}.tar.gz
|
||||
register: tarball_checksum
|
||||
changed_when: false
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Extract expected checksum from SHA256SUMS file
|
||||
shell: grep "bitcoin-{{ bitcoin_knots_version_short }}.tar.gz" /tmp/bitcoin-knots-{{ bitcoin_knots_version_short }}-SHA256SUMS | awk '{print $1}'
|
||||
register: expected_checksum
|
||||
changed_when: false
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
failed_when: expected_checksum.stdout == ""
|
||||
|
||||
- name: Display checksum comparison
|
||||
debug:
|
||||
msg:
|
||||
- "Expected: {{ expected_checksum.stdout | trim }}"
|
||||
- "Actual: {{ tarball_checksum.stdout.split()[0] }}"
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Verify tarball checksum matches SHA256SUMS
|
||||
fail:
|
||||
msg: "Tarball checksum mismatch! Expected {{ expected_checksum.stdout | trim }}, got {{ tarball_checksum.stdout.split()[0] }}"
|
||||
when: not bitcoind_binary_exists.stat.exists and expected_checksum.stdout | trim != tarball_checksum.stdout.split()[0]
|
||||
|
||||
- name: Remove existing source directory if it exists (to force fresh extraction)
|
||||
file:
|
||||
path: "{{ bitcoin_knots_source_dir }}"
|
||||
state: absent
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Remove extracted directory if it exists (from previous runs)
|
||||
file:
|
||||
path: "{{ bitcoin_knots_dir }}/bitcoin-{{ bitcoin_knots_version_short }}"
|
||||
state: absent
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Extract verified source tarball
|
||||
unarchive:
|
||||
src: /tmp/bitcoin-{{ bitcoin_knots_version_short }}.tar.gz
|
||||
dest: "{{ bitcoin_knots_dir }}"
|
||||
remote_src: yes
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Check if extracted directory exists
|
||||
stat:
|
||||
path: "{{ bitcoin_knots_dir }}/bitcoin-{{ bitcoin_knots_version_short }}"
|
||||
register: extracted_dir_stat
|
||||
changed_when: false
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Rename extracted directory to expected name
|
||||
command: mv "{{ bitcoin_knots_dir }}/bitcoin-{{ bitcoin_knots_version_short }}" "{{ bitcoin_knots_source_dir }}"
|
||||
when: not bitcoind_binary_exists.stat.exists and extracted_dir_stat.stat.exists
|
||||
|
||||
- name: Check if CMakeLists.txt exists
|
||||
stat:
|
||||
path: "{{ bitcoin_knots_source_dir }}/CMakeLists.txt"
|
||||
register: cmake_exists
|
||||
changed_when: false
|
||||
when: not bitcoind_binary_exists.stat.exists
|
||||
|
||||
- name: Create CMake build directory
|
||||
file:
|
||||
path: "{{ bitcoin_knots_source_dir }}/build"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
when: not bitcoind_binary_exists.stat.exists and cmake_exists.stat.exists | default(false)
|
||||
|
||||
- name: Configure Bitcoin Knots build with CMake
|
||||
command: >
|
||||
cmake
|
||||
-DCMAKE_INSTALL_PREFIX={{ bitcoin_build_prefix }}
|
||||
-DBUILD_BITCOIN_WALLET=OFF
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DWITH_ZMQ=ON
|
||||
..
|
||||
args:
|
||||
chdir: "{{ bitcoin_knots_source_dir }}/build"
|
||||
when: not bitcoind_binary_exists.stat.exists and cmake_exists.stat.exists | default(false)
|
||||
register: configure_result
|
||||
changed_when: true
|
||||
|
||||
- name: Verify CMake enabled ZMQ
|
||||
shell: |
|
||||
set -e
|
||||
cd "{{ bitcoin_knots_source_dir }}/build"
|
||||
cmake -LAH .. | grep -iE 'ZMQ|WITH_ZMQ|ENABLE_ZMQ|USE_ZMQ'
|
||||
when: not bitcoind_binary_exists.stat.exists and cmake_exists.stat.exists | default(false)
|
||||
register: zmq_check
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if CMakeLists.txt not found
|
||||
fail:
|
||||
msg: "CMakeLists.txt not found in {{ bitcoin_knots_source_dir }}. Cannot build Bitcoin Knots."
|
||||
when: not bitcoind_binary_exists.stat.exists and not (cmake_exists.stat.exists | default(false))
|
||||
|
||||
- name: Build Bitcoin Knots with CMake (this may take 30-60+ minutes)
|
||||
command: cmake --build . -j{{ bitcoin_build_jobs }}
|
||||
args:
|
||||
chdir: "{{ bitcoin_knots_source_dir }}/build"
|
||||
when: not bitcoind_binary_exists.stat.exists and cmake_exists.stat.exists | default(false)
|
||||
async: 3600
|
||||
poll: 0
|
||||
register: build_result
|
||||
changed_when: true
|
||||
|
||||
- name: Check build status
|
||||
async_status:
|
||||
jid: "{{ build_result.ansible_job_id }}"
|
||||
register: build_job_result
|
||||
until: build_job_result.finished
|
||||
retries: 120
|
||||
delay: 60
|
||||
when: not bitcoind_binary_exists.stat.exists and build_result.ansible_job_id is defined
|
||||
|
||||
- name: Fail if build failed
|
||||
fail:
|
||||
msg: "Bitcoin Knots build failed: {{ build_job_result.msg }}"
|
||||
when: not bitcoind_binary_exists.stat.exists and build_result.ansible_job_id is defined and build_job_result.failed | default(false)
|
||||
|
||||
- name: Install Bitcoin Knots binaries
|
||||
command: cmake --install .
|
||||
args:
|
||||
chdir: "{{ bitcoin_knots_source_dir }}/build"
|
||||
when: not bitcoind_binary_exists.stat.exists and cmake_exists.stat.exists | default(false)
|
||||
changed_when: true
|
||||
|
||||
- name: Verify bitcoind binary exists
|
||||
stat:
|
||||
path: "{{ bitcoin_build_prefix }}/bin/bitcoind"
|
||||
register: bitcoind_installed
|
||||
changed_when: false
|
||||
|
||||
- name: Verify bitcoin-cli binary exists
|
||||
stat:
|
||||
path: "{{ bitcoin_build_prefix }}/bin/bitcoin-cli"
|
||||
register: bitcoin_cli_installed
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if binaries not found
|
||||
fail:
|
||||
msg: "Bitcoin Knots binaries not found after installation"
|
||||
when: not bitcoind_installed.stat.exists or not bitcoin_cli_installed.stat.exists
|
||||
20
ansible/roles/bitcoin_knots/tasks/configure.yml
Normal file
20
ansible/roles/bitcoin_knots/tasks/configure.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
# Ownership copied verbatim from the playbook this replaces; verified
|
||||
# mechanically against `git show HEAD:` rather than retyped from memory.
|
||||
- name: Create bitcoin.conf configuration file
|
||||
ansible.builtin.template:
|
||||
src: bitcoin.conf.j2
|
||||
dest: "{{ bitcoin_conf_dir }}/bitcoin.conf"
|
||||
owner: "{{ bitcoin_user }}"
|
||||
group: "{{ bitcoin_group }}"
|
||||
mode: '0640'
|
||||
notify: Restart bitcoind
|
||||
|
||||
- name: Create systemd service file for bitcoind
|
||||
ansible.builtin.template:
|
||||
src: bitcoind.service.j2
|
||||
dest: /etc/systemd/system/bitcoind.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: Restart bitcoind
|
||||
56
ansible/roles/bitcoin_knots/tasks/healthcheck.yml
Normal file
56
ansible/roles/bitcoin_knots/tasks/healthcheck.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
# Everything here answers "is bitcoind healthy" and records the answer. The
|
||||
# Uptime Kuma specifics that used to follow — an embedded Python script creating
|
||||
# monitors over the API, a /tmp credentials file, push-URL extraction and a
|
||||
# systemd Environment= rewrite — are gone. Where it reports is now one variable,
|
||||
# healthcheck_push_url. See the role README.
|
||||
- name: Install curl for health check script
|
||||
apt:
|
||||
name: curl
|
||||
state: present
|
||||
|
||||
- name: Create Bitcoin Knots health check script
|
||||
ansible.builtin.template:
|
||||
src: healthcheck.sh.j2
|
||||
dest: /usr/local/bin/bitcoin-knots-healthcheck-push.sh
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
validate: "bash -n %s"
|
||||
|
||||
- name: Create systemd service for Bitcoin Knots health check
|
||||
ansible.builtin.template:
|
||||
src: healthcheck.service.j2
|
||||
dest: /etc/systemd/system/bitcoin-knots-healthcheck.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Create systemd timer for Bitcoin Knots health check
|
||||
ansible.builtin.template:
|
||||
src: healthcheck.timer.j2
|
||||
dest: /etc/systemd/system/bitcoin-knots-healthcheck.timer
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Reload systemd daemon for health check
|
||||
systemd:
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Enable and restart the Bitcoin Knots health check timer
|
||||
systemd:
|
||||
name: bitcoin-knots-healthcheck.timer
|
||||
enabled: yes
|
||||
state: restarted
|
||||
daemon_reload: yes
|
||||
|
||||
# Runs the check once, which is both a smoke test and the thing that actually
|
||||
# arms the timer. This timer is OnBootSec + OnUnitActiveSec with no OnCalendar:
|
||||
# OnBootSec elapses once, and OnUnitActiveSec needs the SERVICE to have run this
|
||||
# boot to have anything to schedule from. Restarting the timer does not supply
|
||||
# that reference; running the service does. The live timer had last fired on
|
||||
# 2026-08-09 while still reporting `active` and `enabled`.
|
||||
- name: Run the Bitcoin Knots health check once to arm the timer
|
||||
command: systemctl start bitcoin-knots-healthcheck.service
|
||||
changed_when: false
|
||||
104
ansible/roles/bitcoin_knots/tasks/install.yml
Normal file
104
ansible/roles/bitcoin_knots/tasks/install.yml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
- name: Calculate dbcache as a share of system RAM
|
||||
set_fact:
|
||||
bitcoin_dbcache_mb: "{{ (ansible_memtotal_mb | float * 0.9) | int }}"
|
||||
when: bitcoin_dbcache_mb_override | string | length == 0
|
||||
|
||||
- name: Use the explicit dbcache override
|
||||
set_fact:
|
||||
bitcoin_dbcache_mb: "{{ bitcoin_dbcache_mb_override }}"
|
||||
when: bitcoin_dbcache_mb_override | string | length > 0
|
||||
changed_when: false
|
||||
|
||||
- name: Display calculated dbcache value
|
||||
debug:
|
||||
msg: "Setting dbcache to {{ bitcoin_dbcache_mb }} MB (90% of {{ ansible_memtotal_mb }} MB total RAM)"
|
||||
|
||||
|
||||
- name: Install build dependencies
|
||||
apt:
|
||||
name:
|
||||
- build-essential
|
||||
- libtool
|
||||
- autotools-dev
|
||||
- automake
|
||||
- pkg-config
|
||||
- bsdmainutils
|
||||
- python3
|
||||
- python3-pip
|
||||
- libevent-dev
|
||||
- libboost-system-dev
|
||||
- libboost-filesystem-dev
|
||||
- libboost-test-dev
|
||||
- libboost-thread-dev
|
||||
- libboost-chrono-dev
|
||||
- libboost-program-options-dev
|
||||
- libboost-dev
|
||||
- libssl-dev
|
||||
- libdb-dev
|
||||
- libminiupnpc-dev
|
||||
- libzmq3-dev
|
||||
- libnatpmp-dev
|
||||
- libsqlite3-dev
|
||||
- git
|
||||
- curl
|
||||
- wget
|
||||
- cmake
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Create bitcoin group
|
||||
group:
|
||||
name: "{{ bitcoin_group }}"
|
||||
system: yes
|
||||
state: present
|
||||
|
||||
- name: Create bitcoin user
|
||||
user:
|
||||
name: "{{ bitcoin_user }}"
|
||||
group: "{{ bitcoin_group }}"
|
||||
system: yes
|
||||
shell: /usr/sbin/nologin
|
||||
home: "{{ bitcoin_data_dir }}"
|
||||
create_home: yes
|
||||
state: present
|
||||
|
||||
- name: Create bitcoin-knots directory
|
||||
file:
|
||||
path: "{{ bitcoin_knots_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: Create bitcoin-knots source directory
|
||||
file:
|
||||
path: "{{ bitcoin_knots_source_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: Create bitcoin data directory (for config, logs, wallets)
|
||||
file:
|
||||
path: "{{ bitcoin_data_dir }}"
|
||||
state: directory
|
||||
owner: "{{ bitcoin_user }}"
|
||||
group: "{{ bitcoin_group }}"
|
||||
mode: '0750'
|
||||
|
||||
- name: Create bitcoin large data directory (for blockchain)
|
||||
file:
|
||||
path: "{{ bitcoin_large_data_dir }}"
|
||||
state: directory
|
||||
owner: "{{ bitcoin_user }}"
|
||||
group: "{{ bitcoin_group }}"
|
||||
mode: '0750'
|
||||
|
||||
- name: Create bitcoin config directory
|
||||
file:
|
||||
path: "{{ bitcoin_conf_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
8
ansible/roles/bitcoin_knots/tasks/main.yml
Normal file
8
ansible/roles/bitcoin_knots/tasks/main.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
# import_tasks, not include_tasks: static imports stay visible to --list-tasks,
|
||||
# which is how this conversion was verified against the playbook it replaced.
|
||||
- ansible.builtin.import_tasks: install.yml
|
||||
- ansible.builtin.import_tasks: build.yml
|
||||
- ansible.builtin.import_tasks: configure.yml
|
||||
- ansible.builtin.import_tasks: service.yml
|
||||
- ansible.builtin.import_tasks: healthcheck.yml
|
||||
37
ansible/roles/bitcoin_knots/tasks/service.yml
Normal file
37
ansible/roles/bitcoin_knots/tasks/service.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
- name: Reload systemd daemon
|
||||
systemd:
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Enable and start bitcoind service
|
||||
systemd:
|
||||
name: bitcoind
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
- name: Wait for bitcoind RPC to be available
|
||||
uri:
|
||||
url: "http://{{ bitcoin_rpc_bind }}:{{ bitcoin_rpc_port }}"
|
||||
method: POST
|
||||
body_format: json
|
||||
body:
|
||||
jsonrpc: "1.0"
|
||||
id: "healthcheck"
|
||||
method: "getblockchaininfo"
|
||||
params: []
|
||||
user: "{{ bitcoin_rpc_user }}"
|
||||
password: "{{ bitcoin_rpc_password }}"
|
||||
status_code: 200
|
||||
timeout: 10
|
||||
register: rpc_check
|
||||
until: rpc_check.status == 200
|
||||
retries: 30
|
||||
delay: 5
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Display RPC connection status
|
||||
debug:
|
||||
msg: "Bitcoin Knots RPC is {{ 'available' if rpc_check.status == 200 else 'not yet available' }}"
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
# DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11.
|
||||
67
ansible/roles/bitcoin_knots/templates/bitcoin.conf.j2
Normal file
67
ansible/roles/bitcoin_knots/templates/bitcoin.conf.j2
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Bitcoin Knots Configuration
|
||||
# Generated by Ansible
|
||||
|
||||
# Data directory (blockchain storage)
|
||||
datadir={{ bitcoin_large_data_dir }}
|
||||
|
||||
# RPC Configuration
|
||||
server=1
|
||||
rpcuser={{ bitcoin_rpc_user }}
|
||||
rpcpassword={{ bitcoin_rpc_password }}
|
||||
rpcbind={{ bitcoin_rpc_bind }}
|
||||
rpcport={{ bitcoin_rpc_port }}
|
||||
rpcallowip=0.0.0.0/0
|
||||
|
||||
# Network Configuration
|
||||
listen=1
|
||||
port={{ bitcoin_p2p_port }}
|
||||
maxconnections={{ bitcoin_max_connections }}
|
||||
|
||||
# Performance
|
||||
dbcache={{ bitcoin_dbcache_mb }}
|
||||
|
||||
# Transaction Index (optional)
|
||||
{% if bitcoin_enable_txindex %}
|
||||
txindex=1
|
||||
{% endif %}
|
||||
|
||||
{# The live node carries this comment and the template never produced it, so a
|
||||
run would have silently deleted it. Harmless in itself, but matching it keeps
|
||||
this task at `ok` - which means any future `changed` here is a real signal
|
||||
rather than known noise. #}
|
||||
# Pruning (optional)
|
||||
|
||||
# Logging
|
||||
logtimestamps=1
|
||||
{% if bitcoin_logfile %}
|
||||
logfile={{ bitcoin_logfile }}
|
||||
{% else %}
|
||||
printtoconsole=1
|
||||
{% endif %}
|
||||
|
||||
# ZMQ Configuration
|
||||
{% if bitcoin_zmq_enabled | default(false) %}
|
||||
zmqpubrawblock={{ bitcoin_zmq_bind }}:{{ bitcoin_zmq_port_rawblock }}
|
||||
zmqpubrawtx={{ bitcoin_zmq_bind }}:{{ bitcoin_zmq_port_rawtx }}
|
||||
zmqpubhashblock={{ bitcoin_zmq_bind }}:{{ bitcoin_zmq_port_hashblock }}
|
||||
zmqpubhashtx={{ bitcoin_zmq_bind }}:{{ bitcoin_zmq_port_hashtx }}
|
||||
{% endif %}
|
||||
|
||||
# Security
|
||||
disablewallet=1
|
||||
{% if bitcoin_datum_gateway_enabled %}
|
||||
|
||||
{# These were hand-added on the node and were NOT in this template, so running
|
||||
the playbook would have stripped them. blocknotify is how datum_gateway
|
||||
learns a new block landed; without it solo mining keeps grinding on a stale
|
||||
template - a silent failure that costs money rather than raising an error.
|
||||
Kept as a Jinja comment so the explanation stays in the repo and out of the
|
||||
deployed config. #}
|
||||
# Specific for DATUM gateway
|
||||
blockmaxsize={{ bitcoin_blockmaxsize }}
|
||||
blockmaxweight={{ bitcoin_blockmaxweight }}
|
||||
blocknotify={{ bitcoin_blocknotify }}
|
||||
maxmempool={{ bitcoin_maxmempool }}
|
||||
blockreconstructionextratxn={{ bitcoin_blockreconstructionextratxn }}
|
||||
|
||||
{% endif %}
|
||||
17
ansible/roles/bitcoin_knots/templates/bitcoind.service.j2
Normal file
17
ansible/roles/bitcoin_knots/templates/bitcoind.service.j2
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Bitcoin Knots daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ bitcoin_user }}
|
||||
Group={{ bitcoin_group }}
|
||||
ExecStart={{ bitcoin_build_prefix }}/bin/bitcoind -conf={{ bitcoin_conf_dir }}/bitcoin.conf
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutStopSec=600
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
14
ansible/roles/bitcoin_knots/templates/healthcheck.service.j2
Normal file
14
ansible/roles/bitcoin_knots/templates/healthcheck.service.j2
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=Bitcoin Knots Health Check
|
||||
After=network.target bitcoind.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
ExecStart=/usr/local/bin/bitcoin-knots-healthcheck-push.sh
|
||||
Environment=HEALTHCHECK_PUSH_URL={{ healthcheck_push_url }}
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
62
ansible/roles/bitcoin_knots/templates/healthcheck.sh.j2
Normal file
62
ansible/roles/bitcoin_knots/templates/healthcheck.sh.j2
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#!/bin/bash
|
||||
# Bitcoin Knots health check — managed by Ansible (roles/bitcoin_knots)
|
||||
#
|
||||
# The exit code is the answer and systemd keeps it:
|
||||
# systemctl is-failed bitcoin-knots-healthcheck.service
|
||||
# Reporting anywhere else is optional and generic.
|
||||
#
|
||||
#
|
||||
|
||||
RPC_HOST="{{ bitcoin_rpc_bind }}"
|
||||
RPC_PORT={{ bitcoin_rpc_port }}
|
||||
RPC_USER="{{ bitcoin_rpc_user }}"
|
||||
RPC_PASSWORD="{{ bitcoin_rpc_password }}"
|
||||
PUSH_URL="${HEALTHCHECK_PUSH_URL:-}"
|
||||
|
||||
# Check if bitcoind RPC is responding
|
||||
check_bitcoind() {
|
||||
local response
|
||||
response=$(curl -s --max-time 30 \
|
||||
--user "${RPC_USER}:${RPC_PASSWORD}" \
|
||||
--data-binary '{"jsonrpc":"1.0","id":"healthcheck","method":"getblockchaininfo","params":[]}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
"http://${RPC_HOST}:${RPC_PORT}" 2>&1)
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
# Check if response contains a non-null error
|
||||
# Successful responses have "error": null, failures have "error": {...}
|
||||
if echo "$response" | grep -q '"error":null\|"error": null'; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
report() {
|
||||
local status=$1
|
||||
local msg=$2
|
||||
|
||||
# No push URL is normal, not an error: the exit code below is still a
|
||||
# complete answer for anything reading unit state.
|
||||
[ -n "$PUSH_URL" ] || return 0
|
||||
|
||||
# URL encode spaces in message
|
||||
local encoded_msg="${msg// /%20}"
|
||||
|
||||
if ! curl -s --max-time 10 --retry 2 -o /dev/null \
|
||||
"${PUSH_URL}?status=${status}&msg=${encoded_msg}&ping="; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main health check
|
||||
if check_bitcoind; then
|
||||
report "up" "OK"
|
||||
exit 0
|
||||
else
|
||||
report "down" "bitcoind RPC not responding"
|
||||
exit 1
|
||||
fi
|
||||
11
ansible/roles/bitcoin_knots/templates/healthcheck.timer.j2
Normal file
11
ansible/roles/bitcoin_knots/templates/healthcheck.timer.j2
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[Unit]
|
||||
Description=Bitcoin Knots Health Check Timer
|
||||
Requires=bitcoind.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=1min
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Loading…
Add table
Add a link
Reference in a new issue