phoenixd upgrade
This commit is contained in:
parent
ba4ba504d0
commit
d0bfc2650d
5 changed files with 795 additions and 5 deletions
|
|
@ -198,9 +198,58 @@ Checklist:
|
|||
- [ ] SSH cloning works after setting up your SSH pub key
|
||||
|
||||
|
||||
## Phoenixd
|
||||
|
||||
phoenixd is the server version of the Phoenix wallet: a self-custodial Lightning node that leans on the ACINQ node for liquidity and channel management. It is the Lightning backend that LNBits talks to, so deploy it first.
|
||||
|
||||
It is not exposed to the internet. Its HTTP API is bound to `127.0.0.1` and only local consumers (LNBits, running on the same host) use it. There is no subdomain and no Caddy config for it.
|
||||
|
||||
### Deploy
|
||||
|
||||
* Review `ansible/services/phoenixd/phoenixd_vars.yml`. The things worth a look:
|
||||
* `phoenixd_version`: pinned release tag.
|
||||
* `phoenixd_auto_liquidity`: how much inbound liquidity phoenixd buys when it needs some (`off`, `2m`, `5m`, `10m`). This costs sats, it is not free.
|
||||
* `phoenixd_http_bind_port`: the loopback API port LNBits will point at.
|
||||
* No new secrets are needed: phoenixd generates its own API password on first start.
|
||||
* Run the deployment playbook: `ansible-playbook -i inventory.ini services/phoenixd/deploy_phoenixd_playbook.yml`.
|
||||
* The playbook will:
|
||||
* Install `phoenixd` and `phoenix-cli` into `/usr/local/bin`
|
||||
* Create the `phoenix` system user and a `0700` data dir at `/opt/phoenixd/.phoenix`
|
||||
* Create and start the `phoenixd` systemd service
|
||||
* Create a health check timer that runs `phoenix-cli getinfo` every minute
|
||||
* Register a push monitor named `Phoenixd` in Uptime Kuma under the `services` group
|
||||
|
||||
### Back up the seed
|
||||
|
||||
**Do this immediately after the first deploy.** The seed is the only thing that recovers the funds.
|
||||
|
||||
* Run the backup playbook: `ansible-playbook -i inventory.ini services/phoenixd/setup_backup_phoenixd_to_lapy.yml`.
|
||||
* This gpg encrypts `seed.dat` and `phoenix.conf` to Lapy daily and keeps 14 days. It does not stop the node and it does not back up the channel database on purpose: phoenixd keeps channel state with its peer, and restoring a stale channel db to a live node can force close channels and cost you a penalty.
|
||||
* Also write the 12 words down offline, once: `sudo cat /opt/phoenixd/.phoenix/seed.dat`.
|
||||
|
||||
### Operate
|
||||
|
||||
* Status: `sudo systemctl status phoenixd`, logs: `sudo journalctl -u phoenixd -f`.
|
||||
* CLI: `sudo PHOENIX_DATADIR=/opt/phoenixd/.phoenix phoenix-cli getinfo` (also `getbalance`, `listchannels`, `createinvoice`). If you changed `phoenixd_http_bind_port`, add `--http-bind-port <port>` before the subcommand.
|
||||
* API password, needed to wire anything up to the node: `sudo grep '^http-password=' /opt/phoenixd/.phoenix/phoenix.conf`.
|
||||
|
||||
### Restoring on a fresh host
|
||||
|
||||
* Deploy phoenixd but stop the service before it generates a new seed, or just drop the file in before the first run:
|
||||
* `sudo mkdir -p /opt/phoenixd/.phoenix`
|
||||
* `echo "your twelve words here" | sudo tee /opt/phoenixd/.phoenix/seed.dat`
|
||||
* `sudo chown -R phoenix:phoenix /opt/phoenixd/.phoenix && sudo chmod 600 /opt/phoenixd/.phoenix/seed.dat`
|
||||
* Then run the deployment playbook. Never run two nodes on the same seed at once.
|
||||
|
||||
Checklist:
|
||||
- [ ] `phoenix-cli getinfo` returns a node id
|
||||
- [ ] The `Phoenixd` monitor in Uptime Kuma is green
|
||||
- [ ] The seed is backed up to Lapy *and* written down offline
|
||||
|
||||
|
||||
## LNBits
|
||||
|
||||
LNBits is a Lightning Network wallet and accounts system.
|
||||
LNBits is a Lightning Network wallet and accounts system. It uses the phoenixd node deployed above as its Lightning backend.
|
||||
|
||||
### Deploy
|
||||
|
||||
|
|
@ -214,6 +263,7 @@ LNBits is a Lightning Network wallet and accounts system.
|
|||
* From that point on, you can configure through the Web UI.
|
||||
* Some advice around specifics of LNbits:
|
||||
* The default setup uses a FakeWallet backend for testing. Configure a real Lightning backend as needed by modifying the `.env` file located or using the superuser UI.
|
||||
* To use the phoenixd node deployed above, set the backend to `PhoenixdWallet` with endpoint `http://127.0.0.1:9740` and the API password from `/opt/phoenixd/.phoenix/phoenix.conf`. The deployment playbook does not do this for you, since flipping the wallet backend on a live instance is not something to do behind your back.
|
||||
* For security, disable the new users registration.
|
||||
|
||||
### Set up backups to Lapy
|
||||
|
|
|
|||
|
|
@ -13,17 +13,34 @@
|
|||
state: present
|
||||
update_cache: yes
|
||||
|
||||
# Caddy's signing subkey carries an expiry date, and Cloudsmith republishes
|
||||
# the key with a refreshed binding signature before it lapses. Re-fetch on
|
||||
# every run: pinning the keyring with `creates:` leaves the host stuck on a
|
||||
# key that eventually expires and breaks `apt update` with an sqv error.
|
||||
- name: Ensure apt keyrings directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/apt/keyrings
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: Download Caddy GPG armored key
|
||||
ansible.builtin.get_url:
|
||||
url: https://dl.cloudsmith.io/public/caddy/stable/gpg.key
|
||||
dest: /tmp/caddy-stable-archive-keyring.asc
|
||||
dest: /etc/apt/keyrings/caddy-stable-archive-keyring.asc
|
||||
mode: '0644'
|
||||
register: caddy_key_download
|
||||
|
||||
- name: Check for existing Caddy keyring
|
||||
ansible.builtin.stat:
|
||||
path: /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
register: caddy_keyring
|
||||
|
||||
- name: Convert ASCII armored key to binary keyring
|
||||
ansible.builtin.command:
|
||||
cmd: gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg /tmp/caddy-stable-archive-keyring.asc
|
||||
args:
|
||||
creates: /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
cmd: gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg /etc/apt/keyrings/caddy-stable-archive-keyring.asc
|
||||
when: caddy_key_download.changed or not caddy_keyring.stat.exists
|
||||
|
||||
- name: Ensure permissions on keyring file
|
||||
ansible.builtin.file:
|
||||
|
|
|
|||
535
ansible/services/phoenixd/deploy_phoenixd_playbook.yml
Normal file
535
ansible/services/phoenixd/deploy_phoenixd_playbook.yml
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
---
|
||||
# phoenixd Deployment Playbook
|
||||
#
|
||||
# Deploys phoenixd (https://phoenix.acinq.co/server), the server version of the
|
||||
# Phoenix Lightning wallet, on vipy so LNBits can use it as a wallet backend
|
||||
# over loopback.
|
||||
#
|
||||
# What this does:
|
||||
# 1. Downloads the pinned phoenixd release and installs phoenixd + phoenix-cli
|
||||
# 2. Creates a dedicated system user and a 0700 data directory
|
||||
# 3. Creates and enables a systemd service
|
||||
# 4. Creates a push-monitor health check script + systemd timer
|
||||
# 5. Registers a push monitor in Uptime Kuma
|
||||
#
|
||||
# The HTTP API stays bound to 127.0.0.1 and is NOT proxied by Caddy: phoenixd
|
||||
# holds funds and its API is protected by a single password. Anything that needs
|
||||
# it either runs on this host or reaches it over the Tailscale mesh.
|
||||
#
|
||||
# ⚠️ After the first run, back up {{ phoenixd_data_dir }}/seed.dat. Losing it
|
||||
# means losing the funds. See setup_backup_phoenixd_to_lapy.yml.
|
||||
|
||||
- name: Deploy phoenixd on vipy
|
||||
hosts: vipy
|
||||
become: yes
|
||||
vars_files:
|
||||
- ../../infra_vars.yml
|
||||
- ../../services_config.yml
|
||||
- ../../infra_secrets.yml
|
||||
- ./phoenixd_vars.yml
|
||||
vars:
|
||||
uptime_kuma_api_url: "https://{{ subdomains.uptime_kuma }}.{{ root_domain }}"
|
||||
|
||||
tasks:
|
||||
# ===========================================
|
||||
# Prerequisites
|
||||
# ===========================================
|
||||
- name: Install phoenixd runtime dependencies
|
||||
apt:
|
||||
name:
|
||||
- unzip
|
||||
- curl
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
# ===========================================
|
||||
# System User and Directories
|
||||
# ===========================================
|
||||
- name: Create phoenixd system group
|
||||
group:
|
||||
name: "{{ phoenixd_group }}"
|
||||
system: yes
|
||||
|
||||
- name: Create phoenixd system user
|
||||
user:
|
||||
name: "{{ phoenixd_user }}"
|
||||
group: "{{ phoenixd_group }}"
|
||||
system: yes
|
||||
shell: /usr/sbin/nologin
|
||||
home: "{{ phoenixd_home }}"
|
||||
create_home: yes
|
||||
comment: "phoenixd Lightning node"
|
||||
|
||||
- name: Create phoenixd home directory
|
||||
file:
|
||||
path: "{{ phoenixd_home }}"
|
||||
state: directory
|
||||
owner: "{{ phoenixd_user }}"
|
||||
group: "{{ phoenixd_group }}"
|
||||
mode: "0750"
|
||||
|
||||
- name: Create phoenixd data directory
|
||||
file:
|
||||
path: "{{ phoenixd_data_dir }}"
|
||||
state: directory
|
||||
owner: "{{ phoenixd_user }}"
|
||||
group: "{{ phoenixd_group }}"
|
||||
mode: "0700"
|
||||
|
||||
# ===========================================
|
||||
# Download and Install
|
||||
# ===========================================
|
||||
- name: Check if phoenixd is already installed
|
||||
stat:
|
||||
path: "{{ phoenixd_bin_dir }}/phoenixd"
|
||||
register: phoenixd_binary
|
||||
|
||||
- name: Check installed phoenixd version
|
||||
command: "{{ phoenixd_bin_dir }}/phoenixd --version"
|
||||
register: phoenixd_installed_version
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: phoenixd_binary.stat.exists
|
||||
|
||||
- name: Decide whether phoenixd needs installing
|
||||
set_fact:
|
||||
phoenixd_needs_install: >-
|
||||
{{ not phoenixd_binary.stat.exists
|
||||
or phoenixd_version not in (phoenixd_installed_version.stdout | default('')) }}
|
||||
|
||||
- name: Download phoenixd {{ phoenixd_version }}
|
||||
get_url:
|
||||
url: "{{ phoenixd_url }}"
|
||||
dest: "/tmp/phoenixd-{{ phoenixd_version }}.zip"
|
||||
mode: "0644"
|
||||
when: phoenixd_needs_install | bool
|
||||
|
||||
- name: Create temporary extraction directory
|
||||
file:
|
||||
path: /tmp/phoenixd-extract
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: phoenixd_needs_install | bool
|
||||
|
||||
- name: Extract phoenixd archive
|
||||
unarchive:
|
||||
src: "/tmp/phoenixd-{{ phoenixd_version }}.zip"
|
||||
dest: /tmp/phoenixd-extract
|
||||
remote_src: yes
|
||||
when: phoenixd_needs_install | bool
|
||||
|
||||
- name: Locate extracted binaries
|
||||
find:
|
||||
paths: /tmp/phoenixd-extract
|
||||
patterns: "{{ item }}"
|
||||
recurse: yes
|
||||
file_type: file
|
||||
register: phoenixd_extracted
|
||||
loop:
|
||||
- phoenixd
|
||||
- phoenix-cli
|
||||
when: phoenixd_needs_install | bool
|
||||
|
||||
- name: Fail if the archive did not contain the expected binaries
|
||||
assert:
|
||||
that:
|
||||
- item.files | length > 0
|
||||
fail_msg: "Could not find '{{ item.item }}' in the phoenixd {{ phoenixd_version }} archive"
|
||||
loop: "{{ phoenixd_extracted.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: phoenixd_needs_install | bool
|
||||
|
||||
- name: Install phoenixd and phoenix-cli binaries
|
||||
copy:
|
||||
src: "{{ item.files[0].path }}"
|
||||
dest: "{{ phoenixd_bin_dir }}/{{ item.item }}"
|
||||
remote_src: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop: "{{ phoenixd_extracted.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: phoenixd_needs_install | bool
|
||||
notify: Restart phoenixd
|
||||
|
||||
- name: Clean up phoenixd download artifacts
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "/tmp/phoenixd-{{ phoenixd_version }}.zip"
|
||||
- /tmp/phoenixd-extract
|
||||
|
||||
# ===========================================
|
||||
# Systemd Service
|
||||
# ===========================================
|
||||
- name: Build phoenixd command line arguments
|
||||
set_fact:
|
||||
phoenixd_args: >-
|
||||
{{ (['--agree-to-terms-of-service'] if phoenixd_agree_tos else [])
|
||||
+ ['--chain', phoenixd_chain]
|
||||
+ ['--auto-liquidity', phoenixd_auto_liquidity]
|
||||
+ ['--http-bind-ip', phoenixd_http_bind_ip]
|
||||
+ ['--http-bind-port', phoenixd_http_bind_port | string]
|
||||
+ (['--max-mining-fee', phoenixd_max_mining_fee | string] if phoenixd_max_mining_fee else [])
|
||||
+ (['--webhook', phoenixd_webhook_url] if phoenixd_webhook_url else [])
|
||||
+ ['--silent'] }}
|
||||
|
||||
- name: Create phoenixd systemd service
|
||||
copy:
|
||||
dest: /etc/systemd/system/phoenixd.service
|
||||
content: |
|
||||
[Unit]
|
||||
Description=phoenixd - Lightning Network Node
|
||||
Documentation=https://phoenix.acinq.co/server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ phoenixd_user }}
|
||||
Group={{ phoenixd_group }}
|
||||
WorkingDirectory={{ phoenixd_home }}
|
||||
Environment=PHOENIX_DATADIR={{ phoenixd_data_dir }}
|
||||
ExecStart={{ phoenixd_bin_dir }}/phoenixd {{ phoenixd_args | join(' ') }}
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
TimeoutStartSec=120
|
||||
TimeoutStopSec=120
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Hardening: the node only ever writes to its own data directory
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths={{ phoenixd_data_dir }}
|
||||
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Restart phoenixd
|
||||
|
||||
- name: Reload systemd daemon
|
||||
systemd:
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Enable and start phoenixd
|
||||
systemd:
|
||||
name: phoenixd
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
- name: Flush handlers so phoenixd is running before we inspect its data dir
|
||||
meta: flush_handlers
|
||||
|
||||
# ===========================================
|
||||
# First Boot Checks
|
||||
# ===========================================
|
||||
- name: Wait for phoenixd to write its config file
|
||||
wait_for:
|
||||
path: "{{ phoenixd_data_dir }}/phoenix.conf"
|
||||
state: present
|
||||
timeout: 120
|
||||
|
||||
- name: Check that the seed file exists
|
||||
stat:
|
||||
path: "{{ phoenixd_data_dir }}/seed.dat"
|
||||
register: phoenixd_seed_file
|
||||
|
||||
- name: Fail if phoenixd did not create a seed
|
||||
assert:
|
||||
that:
|
||||
- phoenixd_seed_file.stat.exists
|
||||
fail_msg: "phoenixd started but {{ phoenixd_data_dir }}/seed.dat is missing - check 'journalctl -u phoenixd'"
|
||||
|
||||
# ===========================================
|
||||
# Health Check Script + Systemd Timer
|
||||
# ===========================================
|
||||
- name: Create phoenixd health check script
|
||||
copy:
|
||||
dest: "{{ phoenixd_healthcheck_script_path }}"
|
||||
content: |
|
||||
#!/bin/bash
|
||||
# Checks phoenixd and pushes the result to Uptime Kuma.
|
||||
UPTIME_KUMA_PUSH_URL="${UPTIME_KUMA_PUSH_URL}"
|
||||
export PHOENIX_DATADIR="{{ phoenixd_data_dir }}"
|
||||
|
||||
check_phoenixd() {
|
||||
# Service must be active and the node must answer getinfo.
|
||||
# phoenix-cli reads the api password from $PHOENIX_DATADIR/phoenix.conf,
|
||||
# but not the bind address, so pass it explicitly.
|
||||
systemctl is-active --quiet phoenixd && \
|
||||
{{ phoenixd_bin_dir }}/phoenix-cli \
|
||||
--http-bind-ip {{ phoenixd_http_bind_ip }} \
|
||||
--http-bind-port {{ phoenixd_http_bind_port }} \
|
||||
getinfo 2>/dev/null | grep -q '"nodeId"'
|
||||
}
|
||||
|
||||
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_phoenixd; then
|
||||
push_to_uptime_kuma "up" "OK"
|
||||
exit 0
|
||||
else
|
||||
push_to_uptime_kuma "down" "phoenixd not responding"
|
||||
exit 1
|
||||
fi
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Create phoenixd health check systemd service
|
||||
copy:
|
||||
dest: "/etc/systemd/system/{{ phoenixd_healthcheck_service_name }}.service"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=phoenixd Health Check
|
||||
After=network.target phoenixd.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
ExecStart={{ phoenixd_healthcheck_script_path }}
|
||||
Environment=UPTIME_KUMA_PUSH_URL=
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Create phoenixd health check systemd timer
|
||||
copy:
|
||||
dest: "/etc/systemd/system/{{ phoenixd_healthcheck_service_name }}.timer"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=phoenixd 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 phoenixd health check timer
|
||||
systemd:
|
||||
name: "{{ phoenixd_healthcheck_service_name }}.timer"
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
# ===========================================
|
||||
# Uptime Kuma Push Monitor Setup
|
||||
# ===========================================
|
||||
- name: Create Uptime Kuma push monitor setup script for phoenixd
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
copy:
|
||||
dest: /tmp/setup_phoenixd_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_phoenixd_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
|
||||
|
||||
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/phoenixd_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_phoenixd_config.yml
|
||||
content: |
|
||||
uptime_kuma_url: "{{ uptime_kuma_api_url }}"
|
||||
username: "{{ uptime_kuma_username }}"
|
||||
password: "{{ uptime_kuma_password }}"
|
||||
monitor_name: "{{ phoenixd_monitor_name }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Run Uptime Kuma push monitor setup
|
||||
command: python3 /tmp/setup_phoenixd_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/phoenixd_push_url.txt
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
register: push_url_file
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Parse push URL
|
||||
set_fact:
|
||||
phoenixd_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/{{ phoenixd_healthcheck_service_name }}.service"
|
||||
regexp: "^Environment=UPTIME_KUMA_PUSH_URL="
|
||||
line: "Environment=UPTIME_KUMA_PUSH_URL={{ phoenixd_push_url }}"
|
||||
when: phoenixd_push_url is defined
|
||||
notify: Restart phoenixd health check timer
|
||||
|
||||
- name: Clean up temporary files
|
||||
delegate_to: localhost
|
||||
become: no
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- /tmp/setup_phoenixd_monitor.py
|
||||
- /tmp/ansible_phoenixd_config.yml
|
||||
- /tmp/phoenixd_push_url.txt
|
||||
|
||||
# ===========================================
|
||||
# Post-install Notes
|
||||
# ===========================================
|
||||
- name: Display post-install information
|
||||
debug:
|
||||
msg: |
|
||||
✓ phoenixd {{ phoenixd_version }} deployed
|
||||
|
||||
Status: systemctl status phoenixd
|
||||
Logs: journalctl -u phoenixd -f
|
||||
CLI: sudo PHOENIX_DATADIR={{ phoenixd_data_dir }} phoenix-cli --http-bind-port {{ phoenixd_http_bind_port }} getinfo
|
||||
HTTP API: http://{{ phoenixd_http_bind_ip }}:{{ phoenixd_http_bind_port }} (loopback only)
|
||||
Data dir: {{ phoenixd_data_dir }}
|
||||
|
||||
API password (needed to wire LNBits up to this node):
|
||||
sudo grep '^http-password=' {{ phoenixd_data_dir }}/phoenix.conf
|
||||
|
||||
⚠️ BACK UP THE SEED NOW: {{ phoenixd_data_dir }}/seed.dat
|
||||
Losing it means losing the funds. Run
|
||||
services/phoenixd/setup_backup_phoenixd_to_lapy.yml and also keep
|
||||
the 12 words somewhere offline.
|
||||
|
||||
handlers:
|
||||
- name: Restart phoenixd
|
||||
systemd:
|
||||
name: phoenixd
|
||||
state: restarted
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Restart phoenixd health check timer
|
||||
systemd:
|
||||
name: "{{ phoenixd_healthcheck_service_name }}.timer"
|
||||
state: restarted
|
||||
daemon_reload: yes
|
||||
52
ansible/services/phoenixd/phoenixd_vars.yml
Normal file
52
ansible/services/phoenixd/phoenixd_vars.yml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# phoenixd Configuration Variables
|
||||
# https://phoenix.acinq.co/server
|
||||
|
||||
# Version - pin to a specific release tag
|
||||
phoenixd_version: "0.9.0"
|
||||
phoenixd_arch: "linux-x64"
|
||||
phoenixd_url: "https://github.com/ACINQ/phoenixd/releases/download/v{{ phoenixd_version }}/phoenixd-{{ phoenixd_version }}-{{ phoenixd_arch }}.zip"
|
||||
|
||||
# Service user
|
||||
phoenixd_user: phoenix
|
||||
phoenixd_group: phoenix
|
||||
|
||||
# Directories
|
||||
phoenixd_home: /opt/phoenixd
|
||||
# phoenixd resolves its data dir from PHOENIX_DATADIR; seed.dat, phoenix.conf
|
||||
# and the channel db all live here.
|
||||
phoenixd_data_dir: "{{ phoenixd_home }}/.phoenix"
|
||||
phoenixd_bin_dir: /usr/local/bin
|
||||
|
||||
# Lightning settings
|
||||
phoenixd_chain: mainnet
|
||||
# Amount automatically requested when inbound liquidity is needed: off, 2m, 5m, 10m
|
||||
phoenixd_auto_liquidity: "2m"
|
||||
# Max mining fee for on-chain operations, in sats. Empty means phoenixd's default.
|
||||
phoenixd_max_mining_fee: ""
|
||||
# Required for non-interactive deployment
|
||||
phoenixd_agree_tos: true
|
||||
|
||||
# HTTP API
|
||||
# Stays on loopback: phoenixd is never exposed through Caddy, only local
|
||||
# consumers (e.g. LNBits on the same host) talk to it.
|
||||
phoenixd_http_bind_ip: "127.0.0.1"
|
||||
phoenixd_http_bind_port: 9740
|
||||
|
||||
# Optional webhook for payment events. Leave empty to disable.
|
||||
phoenixd_webhook_url: ""
|
||||
|
||||
# Monitoring
|
||||
phoenixd_healthcheck_script_path: /usr/local/bin/phoenixd-healthcheck-push.sh
|
||||
phoenixd_healthcheck_service_name: phoenixd-healthcheck
|
||||
phoenixd_monitor_name: "Phoenixd"
|
||||
|
||||
# Remote access
|
||||
remote_host_name: "vipy"
|
||||
remote_host: "{{ hostvars.get(remote_host_name, {}).get('ansible_host', remote_host_name) }}"
|
||||
remote_user: "{{ hostvars.get(remote_host_name, {}).get('ansible_user', 'counterweight') }}"
|
||||
remote_key_file: "{{ hostvars.get(remote_host_name, {}).get('ansible_ssh_private_key_file', '') }}"
|
||||
remote_port: "{{ hostvars.get(remote_host_name, {}).get('ansible_port', 22) }}"
|
||||
|
||||
# Local backup
|
||||
local_backup_dir: "{{ lookup('env', 'HOME') }}/phoenixd-backups"
|
||||
backup_script_path: "{{ lookup('env', 'HOME') }}/.local/bin/phoenixd_backup.sh"
|
||||
136
ansible/services/phoenixd/setup_backup_phoenixd_to_lapy.yml
Normal file
136
ansible/services/phoenixd/setup_backup_phoenixd_to_lapy.yml
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
# Backs up the phoenixd seed and config from vipy to Lapy, gpg encrypted.
|
||||
#
|
||||
# Only seed.dat and phoenix.conf are backed up, on purpose:
|
||||
# - seed.dat is what actually recovers the funds. phoenixd keeps its channel
|
||||
# state with the ACINQ peer, so a wallet is restored from the seed alone.
|
||||
# - restoring a *stale* channel database to a running Lightning node is
|
||||
# dangerous (it can trigger a force close and a penalty), so we do not keep
|
||||
# copies of phoenix.db around to be tempted by.
|
||||
# - phoenix.conf holds the http api password, which is what LNBits and any
|
||||
# other local consumer authenticate with.
|
||||
#
|
||||
# Because both files are static, phoenixd does not need to be stopped.
|
||||
|
||||
- name: Configure local backup for phoenixd from remote
|
||||
hosts: lapy
|
||||
gather_facts: no
|
||||
vars_files:
|
||||
- ../../infra_vars.yml
|
||||
- ./phoenixd_vars.yml
|
||||
vars:
|
||||
remote_data_path: "{{ phoenixd_data_dir }}"
|
||||
gpg_recipient: "{{ hostvars['localhost']['gpg_recipient'] | default('') }}"
|
||||
gpg_key_id: "{{ hostvars['localhost']['gpg_key_id'] | default('') }}"
|
||||
|
||||
tasks:
|
||||
- name: Debug phoenixd backup vars
|
||||
debug:
|
||||
msg:
|
||||
- "remote_host={{ remote_host }}"
|
||||
- "remote_user={{ remote_user }}"
|
||||
- "remote_data_path='{{ remote_data_path }}'"
|
||||
- "local_backup_dir={{ local_backup_dir }}"
|
||||
- "gpg_recipient={{ gpg_recipient }}"
|
||||
- "gpg_key_id={{ gpg_key_id }}"
|
||||
|
||||
- name: Ensure local backup directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ local_backup_dir }}"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
- name: Ensure ~/.local/bin exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ lookup('env', 'HOME') }}/.local/bin"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Create phoenixd backup script
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ backup_script_path }}"
|
||||
mode: "0750"
|
||||
content: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "{{ gpg_recipient }}" ]; then
|
||||
echo "GPG recipient is not configured. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TIMESTAMP=$(date +'%Y-%m-%d')
|
||||
ENCRYPTED_BACKUP="{{ local_backup_dir }}/phoenixd-backup-$TIMESTAMP.tar.gz.gpg"
|
||||
|
||||
{% if remote_key_file %}
|
||||
SSH_CMD="ssh -i {{ remote_key_file }} -p {{ remote_port }}"
|
||||
{% else %}
|
||||
SSH_CMD="ssh -p {{ remote_port }}"
|
||||
{% endif %}
|
||||
|
||||
# seed.dat + phoenix.conf only, see the header of the playbook.
|
||||
echo "Creating encrypted backup archive..."
|
||||
$SSH_CMD {{ remote_user }}@{{ remote_host }} \
|
||||
"sudo tar -czf - -C {{ remote_data_path }} seed.dat phoenix.conf" | \
|
||||
gpg --batch --yes --encrypt --recipient "{{ gpg_recipient }}" --output "$ENCRYPTED_BACKUP"
|
||||
|
||||
chmod 600 "$ENCRYPTED_BACKUP"
|
||||
|
||||
# Rotate old backups (keep 14 days)
|
||||
CUTOFF_DATE=$(date -d '14 days ago' +'%Y-%m-%d')
|
||||
for backup_file in "{{ local_backup_dir }}"/phoenixd-backup-*.tar.gz.gpg; do
|
||||
if [ -f "$backup_file" ]; then
|
||||
# Extract date from filename: phoenixd-backup-YYYY-MM-DD.tar.gz.gpg
|
||||
file_date=$(basename "$backup_file" | sed -n 's/phoenixd-backup-\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\)\.tar\.gz\.gpg/\1/p')
|
||||
if [ -n "$file_date" ] && [ "$file_date" != "$TIMESTAMP" ] && [ "$file_date" \< "$CUTOFF_DATE" ]; then
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Backup completed successfully"
|
||||
|
||||
- name: Ensure cronjob for phoenixd backup exists
|
||||
ansible.builtin.cron:
|
||||
name: "phoenixd backup"
|
||||
user: "{{ lookup('env', 'USER') }}"
|
||||
job: "{{ backup_script_path }}"
|
||||
minute: 15
|
||||
hour: "9"
|
||||
|
||||
- name: Run phoenixd backup script to create initial backup
|
||||
ansible.builtin.command: "{{ backup_script_path }}"
|
||||
|
||||
- name: Verify backup was created
|
||||
block:
|
||||
- name: Get today's date
|
||||
command: date +'%Y-%m-%d'
|
||||
register: today_date
|
||||
changed_when: false
|
||||
|
||||
- name: Check if backup file exists
|
||||
stat:
|
||||
path: "{{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg"
|
||||
register: backup_file_stat
|
||||
|
||||
- name: Verify backup file exists
|
||||
assert:
|
||||
that:
|
||||
- backup_file_stat.stat.exists
|
||||
- backup_file_stat.stat.isreg
|
||||
fail_msg: "Backup file {{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg was not created"
|
||||
success_msg: "Backup file {{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg exists"
|
||||
|
||||
- name: Verify backup file is not empty
|
||||
assert:
|
||||
that:
|
||||
- backup_file_stat.stat.size > 0
|
||||
fail_msg: "Backup file {{ local_backup_dir }}/phoenixd-backup-{{ today_date.stdout }}.tar.gz.gpg exists but is empty"
|
||||
success_msg: "Backup file size is {{ backup_file_stat.stat.size }} bytes"
|
||||
|
||||
- name: Remind about the offline seed copy
|
||||
debug:
|
||||
msg: |
|
||||
These encrypted backups are only as safe as your GPG key.
|
||||
Also write the 12 words down offline once:
|
||||
ssh {{ remote_user }}@{{ remote_host }} "sudo cat {{ remote_data_path }}/seed.dat"
|
||||
Loading…
Add table
Add a link
Reference in a new issue