personal_infra/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml

803 lines
29 KiB
YAML
Raw Permalink Normal View History

2026-08-08 12:00:27 +02:00
---
# 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: bitcoin
2026-08-08 12:00:27 +02:00
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
# ===========================================
# ═════════════════════════════════════════════════════════════════════════
# DEPRECATED — Uptime Kuma was decommissioned on 2026-09-11.
#
# Every task below is inert: uptime_kuma_enabled is false in
# group_vars/all/main.yml, so they all skip and the deployment above still
# runs normally. Kept because the health-check logic is the durable part —
# when a replacement exists, rewire the push transport and flip the flag.
#
# What was being monitored: archive/uptime_kuma/MONITORS.md
# ═════════════════════════════════════════════════════════════════════════
2026-08-08 12:00:27 +02:00
- name: Create DATUM Gateway health check script
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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 the edge host
hosts: edge
2026-08-08 12:00:27 +02:00
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:
ntfy, datum-gateway, headscale: use the caddy_site role Completes Stage 3. No hand-rolled Caddy plumbing remains anywhere: `grep sites-enabled` outside roles/ returns nothing, and so does `grep "systemctl reload caddy"`. ntfy uses caddy_site_body for its plain-HTTP listener and @httpget redirect. Verified ok/unchanged against watchtower; the one other changed task is a pre-existing "Update APT cache". datum-gateway keeps a whole-Caddyfile validate after the role call. The role validates its own fragment, but only a whole-file validate catches a conflict between two sites, and this playbook was the only one that ever had it. Its two debug tasks that echoed command output are gone with the commands. headscale is the one that mattered. Its playbook wrote `reverse_proxy localhost:8080`, but spacey is actually running a /admin* route in front of Headplane behind Caddy basic auth. Running that playbook would have deleted the admin route and its auth - a hazard that predates this work. It now renders the config that is really there, verified ok/unchanged via --start-at-task (the play cannot reach Caddy in check mode: "Install headscale package" fails because the .deb is not really downloaded, before and after this edit alike). Supporting changes for headscale: - headscale_ui_password_hash added to infra_secrets.yml and the identical group_vars/all/vault.yml, read from the live config on spacey. The vault already had headscale_ui_username (= counterweight, confirmed) and headscale_ui_password; I did not verify the password is the plaintext of this hash. - headplane_port added to headscale_vars.yml. - The role's handler now sets become: true. Handlers do not inherit become from the task that notified them, and this play runs become: no. - The include uses `apply: become: yes`; `become:` on an include_role is rejected outright. All 14 site files on all 3 hosts still byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 23:37:38 +02:00
- name: Publish the DATUM Gateway dashboard through Caddy
ansible.builtin.include_role:
name: caddy_site
vars:
caddy_site_name: datum-gateway
caddy_site_domain: "{{ datum_gateway_domain }}"
caddy_site_upstream: "knots-box:{{ datum_gateway_api_port }}"
caddy_site_resolvers: "100.100.100.100"
caddy_site_basic_auth:
- user: "{{ datum_dashboard_username }}"
hash: "{{ datum_dashboard_password_hash }}"
# The role validates the site fragment on its own. This re-validates the
# whole assembled Caddyfile, which is the only thing that catches a
# conflict between this site and another. Kept from the hand-rolled
# version; the other nine services never had it.
- name: Validate the assembled Caddyfile
ansible.builtin.command: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
2026-08-08 12:00:27 +02:00
changed_when: false
- name: Display DATUM Gateway dashboard URL
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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 the edge host
hosts: edge
2026-08-08 12:00:27 +02:00
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:
bitcoin-knots, fulcrum, datum-gateway: add and use the socket_proxy role Three near-identical hosts: edge plays become one role plus three short calls. 183 lines removed, 34 added, plus a 111-line role. Verified before touching any playbook: all six live units on vipy reproduced byte-identically. Then --limit edge --check per playbook - bitcoin-knots and fulcrum changed=0; datum-gateway changed=2, both attributable to the already known caddy_site comment line and the Reload caddy handler it triggers. The 6 units and 14 Caddy files on the hosts are byte-identical afterwards. PLAN_4 claimed these three plays had "no behavioural drift at all". That was wrong - it came from a diff truncated by head -60. The live bitcoin-p2p-proxy units carry four settings this playbook never wrote: .socket Documentation=, FreeBind=true .service Documentation=, TimeoutStopSec=5, StandardOutput=journal, StandardError=journal FreeBind is the one that matters: it lets the socket bind to an address that is not up yet, so without it the socket can fail to start on boot. Running the bitcoin-knots playbook would have stripped it. Same class of hazard as headscale. The role expresses all four; bitcoin-p2p is the only caller that passes any. Also: UFW treats the rule comment as part of the rule. datum-stratum's live comment is "DATUM Gateway Stratum public access" but the role's derived default produced "DATUM Stratum public access", which rewrote the rule. Caught in the dry-run; datum now passes the comment explicitly. Two deliberate differences from the original, both documented in the README: ignore_errors: yes on the upstream check became failed_when: false, and the handler restarts the .socket, which drops connections open through it - it fires only when a unit file actually changes. The inert Uptime Kuma TCP monitor blocks stay in the playbooks rather than being pulled into a new role (12/12/18 guarded tasks). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 23:50:15 +02:00
- name: Expose the DATUM Stratum port through a socket proxy
ansible.builtin.include_role:
name: socket_proxy
vars:
socket_proxy_name: datum-stratum
socket_proxy_description: "DATUM Stratum"
socket_proxy_listen_port: "{{ datum_gateway_stratum_port }}"
socket_proxy_upstream_host: "{{ datum_tailscale_hostname }}"
# Matches the UFW comment already on vipy; the derived default would
# have said "DATUM Stratum" and rewritten the rule.
socket_proxy_ufw_comment: "DATUM Gateway Stratum public access"
2026-08-08 12:00:27 +02:00
- name: Display public Stratum endpoint
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
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
when: uptime_kuma_enabled | default(false)
2026-08-08 12:00:27 +02:00
delegate_to: localhost
become: no
file:
path: "{{ item }}"
state: absent
loop:
- /tmp/setup_datum_stratum_tcp_monitor.py
- /tmp/ansible_datum_stratum_config.yml