personal_infra/ansible/services/datum-gateway/deploy_datum_gateway_playbook.yml
counterweight 16cbd189b8
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

852 lines
30 KiB
YAML

---
# 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
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
# ═════════════════════════════════════════════════════════════════════════
- name: Create DATUM Gateway health check script
when: uptime_kuma_enabled | default(false)
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)
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)
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)
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)
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)
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)
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)
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: edge
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:
- 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
changed_when: false
- name: Display DATUM Gateway dashboard URL
when: uptime_kuma_enabled | default(false)
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)
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)
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)
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)
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: edge
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
when: uptime_kuma_enabled | default(false)
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)
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)
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)
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)
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