- 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: 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 - 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=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 %} # Logging (to journald via systemd) logtimestamps=1 printtoconsole=1 # 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 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: 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 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 } # 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 spaces in message local encoded_msg="${msg// /%20}" if ! curl -s --max-time 10 --retry 2 -o /dev/null \ "${UPTIME_KUMA_PUSH_URL}?status=${status}&msg=${encoded_msg}&ping="; then echo "ERROR: Failed to push to Uptime Kuma" return 1 fi } # 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']})") push_token = existing_monitor.get('pushToken') or existing_monitor.get('push_token') if not push_token: raise ValueError("Could not find push token for monitor") push_url = f"{url}/api/push/{push_token}" print(f"Push URL: {push_url}") else: print(f"Creating push monitor '{monitor_name}'...") 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 {} ) monitors = api.get_monitors() new_monitor = next((m for m in monitors if m.get('name') == monitor_name), None) if new_monitor: push_token = new_monitor.get('pushToken') or new_monitor.get('push_token') if not push_token: raise ValueError("Could not find push token for new monitor") push_url = f"{url}/api/push/{push_token}" 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 - name: Setup public Bitcoin P2P forwarding on vipy via systemd-socket-proxyd hosts: vipy become: yes vars_files: - ../../infra_vars.yml - ../../services_config.yml - ../../infra_secrets.yml - ./bitcoin_knots_vars.yml vars: bitcoin_tailscale_hostname: "knots-box" uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" tasks: - name: Create Bitcoin P2P proxy socket unit copy: dest: /etc/systemd/system/bitcoin-p2p-proxy.socket content: | [Unit] Description=Bitcoin P2P Proxy Socket [Socket] ListenStream={{ bitcoin_p2p_port }} [Install] WantedBy=sockets.target owner: root group: root mode: '0644' notify: Restart bitcoin-p2p-proxy socket - name: Create Bitcoin P2P proxy service unit copy: dest: /etc/systemd/system/bitcoin-p2p-proxy.service content: | [Unit] Description=Bitcoin P2P Proxy to {{ bitcoin_tailscale_hostname }} Requires=bitcoin-p2p-proxy.socket After=network.target [Service] Type=notify ExecStart=/lib/systemd/systemd-socket-proxyd {{ bitcoin_tailscale_hostname }}:{{ bitcoin_p2p_port }} owner: root group: root mode: '0644' - name: Reload systemd daemon systemd: daemon_reload: yes - name: Enable and start Bitcoin P2P proxy socket systemd: name: bitcoin-p2p-proxy.socket enabled: yes state: started - name: Allow Bitcoin P2P port through UFW ufw: rule: allow port: "{{ bitcoin_p2p_port | string }}" proto: tcp comment: "Bitcoin P2P public access" - name: Verify connectivity to knots-box via Tailscale wait_for: host: "{{ bitcoin_tailscale_hostname }}" port: "{{ bitcoin_p2p_port }}" timeout: 10 ignore_errors: yes - name: Display public endpoint debug: msg: "Bitcoin P2P public endpoint: {{ ansible_host }}:{{ bitcoin_p2p_port }}" # =========================================== # Uptime Kuma TCP Monitor for Public P2P # =========================================== - name: Create Uptime Kuma TCP monitor setup script for Bitcoin P2P delegate_to: localhost become: no copy: dest: /tmp/setup_bitcoin_p2p_tcp_monitor.py content: | #!/usr/bin/env python3 import sys import traceback import yaml from uptime_kuma_api import UptimeKumaApi, MonitorType try: with open('/tmp/ansible_bitcoin_p2p_config.yml', 'r') as f: config = yaml.safe_load(f) url = config['uptime_kuma_url'] username = config['username'] password = config['password'] monitor_host = config['monitor_host'] monitor_port = config['monitor_port'] monitor_name = config['monitor_name'] api = UptimeKumaApi(url, timeout=30) api.login(username, password) 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: api.add_monitor(type='group', name='services') 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 = next((m for m in monitors if m.get('name') == monitor_name), None) # 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: print(f"Monitor '{monitor_name}' already exists (ID: {existing['id']})") print("Skipping - monitor already configured") else: print(f"Creating TCP monitor '{monitor_name}'...") api.add_monitor( type=MonitorType.PORT, name=monitor_name, hostname=monitor_host, port=monitor_port, parent=group['id'], interval=60, maxretries=3, retryInterval=60, notificationIDList={ntfy_notification_id: True} if ntfy_notification_id else {} ) api.disconnect() print("SUCCESS") except Exception as e: print(f"ERROR: {str(e)}", file=sys.stderr) traceback.print_exc(file=sys.stderr) sys.exit(1) mode: '0755' - name: Create temporary config for TCP monitor setup delegate_to: localhost become: no copy: dest: /tmp/ansible_bitcoin_p2p_config.yml content: | uptime_kuma_url: "{{ uptime_kuma_api_url }}" username: "{{ uptime_kuma_username }}" password: "{{ uptime_kuma_password }}" monitor_host: "{{ ansible_host }}" monitor_port: {{ bitcoin_p2p_port }} monitor_name: "Bitcoin Knots P2P Public" mode: '0644' - name: Run Uptime Kuma TCP monitor setup command: python3 /tmp/setup_bitcoin_p2p_tcp_monitor.py delegate_to: localhost become: no register: tcp_monitor_setup changed_when: "'SUCCESS' in tcp_monitor_setup.stdout" ignore_errors: yes - name: Display TCP monitor setup output debug: msg: "{{ tcp_monitor_setup.stdout_lines }}" when: tcp_monitor_setup.stdout is defined - name: Clean up TCP monitor temporary files delegate_to: localhost become: no file: path: "{{ item }}" state: absent loop: - /tmp/setup_bitcoin_p2p_tcp_monitor.py - /tmp/ansible_bitcoin_p2p_config.yml handlers: - name: Restart bitcoin-p2p-proxy socket systemd: name: bitcoin-p2p-proxy.socket state: restarted