diff --git a/ansible/infra_secrets.yml.example b/ansible/infra_secrets.yml.example index 14fd498..d539282 100644 --- a/ansible/infra_secrets.yml.example +++ b/ansible/infra_secrets.yml.example @@ -31,3 +31,10 @@ mariadb_mempool_password: "CHANGE_ME_TO_SECURE_PASSWORD" # Used by: services/forgejo-runner/deploy_forgejo_runner_playbook.yml # See: services/forgejo-runner/SETUP.md for how to obtain this token forgejo_runner_registration_token: "YOUR_RUNNER_TOKEN_HERE" + +# DATUM Gateway secrets +# Used by: services/datum-gateway/deploy_datum_gateway_playbook.yml +datum_mining_address: "YOUR_BITCOIN_ADDRESS_FOR_BLOCK_REWARDS" +datum_gateway_admin_password: "CHANGE_ME_TO_SECURE_PASSWORD" +datum_dashboard_username: "admin" +datum_dashboard_password_hash: "$2a$14$..." # Generate with: caddy hash-password diff --git a/ansible/services/datum-gateway/datum_gateway_vars.yml b/ansible/services/datum-gateway/datum_gateway_vars.yml new file mode 100644 index 0000000..48bb5b7 --- /dev/null +++ b/ansible/services/datum-gateway/datum_gateway_vars.yml @@ -0,0 +1,43 @@ +# DATUM Gateway Configuration Variables +# https://github.com/OCEAN-xyz/datum_gateway + +# Version - pin to a specific tag +datum_gateway_version: "v0.4.1beta" + +# Directories +datum_gateway_dir: /opt/datum-gateway +datum_gateway_source_dir: "{{ datum_gateway_dir }}/source" +datum_gateway_config_dir: /etc/datum-gateway +datum_gateway_log_dir: /var/log/datum-gateway + +# Binary +datum_gateway_bin_path: /usr/local/bin/datum_gateway + +# Ports +datum_gateway_stratum_port: 23334 # Miners connect here via Stratum v1 +datum_gateway_api_port: 7152 # Web dashboard / API + +# Stratum settings +datum_vardiff_min: 524288 # Minimum share difficulty (must be power of 2; OCEAN floor overrides if higher) + +# Service user +datum_gateway_user: datum +datum_gateway_group: datum + +# Build options +datum_gateway_build_jobs: 4 + +# Bitcoin node connection +# The gateway runs on the same host as Bitcoin Knots so localhost RPC works. +# datum_bitcoin_rpc_url should include http:// and port. +datum_bitcoin_rpc_url: "http://127.0.0.1:8332" +# Note: bitcoin_rpc_user and bitcoin_rpc_password come from infra_secrets.yml + +# Mining config +datum_coinbase_tag_primary: "DATUM" +datum_coinbase_tag_secondary: "BY ORDER OF BIP110" + +datum_pool_pass_workers: true +datum_pool_pass_full_users: true +datum_pooled_mining_only: true + diff --git a/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml new file mode 100644 index 0000000..f33ded7 --- /dev/null +++ b/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml @@ -0,0 +1,859 @@ +--- +# DATUM Gateway Deployment Playbook +# +# Deploys DATUM Gateway (https://github.com/OCEAN-xyz/datum_gateway) on the +# Bitcoin Knots host so it has direct localhost RPC access to bitcoind. +# +# What this does: +# 1. Installs build deps and compiles datum_gateway from source +# 2. Creates a dedicated system user and config/log directories +# 3. Writes /etc/datum-gateway/config.json from vars/secrets +# 4. Patches bitcoin.conf with the required blockmaxsize/blocknotify lines +# 5. Creates and enables a systemd service +# 6. Creates a push-monitor health check script + systemd timer +# 7. Registers a push monitor in Uptime Kuma +# +# Separate play: adds a Caddy reverse proxy on vipy for the dashboard. +# +# Stratum port (23334) is bound on knots_box_local. Expose it to miners via +# a firewall rule, Tailscale, or a socket proxy on vipy — not handled here. +# +# Required secrets in infra_secrets.yml: +# datum_mining_address - Bitcoin address for block rewards +# datum_gateway_admin_password - Password for the /api admin endpoint +# bitcoin_rpc_user - Shared with the bitcoin-knots deployment +# bitcoin_rpc_password - Shared with the bitcoin-knots deployment + +- name: Deploy DATUM Gateway on knots_box_local + hosts: knots_box_local + become: yes + vars_files: + - ../../infra_vars.yml + - ../../services_config.yml + - ../../infra_secrets.yml + - ./datum_gateway_vars.yml + vars: + datum_gateway_subdomain: "{{ subdomains.datum_gateway }}" + datum_gateway_domain: "{{ datum_gateway_subdomain }}.{{ root_domain }}" + uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" + + tasks: + # =========================================== + # Build Dependencies + # =========================================== + - name: Install DATUM Gateway build dependencies + apt: + name: + - cmake + - build-essential + - git + - libjansson-dev + - libmicrohttpd-dev + - libsodium-dev + - libcurl4-openssl-dev + # Runtime-only (netcat for health check) + - netcat-openbsd + state: present + update_cache: yes + + # =========================================== + # System User and Directories + # =========================================== + - name: Create datum system user + user: + name: "{{ datum_gateway_user }}" + system: yes + shell: /usr/sbin/nologin + home: "{{ datum_gateway_dir }}" + create_home: no + comment: "DATUM Gateway" + + - name: Create DATUM Gateway directories + file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner }}" + group: "{{ datum_gateway_group }}" + mode: "{{ item.mode }}" + loop: + - { path: "{{ datum_gateway_dir }}", owner: root, mode: "0755" } + - { path: "{{ datum_gateway_source_dir }}", owner: root, mode: "0755" } + - { path: "{{ datum_gateway_config_dir }}", owner: "{{ datum_gateway_user }}", mode: "0750" } + - { path: "{{ datum_gateway_log_dir }}", owner: "{{ datum_gateway_user }}", mode: "0750" } + + # =========================================== + # Build from Source + # =========================================== + - name: Clone DATUM Gateway repository at {{ datum_gateway_version }} + git: + repo: https://github.com/OCEAN-xyz/datum_gateway.git + dest: "{{ datum_gateway_source_dir }}" + version: "{{ datum_gateway_version }}" + force: yes + register: git_clone + + - name: Configure cmake build + command: cmake . -DCMAKE_BUILD_TYPE=Release + args: + chdir: "{{ datum_gateway_source_dir }}" + + - name: Compile datum_gateway + command: make -j{{ datum_gateway_build_jobs }} + args: + chdir: "{{ datum_gateway_source_dir }}" + + - name: Install datum_gateway binary + copy: + src: "{{ datum_gateway_source_dir }}/datum_gateway" + dest: "{{ datum_gateway_bin_path }}" + remote_src: yes + owner: root + group: root + mode: "0755" + notify: Restart datum-gateway + + # =========================================== + # Configuration + # =========================================== + - name: Write DATUM Gateway config.json + copy: + dest: "{{ datum_gateway_config_dir }}/config.json" + content: | + { + "bitcoind": { + "rpcuser": "{{ bitcoin_rpc_user }}", + "rpcpassword": "{{ bitcoin_rpc_password }}", + "rpcurl": "{{ datum_bitcoin_rpc_url }}", + "notify_fallback": true + }, + "stratum": { + "listen_port": {{ datum_gateway_stratum_port }}, + "vardiff_min": {{ datum_vardiff_min }} + }, + "mining": { + "pool_address": "{{ datum_mining_address }}", + "coinbase_tag_primary": "{{ datum_coinbase_tag_primary }}", + "coinbase_tag_secondary": "{{ datum_coinbase_tag_secondary }}" + }, + "api": { + "admin_password": "{{ datum_gateway_admin_password }}", + "listen_port": {{ datum_gateway_api_port }}, + "modify_conf": false + }, + "logger": { + "log_to_console": true, + "log_to_file": true, + "log_file": "{{ datum_gateway_log_dir }}/datum_gateway.log", + "log_rotate_daily": true, + "log_level_console": 2, + "log_level_file": 1 + }, + "datum": { + "pool_pass_workers": {{ datum_pool_pass_workers | lower }}, + "pool_pass_full_users": {{ datum_pool_pass_full_users | lower }}, + "pooled_mining_only": {{ datum_pooled_mining_only | lower }} + } + } + owner: "{{ datum_gateway_user }}" + group: "{{ datum_gateway_group }}" + mode: "0640" + notify: Restart datum-gateway + + # =========================================== + # Systemd Service + # =========================================== + - name: Create datum-gateway systemd service + copy: + dest: /etc/systemd/system/datum-gateway.service + content: | + [Unit] + Description=DATUM Gateway - Bitcoin Mining Gateway + Documentation=https://github.com/OCEAN-xyz/datum_gateway + After=network.target bitcoind.service + Wants=bitcoind.service + + [Service] + User={{ datum_gateway_user }} + Group={{ datum_gateway_group }} + Type=simple + ExecStart={{ datum_gateway_bin_path }} --config {{ datum_gateway_config_dir }}/config.json + Restart=on-failure + RestartSec=10 + StandardOutput=journal + StandardError=journal + + # Prevent config from being read by other users + ReadWritePaths={{ datum_gateway_log_dir }} + ReadOnlyPaths={{ datum_gateway_config_dir }} + + [Install] + WantedBy=multi-user.target + owner: root + group: root + mode: "0644" + notify: Restart datum-gateway + + - name: Reload systemd daemon + systemd: + daemon_reload: yes + + - name: Enable and start datum-gateway + systemd: + name: datum-gateway + enabled: yes + state: started + + # =========================================== + # Health Check Script + Systemd Timer + # =========================================== + - name: Create DATUM Gateway health check script + copy: + dest: /usr/local/bin/datum-gateway-healthcheck-push.sh + content: | + #!/bin/bash + UPTIME_KUMA_PUSH_URL="${UPTIME_KUMA_PUSH_URL}" + STRATUM_PORT={{ datum_gateway_stratum_port }} + + check_datum() { + # Service must be active and stratum port must be listening + systemctl is-active --quiet datum-gateway && \ + nc -z 127.0.0.1 "${STRATUM_PORT}" + } + + 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 + curl -s --max-time 10 --retry 2 -o /dev/null \ + "${UPTIME_KUMA_PUSH_URL}?status=${status}&msg=${msg// /%20}&ping=" || true + } + + if check_datum; then + push_to_uptime_kuma "up" "OK" + exit 0 + else + push_to_uptime_kuma "down" "DATUM Gateway not responding" + exit 1 + fi + owner: root + group: root + mode: "0755" + + - name: Create datum-gateway health check systemd service + copy: + dest: /etc/systemd/system/datum-gateway-healthcheck.service + content: | + [Unit] + Description=DATUM Gateway Health Check + After=network.target datum-gateway.service + + [Service] + Type=oneshot + User=root + ExecStart=/usr/local/bin/datum-gateway-healthcheck-push.sh + Environment=UPTIME_KUMA_PUSH_URL= + StandardOutput=journal + StandardError=journal + + [Install] + WantedBy=multi-user.target + owner: root + group: root + mode: "0644" + + - name: Create datum-gateway health check systemd timer + copy: + dest: /etc/systemd/system/datum-gateway-healthcheck.timer + content: | + [Unit] + Description=DATUM Gateway Health Check Timer + + [Timer] + OnBootSec=2min + OnUnitActiveSec=1min + Persistent=true + + [Install] + WantedBy=timers.target + owner: root + group: root + mode: "0644" + + - name: Reload systemd daemon after health check units + systemd: + daemon_reload: yes + + - name: Enable and start datum-gateway health check timer + systemd: + name: datum-gateway-healthcheck.timer + enabled: yes + state: started + + # =========================================== + # Uptime Kuma Push Monitor Setup + # =========================================== + - name: Create Uptime Kuma push monitor setup script for DATUM Gateway + delegate_to: localhost + become: no + copy: + dest: /tmp/setup_datum_gateway_monitor.py + content: | + #!/usr/bin/env python3 + import sys + import time + import traceback + import yaml + + try: + import socketio.exceptions + except ImportError: + pass + + from uptime_kuma_api import UptimeKumaApi, MonitorType + + try: + with open('/tmp/ansible_datum_gateway_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'] + + 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: + try: + api.add_monitor(type='group', name='services') + except Exception: + time.sleep(2) + monitors = api.get_monitors() + group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), 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 + + # Check if monitor already exists + existing = next((m for m in monitors if m.get('name') == monitor_name), None) + + push_url = None + + if existing: + print(f"Monitor '{monitor_name}' already exists (ID: {existing['id']})") + push_token = existing.get('pushToken') or existing.get('push_token') + if push_token: + push_url = f"{url}/api/push/{push_token}" + else: + print(f"Creating push monitor '{monitor_name}'...") + try: + api.add_monitor( + type=MonitorType.PUSH, + name=monitor_name, + parent=group['id'], + interval=90, + maxretries=3, + retryInterval=60, + notificationIDList={ntfy_notification_id: True} if ntfy_notification_id else {} + ) + except Exception as e: + # socketio timeout: add_monitor may have succeeded server-side + print(f"add_monitor raised (possibly timeout): {e}", file=sys.stderr) + time.sleep(2) + + 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 push_token: + push_url = f"{url}/api/push/{push_token}" + + api.disconnect() + + if push_url: + print(f"PUSH_URL={push_url}") + with open('/tmp/datum_gateway_push_url.txt', 'w') as f: + f.write(push_url) + + 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 push monitor setup + delegate_to: localhost + become: no + copy: + dest: /tmp/ansible_datum_gateway_config.yml + content: | + uptime_kuma_url: "{{ uptime_kuma_api_url }}" + username: "{{ uptime_kuma_username }}" + password: "{{ uptime_kuma_password }}" + monitor_name: "DATUM Gateway" + mode: "0644" + + - name: Run Uptime Kuma push monitor setup + command: python3 /tmp/setup_datum_gateway_monitor.py + delegate_to: localhost + become: no + register: monitor_setup + changed_when: "'SUCCESS' in monitor_setup.stdout" + ignore_errors: yes + + - name: Display monitor setup output + debug: + msg: "{{ monitor_setup.stdout_lines }}" + when: monitor_setup.stdout is defined + + - name: Read push URL from file + slurp: + src: /tmp/datum_gateway_push_url.txt + delegate_to: localhost + become: no + register: push_url_file + ignore_errors: yes + + - name: Parse push URL + set_fact: + datum_push_url: "{{ push_url_file.content | b64decode | trim }}" + when: push_url_file.content is defined + + - name: Update health check service with push URL + lineinfile: + path: /etc/systemd/system/datum-gateway-healthcheck.service + regexp: "^Environment=UPTIME_KUMA_PUSH_URL=" + line: "Environment=UPTIME_KUMA_PUSH_URL={{ datum_push_url }}" + when: datum_push_url is defined + notify: Restart datum-gateway health check timer + + - name: Clean up temporary files + delegate_to: localhost + become: no + file: + path: "{{ item }}" + state: absent + loop: + - /tmp/setup_datum_gateway_monitor.py + - /tmp/ansible_datum_gateway_config.yml + - /tmp/datum_gateway_push_url.txt + + handlers: + - name: Restart datum-gateway + systemd: + name: datum-gateway + state: restarted + daemon_reload: yes + + - name: Restart datum-gateway health check timer + systemd: + name: datum-gateway-healthcheck.timer + state: restarted + daemon_reload: yes + + +# =========================================== +# Caddy Reverse Proxy for DATUM Dashboard (on vipy) +# =========================================== +- name: Configure Caddy reverse proxy for DATUM Gateway dashboard on vipy + hosts: vipy + become: yes + vars_files: + - ../../infra_vars.yml + - ../../services_config.yml + - ../../infra_secrets.yml + - ./datum_gateway_vars.yml + vars: + datum_gateway_subdomain: "{{ subdomains.datum_gateway }}" + datum_gateway_domain: "{{ datum_gateway_subdomain }}.{{ root_domain }}" + caddy_sites_dir: "{{ caddy_sites_dir }}" + uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" + + tasks: + - name: Ensure Caddy sites-enabled directory exists + file: + path: "{{ caddy_sites_dir }}" + state: directory + owner: root + group: root + mode: "0755" + + - name: Ensure Caddyfile includes import directive for sites-enabled + lineinfile: + path: /etc/caddy/Caddyfile + line: "import sites-enabled/*" + insertafter: EOF + state: present + backup: yes + create: yes + mode: "0644" + + - name: Create Caddy reverse proxy config for DATUM Gateway dashboard + copy: + dest: "{{ caddy_sites_dir }}/datum-gateway.conf" + content: | + {{ datum_gateway_domain }} { + basic_auth { + {{ datum_dashboard_username }} {{ datum_dashboard_password_hash }} + } + reverse_proxy knots-box:{{ datum_gateway_api_port }} { + # Resolve via Tailscale MagicDNS + transport http { + resolvers 100.100.100.100 + } + } + } + owner: root + group: root + mode: "0644" + + - name: Validate Caddy config + command: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile + register: caddy_validate + changed_when: false + + - name: Display Caddy validation output + debug: + msg: "{{ caddy_validate.stdout_lines + caddy_validate.stderr_lines }}" + + - name: Reload Caddy + command: systemctl reload caddy + register: caddy_reload + + - name: Display Caddy reload output + debug: + msg: "{{ caddy_reload.stdout_lines + caddy_reload.stderr_lines }}" + + - name: Display DATUM Gateway dashboard URL + debug: + msg: "DATUM Gateway dashboard: https://{{ datum_gateway_domain }}" + + # =========================================== + # Uptime Kuma HTTP Monitor for Public Dashboard + # =========================================== + - name: Create Uptime Kuma HTTP monitor setup script for DATUM dashboard + delegate_to: localhost + become: no + copy: + dest: /tmp/setup_datum_http_monitor.py + content: | + #!/usr/bin/env python3 + import sys + import time + import traceback + import yaml + + from uptime_kuma_api import UptimeKumaApi, MonitorType + + try: + with open('/tmp/ansible_datum_http_config.yml', 'r') as f: + config = yaml.safe_load(f) + + url = config['uptime_kuma_url'] + username = config['username'] + password = config['password'] + monitor_url = config['monitor_url'] + monitor_name = config['monitor_name'] + + api = UptimeKumaApi(url, timeout=30) + api.login(username, password) + + monitors = api.get_monitors() + + group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None) + if not group: + try: + api.add_monitor(type='group', name='services') + except Exception: + time.sleep(2) + monitors = api.get_monitors() + group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None) + + notifications = api.get_notifications() + ntfy_notification_id = None + for notif in notifications: + if notif.get('type') == 'ntfy': + ntfy_notification_id = notif.get('id') + break + + existing = next((m for m in monitors if m.get('name') == monitor_name), None) + + if existing: + print(f"Monitor '{monitor_name}' already exists (ID: {existing['id']})") + else: + print(f"Creating HTTP monitor '{monitor_name}'...") + try: + api.add_monitor( + type=MonitorType.HTTP, + name=monitor_name, + url=monitor_url, + parent=group['id'], + interval=60, + maxretries=3, + retryInterval=60, + notificationIDList={ntfy_notification_id: True} if ntfy_notification_id else {} + ) + except Exception as e: + print(f"add_monitor raised (possibly timeout): {e}", file=sys.stderr) + time.sleep(2) + + 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 HTTP monitor + delegate_to: localhost + become: no + copy: + dest: /tmp/ansible_datum_http_config.yml + content: | + uptime_kuma_url: "{{ uptime_kuma_api_url }}" + username: "{{ uptime_kuma_username }}" + password: "{{ uptime_kuma_password }}" + monitor_url: "https://{{ datum_gateway_domain }}" + monitor_name: "DATUM Gateway Dashboard" + mode: "0644" + + - name: Run Uptime Kuma HTTP monitor setup + command: python3 /tmp/setup_datum_http_monitor.py + delegate_to: localhost + become: no + register: http_monitor_setup + changed_when: "'SUCCESS' in http_monitor_setup.stdout" + ignore_errors: yes + + - name: Display HTTP monitor setup output + debug: + msg: "{{ http_monitor_setup.stdout_lines }}" + when: http_monitor_setup.stdout is defined + + - name: Clean up HTTP monitor temporary files + delegate_to: localhost + become: no + file: + path: "{{ item }}" + state: absent + loop: + - /tmp/setup_datum_http_monitor.py + - /tmp/ansible_datum_http_config.yml + + +# =========================================== +# Stratum Port Forwarding on vipy via systemd-socket-proxyd +# Miners connect to vipy:23334; traffic is forwarded to knots-box:23334 +# over the Tailscale network, matching the Bitcoin P2P proxy pattern. +# =========================================== +- name: Setup public Stratum port forwarding on vipy via systemd-socket-proxyd + hosts: vipy + become: yes + vars_files: + - ../../infra_vars.yml + - ../../services_config.yml + - ../../infra_secrets.yml + - ./datum_gateway_vars.yml + vars: + datum_tailscale_hostname: "knots-box" + uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}" + + tasks: + - name: Create Stratum proxy socket unit + copy: + dest: /etc/systemd/system/datum-stratum-proxy.socket + content: | + [Unit] + Description=DATUM Stratum Proxy Socket + + [Socket] + ListenStream={{ datum_gateway_stratum_port }} + + [Install] + WantedBy=sockets.target + owner: root + group: root + mode: "0644" + notify: Restart datum-stratum-proxy socket + + - name: Create Stratum proxy service unit + copy: + dest: /etc/systemd/system/datum-stratum-proxy.service + content: | + [Unit] + Description=DATUM Stratum Proxy to {{ datum_tailscale_hostname }} + Requires=datum-stratum-proxy.socket + After=network.target + + [Service] + Type=notify + ExecStart=/lib/systemd/systemd-socket-proxyd {{ datum_tailscale_hostname }}:{{ datum_gateway_stratum_port }} + owner: root + group: root + mode: "0644" + + - name: Reload systemd daemon + systemd: + daemon_reload: yes + + - name: Enable and start Stratum proxy socket + systemd: + name: datum-stratum-proxy.socket + enabled: yes + state: started + + - name: Allow Stratum port through UFW + ufw: + rule: allow + port: "{{ datum_gateway_stratum_port | string }}" + proto: tcp + comment: "DATUM Gateway Stratum public access" + + - name: Verify connectivity to knots-box Stratum via Tailscale + wait_for: + host: "{{ datum_tailscale_hostname }}" + port: "{{ datum_gateway_stratum_port }}" + timeout: 10 + ignore_errors: yes + + - name: Display public Stratum endpoint + debug: + msg: "DATUM Stratum public endpoint: {{ ansible_host }}:{{ datum_gateway_stratum_port }}" + + # =========================================== + # Uptime Kuma TCP Monitor for Public Stratum + # =========================================== + - name: Create Uptime Kuma TCP monitor setup script for Stratum + delegate_to: localhost + become: no + copy: + dest: /tmp/setup_datum_stratum_tcp_monitor.py + content: | + #!/usr/bin/env python3 + import sys + import time + import traceback + import yaml + from uptime_kuma_api import UptimeKumaApi, MonitorType + + try: + with open('/tmp/ansible_datum_stratum_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() + + group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None) + if not group: + try: + api.add_monitor(type='group', name='services') + except Exception: + time.sleep(2) + monitors = api.get_monitors() + group = next((m for m in monitors if m.get('name') == 'services' and m.get('type') == 'group'), None) + + notifications = api.get_notifications() + ntfy_notification_id = None + for notif in notifications: + if notif.get('type') == 'ntfy': + ntfy_notification_id = notif.get('id') + break + + existing = next((m for m in monitors if m.get('name') == monitor_name), None) + + if existing: + print(f"Monitor '{monitor_name}' already exists (ID: {existing['id']})") + else: + print(f"Creating TCP monitor '{monitor_name}'...") + try: + 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 {} + ) + except Exception as e: + print(f"add_monitor raised (possibly timeout): {e}", file=sys.stderr) + time.sleep(2) + + 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 Stratum TCP monitor + delegate_to: localhost + become: no + copy: + dest: /tmp/ansible_datum_stratum_config.yml + content: | + uptime_kuma_url: "{{ uptime_kuma_api_url }}" + username: "{{ uptime_kuma_username }}" + password: "{{ uptime_kuma_password }}" + monitor_host: "{{ ansible_host }}" + monitor_port: {{ datum_gateway_stratum_port }} + monitor_name: "DATUM Stratum (public)" + mode: "0644" + + - name: Run Uptime Kuma TCP monitor setup + command: python3 /tmp/setup_datum_stratum_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 Stratum TCP monitor temporary files + delegate_to: localhost + become: no + file: + path: "{{ item }}" + state: absent + loop: + - /tmp/setup_datum_stratum_tcp_monitor.py + - /tmp/ansible_datum_stratum_config.yml + + handlers: + - name: Restart datum-stratum-proxy socket + systemd: + name: datum-stratum-proxy.socket + state: restarted diff --git a/ansible/services_config.yml b/ansible/services_config.yml index 56cc570..1bc9db0 100644 --- a/ansible/services_config.yml +++ b/ansible/services_config.yml @@ -25,6 +25,9 @@ subdomains: # Mempool Block Explorer (on mempool_box, proxied via vipy) mempool: mempool + # DATUM Gateway dashboard (on knots_box, proxied via vipy) + datum_gateway: datum + # Caddy configuration caddy_sites_dir: /etc/caddy/sites-enabled