personal_infra/ansible/services/bitcoin-knots/deploy_bitcoin_knots_playbook.yml

735 lines
25 KiB
YAML
Raw Normal View History

2025-12-08 10:34:04 +01:00
- name: Build and Deploy Bitcoin Knots from Source
hosts: knots_box_local
become: yes
vars_files:
- ../../infra_vars.yml
- ../../services_config.yml
- ../../infra_secrets.yml
- ./bitcoin_knots_vars.yml
vars:
bitcoin_repo_url: "https://github.com/bitcoinknots/bitcoin.git"
bitcoin_sigs_base_url: "https://raw.githubusercontent.com/bitcoinknots/guix.sigs/knots"
bitcoin_version_major: "{{ bitcoin_knots_version_short | regex_replace('^(\\d+)\\..*', '\\1') }}"
bitcoin_source_tarball_url: "https://bitcoinknots.org/files/{{ bitcoin_version_major }}.x/{{ bitcoin_knots_version_short }}/bitcoin-{{ bitcoin_knots_version_short }}.tar.gz"
uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}"
tasks:
- name: Calculate 90% of system RAM for dbcache
set_fact:
bitcoin_dbcache_mb: "{{ (ansible_memtotal_mb | float * 0.9) | int }}"
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'
- 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: sha256sums_verification.rc != 0
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
..
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: 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
- name: Create bitcoin.conf configuration file
copy:
dest: "{{ bitcoin_conf_dir }}/bitcoin.conf"
content: |
# 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=127.0.0.1
# 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 %}
# Pruning (optional)
{% if bitcoin_enable_prune %}
prune={{ bitcoin_enable_prune }}
{% endif %}
# Logging
logtimestamps=1
logfile={{ bitcoin_data_dir }}/debug.log
# Security
disablewallet=1
owner: "{{ bitcoin_user }}"
group: "{{ bitcoin_group }}"
mode: '0640'
notify: Restart bitcoind
- name: Create systemd service file for bitcoind
copy:
dest: /etc/systemd/system/bitcoind.service
content: |
[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
owner: root
group: root
mode: '0644'
notify: Restart bitcoind
- 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' }}"
- name: Allow Bitcoin P2P port on Tailscale interface only
ufw:
rule: allow
direction: in
port: "{{ bitcoin_p2p_port }}"
proto: tcp
interface: "{{ bitcoin_tailscale_interface }}"
comment: "Bitcoin Knots P2P (Tailscale only)"
- name: Allow Bitcoin P2P port (UDP) on Tailscale interface only
ufw:
rule: allow
direction: in
port: "{{ bitcoin_p2p_port }}"
proto: udp
interface: "{{ bitcoin_tailscale_interface }}"
comment: "Bitcoin Knots P2P UDP (Tailscale only)"
- name: Verify UFW rules for Bitcoin Knots
command: ufw status numbered
register: ufw_status
changed_when: false
- name: Display UFW status
debug:
msg: "{{ ufw_status.stdout_lines }}"
- name: Create Bitcoin Knots health check and push script
copy:
dest: /usr/local/bin/bitcoin-knots-healthcheck-push.sh
content: |
#!/bin/bash
#
# Bitcoin Knots Health Check and Push to Uptime Kuma
# Checks if bitcoind RPC is responding and pushes status to Uptime Kuma
#
RPC_HOST="{{ bitcoin_rpc_bind }}"
RPC_PORT={{ bitcoin_rpc_port }}
RPC_USER="{{ bitcoin_rpc_user }}"
RPC_PASSWORD="{{ bitcoin_rpc_password }}"
UPTIME_KUMA_PUSH_URL="${UPTIME_KUMA_PUSH_URL}"
# Check if bitcoind RPC is responding
check_bitcoind() {
local response
response=$(curl -s --max-time 5 \
--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 error
if echo "$response" | grep -q '"error"'; then
return 1
else
return 0
fi
else
return 1
fi
}
# Push status to Uptime Kuma
push_to_uptime_kuma() {
local status=$1
local msg=$2
if [ -z "$UPTIME_KUMA_PUSH_URL" ]; then
echo "ERROR: UPTIME_KUMA_PUSH_URL not set"
return 1
fi
# URL encode the message
local encoded_msg=$(echo -n "$msg" | curl -Gso /dev/null -w %{url_effective} --data-urlencode "msg=$msg" "" | cut -c 3-)
curl -s --max-time 10 --retry 2 -o /dev/null \
"${UPTIME_KUMA_PUSH_URL}?status=${status}&msg=${encoded_msg}&ping=" || true
}
# Main health check
if check_bitcoind; then
push_to_uptime_kuma "up" "OK"
exit 0
else
push_to_uptime_kuma "down" "bitcoind RPC not responding"
exit 1
fi
owner: root
group: root
mode: '0755'
- name: Install curl for health check script
apt:
name: curl
state: present
- name: Create systemd timer for Bitcoin Knots health check
copy:
dest: /etc/systemd/system/bitcoin-knots-healthcheck.timer
content: |
[Unit]
Description=Bitcoin Knots Health Check Timer
Requires=bitcoind.service
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Persistent=true
[Install]
WantedBy=timers.target
owner: root
group: root
mode: '0644'
- name: Create systemd service for Bitcoin Knots health check
copy:
dest: /etc/systemd/system/bitcoin-knots-healthcheck.service
content: |
[Unit]
Description=Bitcoin Knots Health Check and Push to Uptime Kuma
After=network.target bitcoind.service
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/bitcoin-knots-healthcheck-push.sh
Environment=UPTIME_KUMA_PUSH_URL=
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
owner: root
group: root
mode: '0644'
- name: Reload systemd daemon for health check
systemd:
daemon_reload: yes
- name: Enable and start Bitcoin Knots health check timer
systemd:
name: bitcoin-knots-healthcheck.timer
enabled: yes
state: started
- name: Create Uptime Kuma push monitor setup script for Bitcoin Knots
delegate_to: localhost
become: no
copy:
dest: /tmp/setup_bitcoin_knots_monitor.py
content: |
#!/usr/bin/env python3
import sys
import traceback
import yaml
from uptime_kuma_api import UptimeKumaApi, MonitorType
try:
# Load configs
with open('/tmp/ansible_config.yml', 'r') as f:
config = yaml.safe_load(f)
url = config['uptime_kuma_url']
username = config['username']
password = config['password']
monitor_name = config['monitor_name']
# Connect to Uptime Kuma
api = UptimeKumaApi(url, timeout=30)
api.login(username, password)
# Get all monitors
monitors = api.get_monitors()
# Find or create "services" group
group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None)
if not group:
group_result = api.add_monitor(type='group', name='services')
# Refresh to get the group with id
monitors = api.get_monitors()
group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None)
# Check if monitor already exists
existing_monitor = None
for monitor in monitors:
if monitor.get('name') == monitor_name:
existing_monitor = monitor
break
# Get ntfy notification ID
notifications = api.get_notifications()
ntfy_notification_id = None
for notif in notifications:
if notif.get('type') == 'ntfy':
ntfy_notification_id = notif.get('id')
break
if existing_monitor:
print(f"Monitor '{monitor_name}' already exists (ID: {existing_monitor['id']})")
# Get push URL from existing monitor
push_id = existing_monitor.get('push_token', existing_monitor.get('id'))
push_url = f"{url}/api/push/{push_id}"
print(f"Push URL: {push_url}")
print("Skipping - monitor already configured")
else:
print(f"Creating push monitor '{monitor_name}'...")
result = api.add_monitor(
type=MonitorType.PUSH,
name=monitor_name,
parent=group['id'],
interval=60,
maxretries=3,
retryInterval=60,
notificationIDList={ntfy_notification_id: True} if ntfy_notification_id else {}
)
# Get push URL from created monitor
monitors = api.get_monitors()
new_monitor = next((m for m in monitors if m.get('name') == monitor_name), None)
if new_monitor:
push_id = new_monitor.get('push_token', new_monitor.get('id'))
push_url = f"{url}/api/push/{push_id}"
print(f"Push URL: {push_url}")
api.disconnect()
print("SUCCESS")
except Exception as e:
error_msg = str(e) if str(e) else repr(e)
print(f"ERROR: {error_msg}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
sys.exit(1)
mode: '0755'
- name: Create temporary config for monitor setup
delegate_to: localhost
become: no
copy:
dest: /tmp/ansible_config.yml
content: |
uptime_kuma_url: "{{ uptime_kuma_api_url }}"
username: "{{ uptime_kuma_username }}"
password: "{{ uptime_kuma_password }}"
monitor_name: "Bitcoin Knots"
mode: '0644'
- name: Run Uptime Kuma push monitor setup
command: python3 /tmp/setup_bitcoin_knots_monitor.py
delegate_to: localhost
become: no
register: monitor_setup
changed_when: "'SUCCESS' in monitor_setup.stdout"
ignore_errors: yes
- name: Extract push URL from monitor setup output
set_fact:
uptime_kuma_push_url: "{{ monitor_setup.stdout | regex_search('Push URL: (https?://[^\\s]+)', '\\1') | first | default('') }}"
delegate_to: localhost
become: no
when: monitor_setup.stdout is defined
- name: Display extracted push URL
debug:
msg: "Uptime Kuma Push URL: {{ uptime_kuma_push_url }}"
when: uptime_kuma_push_url | default('') != ''
- name: Set push URL in systemd service environment
lineinfile:
path: /etc/systemd/system/bitcoin-knots-healthcheck.service
regexp: '^Environment=UPTIME_KUMA_PUSH_URL='
line: "Environment=UPTIME_KUMA_PUSH_URL={{ uptime_kuma_push_url }}"
state: present
insertafter: '^\[Service\]'
when: uptime_kuma_push_url | default('') != ''
- name: Reload systemd daemon after push URL update
systemd:
daemon_reload: yes
when: uptime_kuma_push_url | default('') != ''
- name: Restart health check timer to pick up new environment
systemd:
name: bitcoin-knots-healthcheck.timer
state: restarted
when: uptime_kuma_push_url | default('') != ''
- name: Clean up temporary files
delegate_to: localhost
become: no
file:
path: "{{ item }}"
state: absent
loop:
- /tmp/setup_bitcoin_knots_monitor.py
- /tmp/ansible_config.yml
handlers:
- name: Restart bitcoind
systemd:
name: bitcoind
state: restarted