ansible-taler-exchange

Ansible playbook to deploy a production Taler Exchange
Log | Files | Refs | README | LICENSE

commit 05b1ea59fec1a8124e304116c73ad7ea9a41da0d
parent 323ef8f4a478bab0da5bc08f29d62338ab4ee621
Author: Florian Dold <dold@taler.net>
Date:   Sat,  5 Sep 2026 22:38:49 +0200

helpers: forward Ansible arguments and drop .sh suffixes

Diffstat:
MREADME | 57+++++++++++++++++++++++++++++++++++++--------------------
Abackup | 20++++++++++++++++++++
Dbackup.sh | 16----------------
Mcontrib/ci/jobs/001-build/build.sh | 2+-
Mcontrib/tests/test_deployment.py | 2+-
Mcontrib/tests/test_upgrade_policy.py | 2+-
Adeploy | 19+++++++++++++++++++
Ddeploy.sh | 15---------------
Aextract-borg-key | 25+++++++++++++++++++++++++
Dextract-borg-key.sh | 12------------
Mplaybooks/restore.yml | 2+-
Mplaybooks/setup.yml | 2+-
Areboot | 19+++++++++++++++++++
Dreboot.sh | 15---------------
Aremove-monitoring | 19+++++++++++++++++++
Dremove-monitoring.sh | 15---------------
Arestore | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Drestore.sh | 76----------------------------------------------------------------------------
Mroles/borg-start/tasks/main.yml | 2+-
Asanction-check | 30++++++++++++++++++++++++++++++
Dsanction-check.sh | 25-------------------------
Asetup-pixel-borg | 33+++++++++++++++++++++++++++++++++
Dsetup-pixel-borg.sh | 19-------------------
Rstage-activate.sh -> stage-activate | 0
Astart-borg-backups | 28++++++++++++++++++++++++++++
Dstart-borg-backups.sh | 23-----------------------
Rtest.sh -> test | 0
27 files changed, 314 insertions(+), 242 deletions(-)

diff --git a/README b/README @@ -22,9 +22,26 @@ $ gpg -d vault_pass.txt.gpg > vault_pass.txt The canonical playbooks are run via shell scripts in the top-level directory. +The operational helpers pass arguments after their positional arguments +directly to `ansible-playbook`. For example: + +``` +$ ./deploy spec --check --diff +$ ./sanction-check spec sanctions.txt --diff +$ BORG_PASSPHRASE=... ./restore spec archive-name --verbose +``` + +Put the host/group first, followed by the sanction list or restore archive +where required, then any Ansible options. `extract-borg-key` and +`setup-pixel-borg` default to `spec` when called without arguments; supply +an explicit target when passing options. Quote option values containing +spaces as usual. Check-mode support depends on the playbook; restore +explicitly rejects check mode. The `test` runner and `stage-activate` do +not forward Ansible options. + ### Local Podman deployment test -Run `./test.sh` to build a Debian Trixie container, provision the complete test +Run `./test` to build a Debian Trixie container, provision the complete test deployment, repeat `setup.yml` with `--check --diff`, and exercise the main systemd units and HTTP APIs. The script creates a disposable SSH key and removes the container when it exits. @@ -35,16 +52,16 @@ pregenerated DH parameters and a container-only Certbot substitute which creates short-lived self-signed certificates: public ACME validation cannot reach the local test domains. Neither shortcut is used by real deployments. -### Main setup (restore.sh, deploy.sh) +### Main setup (restore, deploy) Database restore is deliberately separate from normal deployment. It is a -fresh-host recovery operation, not an idempotent part of `deploy.sh`, and it +fresh-host recovery operation, not an idempotent part of `deploy`, and it never chooses an archive implicitly. First list the Borg repository and select the exact archive to restore: ``` $ BORG_PASSPHRASE=... borg list ssh://borg@pixel.taler-systems.com/~/spec-backup -$ BORG_PASSPHRASE=... ./restore.sh spec 'archive-name-copied-from-list' +$ BORG_PASSPHRASE=... ./restore spec 'archive-name-copied-from-list' ``` The first argument must be one exact inventory hostname (not a group or host @@ -53,18 +70,18 @@ pattern). The destination PostgreSQL cluster must contain only the standard `root/postgres-backup.sql.gz` artifact, rejects dumps made by a newer PostgreSQL major version, restores all databases and roles, and verifies the exchange database before reporting success. It does not deploy or start the -application; inspect the recovered host and then run `deploy.sh` separately. +application; inspect the recovered host and then run `deploy` separately. If SQL restore or verification fails, the partially restored cluster is considered tainted and must be reinitialized before another attempt. The temporary database dump is removed, while a private error log is retained in `/var/tmp/taler-postgres-restore-*.log` on the target for diagnosis. -The "deploy.sh" script deploys the latest version of a system on a host. +The "deploy" script deploys the latest version of a system on a host. If you are root@rusty.taler-ops.ch, you may be able to: ``` -$ ./deploy.sh rusty +$ ./deploy rusty ``` For TOPS production, replace the "rusty" with "spec" to use the actual secrets @@ -89,12 +106,12 @@ for the staging exchange, where allowing repository users to sign staging configuration is intentional. It must never be used for production, for an exchange holding funds of value, or as the basis for any security assumption. -### sanction-check.sh +### sanction-check This command imports and checks the latest sanction lists: ``` -$ ./sanction-check.sh $TARGET $LIST +$ ./sanction-check $TARGET $LIST ``` where "$TARGET" is a host or group from "inventories/default" and $LIST @@ -108,14 +125,14 @@ host and referenced from the exchange configuration. NOTE: this should still be further automated. -### remove-monitoring.sh +### remove-monitoring The legacy monitoring stack is no longer deployed by this repository. To permanently remove its services, packages, configuration, local metrics data, nginx site, certificate, and database user from an existing deployment, run: ``` -$ ./remove-monitoring.sh $DEPLOYMENT +$ ./remove-monitoring $DEPLOYMENT ``` This is destructive. It does not back up the local Prometheus or Alloy data. @@ -126,7 +143,7 @@ This is destructive. It does not back up the local Prometheus or Alloy data. First run: ``` -$ ./extract-borg-key.sh $DEPLOYMENT +$ ./extract-borg-key $DEPLOYMENT ``` The resulting SSH public key should be added to the borg-account @@ -138,7 +155,7 @@ server-side (see admin-logs/pixel/03-borg.txt), start the daily backups via: ``` -$ ./start-borg-backups.sh $DEPLOYMENT +$ ./start-borg-backups $DEPLOYMENT ``` This will make a backup basically everything relevant to the @@ -158,7 +175,7 @@ To run a backup "immediately" (instead of the daily regular backups), use: ``` -$ ./backup.sh $DEPLOYMENT +$ ./backup $DEPLOYMENT ``` @@ -168,7 +185,7 @@ This should be done via the 'reboot' playbook which can be invoked via the ``` -$ ./reboot.sh $DEPLOYMENT +$ ./reboot $DEPLOYMENT ``` script. The reboot playbook first stops all Taler services, @@ -182,7 +199,7 @@ online cleanly. With podman and ansible installed locally one can run: ``` -$ ./test.sh +$ ./test ``` This will begin building the Containerfile in this repo, which is a Debian @@ -274,7 +291,7 @@ never restores a backup. ### database_restore Implements the guarded fresh-cluster restore used only by `playbooks/restore.yml` -and `restore.sh`. +and `restore`. ### devtesting @@ -346,7 +363,7 @@ post_deployment_http_retries defaults to 60 (with a five-second request timeout and one-second retry delay); systemd unit checks retain their shorter retry limit. A failed deployment leaves applications stopped without removing their boot -enablement. Inspect the failed task, correct the problem, then rerun deploy.sh. +enablement. Inspect the failed task, correct the problem, then rerun deploy. Do not reboot or manually start applications to bypass an unfinished migration. Optional services whose provisioning is skipped are restored if they were active when that deployment began. After a failed deployment, also inspect any optional @@ -371,7 +388,7 @@ remain supported. No archives are moved automatically. The backup script uses /root/.ssh/borg-config and leaves /root/.ssh/config intact. A lock protects the entire backup operation. Install the updated backup script -between backup runs: an already running older script does not acquire this lock. If cron, backup.sh or reboot.sh +between backup runs: an already running older script does not acquire this lock. If cron, backup or reboot encounters another backup, it exits nonzero (script status 75) without touching that run's snapshot. Retry after the existing backup finishes; a reboot playbook aborts before rebooting on this failure. Pruning and compaction are skipped after @@ -381,7 +398,7 @@ an unsuccessful or warning-producing archive creation. Run python3 contrib/tests/test_backup.py for isolated concurrency, failure, cleanup and archive round-trip checks (requires Borg and Python Jinja2). -The extended ./test.sh also exercises real systemd shutdown, package-start +The extended ./test also exercises real systemd shutdown, package-start suppression, a pre-2.20 Ansible dependency bootstrap, secret permissions, devtesting revocation, preservation of unrelated configuration, failed deployment and recovery. All fixtures run in the disposable container, never an inventory diff --git a/backup b/backup @@ -0,0 +1,20 @@ +#!/bin/sh + +set -eu + +if [ -z "${1:-}" ] +then + echo "Usage: $0 <host/group> [ansible-playbook options...]" >&2 + exit 1 +fi + +target=$1 +shift + +ansible-playbook \ + --inventory inventories/default \ + --limit "$target" \ + playbooks/backup.yml \ + "$@" + +exit 0 diff --git a/backup.sh b/backup.sh @@ -1,16 +0,0 @@ -#!/bin/sh - -set -eu - -if [ -z "${1:-}" ] -then - echo "Call with 'spec' or another host/group to select target" - exit 1 -fi - -ansible-playbook \ - --inventory inventories/default \ - --limit "$1" \ - playbooks/backup.yml - -exit 0 diff --git a/contrib/ci/jobs/001-build/build.sh b/contrib/ci/jobs/001-build/build.sh @@ -4,4 +4,4 @@ set -euo pipefail # This job runs inside the disposable CI container. The shared runner provisions # its nested container over the published SSH port and executes all regressions. rm -f /etc/containers/storage.conf -exec ./test.sh +exec ./test diff --git a/contrib/tests/test_deployment.py b/contrib/tests/test_deployment.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Integration regressions for the disposable container created by test.sh.""" +"""Integration regressions for the disposable container created by test.""" import argparse import json import os diff --git a/contrib/tests/test_upgrade_policy.py b/contrib/tests/test_upgrade_policy.py @@ -12,7 +12,7 @@ TEMPLATE = Path(__file__).resolve().parents[2] / 'roles/common_packages/template def verify_upgrade_policy_restoration(container, play): - # This callback is only invoked in test.sh's disposable container. + # This callback is only invoked in test's disposable container. container('sh', '-eu', '-c', ''' rm -f /usr/sbin/policy-rc.d printf '#!/bin/sh\\nexit 0\\n' > /usr/sbin/taler-test-original-policy diff --git a/deploy b/deploy @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu + +if [ -z "${1:-}" ] +then + echo "Usage: $0 <host/group> [ansible-playbook options...]" >&2 + exit 1 +fi + +target=$1 +shift + +ansible-playbook -v \ + --inventory inventories/default \ + --limit "$target" \ + playbooks/setup.yml \ + "$@" + +exit 0 diff --git a/deploy.sh b/deploy.sh @@ -1,15 +0,0 @@ -#!/bin/sh -set -eu - -if [ -z "${1:-}" ] -then - echo "Call with 'spec' or another host/group to select target" - exit 1 -fi - -ansible-playbook -v \ - --inventory inventories/default \ - --limit "$1" \ - playbooks/setup.yml - -exit 0 diff --git a/extract-borg-key b/extract-borg-key @@ -0,0 +1,25 @@ +#!/bin/bash + +set -eu + +case "${1:-}" in + -*) + echo "Usage: $0 [host/group [ansible-playbook options...]]" >&2 + exit 1 + ;; +esac + +target=${1:-spec} +if [ "$#" -gt 0 ]; then + shift +fi + +ansible-playbook \ + --inventory inventories/default \ + --limit "$target" \ + --user root \ + playbooks/borg-ssh-export.yml \ + "$@" +cat borg.pub/*/root/.ssh/borg.pub +rm -rf borg.pub/ +exit 0 diff --git a/extract-borg-key.sh b/extract-borg-key.sh @@ -1,12 +0,0 @@ -#!/bin/bash - -set -eu - -ansible-playbook \ - --inventory inventories/default \ - --limit "${1:-spec}" \ - --user root \ - playbooks/borg-ssh-export.yml -cat borg.pub/*/root/.ssh/borg.pub -rm -rf borg.pub/ -exit 0 diff --git a/playbooks/restore.yml b/playbooks/restore.yml @@ -40,7 +40,7 @@ - database_restore_archive is match('^[A-Za-z0-9][A-Za-z0-9._:+-]*$') - database_restore_controller_directory | length > 0 - lookup('ansible.builtin.env', 'BORG_PASSPHRASE') | length > 0 - fail_msg: Run restore.sh with a passphrase, host, and exact archive. + fail_msg: Run restore with a passphrase, host, and exact archive. quiet: true roles: diff --git a/playbooks/setup.yml b/playbooks/setup.yml @@ -13,7 +13,7 @@ that: not (enable_restore_backup | default(false) | bool) fail_msg: >- enable_restore_backup is no longer supported. Restore a fresh host - with restore.sh before running the normal deployment. + with restore before running the normal deployment. quiet: true - name: "Fail if the deployment kind is not defined" diff --git a/reboot b/reboot @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu + +if [ -z "${1:-}" ] +then + echo "Usage: $0 <host/group> [ansible-playbook options...]" >&2 + exit 1 +fi + +target=$1 +shift + +ansible-playbook \ + --inventory inventories/default \ + --limit "$target" \ + playbooks/reboot.yml \ + "$@" + +exit 0 diff --git a/reboot.sh b/reboot.sh @@ -1,15 +0,0 @@ -#!/bin/sh -set -eu - -if [ -z "${1:-}" ] -then - echo "Call with 'spec' or another host/group to select target" - exit 1 -fi - -ansible-playbook \ - --inventory inventories/default \ - --limit "$1" \ - playbooks/reboot.yml - -exit 0 diff --git a/remove-monitoring b/remove-monitoring @@ -0,0 +1,19 @@ +#!/bin/sh +set -eu + +if [ -z "${1:-}" ] +then + echo "Usage: $0 <host/group> [ansible-playbook options...]" >&2 + exit 1 +fi + +target=$1 +shift + +ansible-playbook \ + --inventory inventories/default \ + --limit "$target" \ + playbooks/remove-monitoring.yml \ + "$@" + +exit 0 diff --git a/remove-monitoring.sh b/remove-monitoring.sh @@ -1,15 +0,0 @@ -#!/bin/sh -set -eu - -if [ -z "${1:-}" ] -then - echo "Call with 'spec' or another host/group to select target" - exit 1 -fi - -ansible-playbook \ - --inventory inventories/default \ - --limit "$1" \ - playbooks/remove-monitoring.yml - -exit 0 diff --git a/restore b/restore @@ -0,0 +1,78 @@ +#!/bin/sh + +set -eu + +usage() +{ + echo "Usage: BORG_PASSPHRASE=... $0 <inventory-host> <archive-name> [ansible-playbook options...]" >&2 +} + +if [ -z "${BORG_PASSPHRASE:-}" ] +then + echo "BORG_PASSPHRASE must be set." >&2 + echo "It can be found encrypted in admin-log.git under the target host." >&2 + exit 1 +fi + +if [ "$#" -lt 2 ] +then + usage + exit 1 +fi + +restore_target=$1 +restore_archive=$2 +shift 2 + +case "$restore_target" in + ''|*[!A-Za-z0-9._-]*) + echo "The target must be one exact inventory hostname." >&2 + exit 1 + ;; +esac + +case "$restore_archive" in + latest|LATEST) + echo "Select an exact Borg archive; 'latest' is not accepted." >&2 + exit 1 + ;; + ''|*[!A-Za-z0-9._:+-]*) + echo "Invalid Borg archive name: $restore_archive" >&2 + exit 1 + ;; +esac + +for restore_command in ansible-playbook borg gzip zgrep +do + if ! command -v "$restore_command" >/dev/null 2>&1 + then + echo "Required command not found: $restore_command" >&2 + exit 1 + fi +done + +restore_workdir=$(mktemp -d "${TMPDIR:-/tmp}/taler-database-restore.XXXXXX") +chmod 700 "$restore_workdir" + +cleanup() +{ + rm -f -- "$restore_workdir/postgres-backup.sql.gz" + rmdir -- "$restore_workdir" 2>/dev/null || true +} +trap cleanup EXIT +trap 'exit 1' HUP INT TERM + +export TALER_RESTORE_TARGET="$restore_target" +export TALER_RESTORE_ARCHIVE="$restore_archive" +export TALER_RESTORE_CONTROLLER_DIRECTORY="$restore_workdir" + +echo "Restoring $restore_target from exact archive $restore_archive" + +ansible-playbook -v \ + --inventory inventories/default \ + --limit "$restore_target" \ + playbooks/restore.yml \ + "$@" + +echo "Database restore and verification completed successfully." +echo "Inspect the host, then deploy the application with: ./deploy $restore_target" diff --git a/restore.sh b/restore.sh @@ -1,76 +0,0 @@ -#!/bin/sh - -set -eu - -usage() -{ - echo "Usage: BORG_PASSPHRASE=... $0 <inventory-host> <archive-name>" >&2 -} - -if [ -z "${BORG_PASSPHRASE:-}" ] -then - echo "BORG_PASSPHRASE must be set." >&2 - echo "It can be found encrypted in admin-log.git under the target host." >&2 - exit 1 -fi - -if [ "$#" -ne 2 ] -then - usage - exit 1 -fi - -restore_target=$1 -restore_archive=$2 - -case "$restore_target" in - ''|*[!A-Za-z0-9._-]*) - echo "The target must be one exact inventory hostname." >&2 - exit 1 - ;; -esac - -case "$restore_archive" in - latest|LATEST) - echo "Select an exact Borg archive; 'latest' is not accepted." >&2 - exit 1 - ;; - ''|*[!A-Za-z0-9._:+-]*) - echo "Invalid Borg archive name: $restore_archive" >&2 - exit 1 - ;; -esac - -for restore_command in ansible-playbook borg gzip zgrep -do - if ! command -v "$restore_command" >/dev/null 2>&1 - then - echo "Required command not found: $restore_command" >&2 - exit 1 - fi -done - -restore_workdir=$(mktemp -d "${TMPDIR:-/tmp}/taler-database-restore.XXXXXX") -chmod 700 "$restore_workdir" - -cleanup() -{ - rm -f -- "$restore_workdir/postgres-backup.sql.gz" - rmdir -- "$restore_workdir" 2>/dev/null || true -} -trap cleanup EXIT -trap 'exit 1' HUP INT TERM - -export TALER_RESTORE_TARGET="$restore_target" -export TALER_RESTORE_ARCHIVE="$restore_archive" -export TALER_RESTORE_CONTROLLER_DIRECTORY="$restore_workdir" - -echo "Restoring $restore_target from exact archive $restore_archive" - -ansible-playbook -v \ - --inventory inventories/default \ - --limit "$restore_target" \ - playbooks/restore.yml - -echo "Database restore and verification completed successfully." -echo "Inspect the host, then deploy the application with: ./deploy.sh $restore_target" diff --git a/roles/borg-start/tasks/main.yml b/roles/borg-start/tasks/main.yml @@ -21,7 +21,7 @@ - name: Fail if we do not have an SSH key for the backup server fail: - msg: "You need to first run extract-borg-key.sh" + msg: "You need to first run extract-borg-key" when: not have_ssh_key.stat.exists - name: Ensure /root/bin/ directory exists diff --git a/sanction-check b/sanction-check @@ -0,0 +1,30 @@ +#!/bin/bash + +set -eu + +if [ -z "${1:-}" ]; then + echo "Usage: $0 <host/group> <sanction-list> [ansible-playbook options...]" >&2 + exit 1 +fi + +if [ -z "${2:-}" ]; then + echo "Pass sanction list as 2nd argument" + exit 1 +fi + +if [ ! -f "$2" ]; then + echo "Sanction list '$2' not found" + exit 1 +fi + +target=$1 +sanction_list=$2 +shift 2 + +ansible-playbook \ + --extra-vars "sanction_list=$sanction_list" \ + --verbose \ + --limit "$target" \ + --inventory inventories/default \ + playbooks/sanctionlist-check.yml \ + "$@" diff --git a/sanction-check.sh b/sanction-check.sh @@ -1,25 +0,0 @@ -#!/bin/bash - -set -eu - -if [ -z "${1:-}" ]; then - echo "Call with 'spec' or other host/group as target" - exit 1 -fi - -if [ -z "${2:-}" ]; then - echo "Pass sanction list as 2nd argument" - exit 1 -fi - -if [ ! -f "$2" ]; then - echo "Sanction list '$2' not found" - exit 1 -fi - -ansible-playbook \ - --extra-vars "sanction_list=$2" \ - --verbose \ - --limit "$1" \ - --inventory inventories/default \ - playbooks/sanctionlist-check.yml diff --git a/setup-pixel-borg b/setup-pixel-borg @@ -0,0 +1,33 @@ +#!/bin/bash + +set -eu + +if [ -z "${PIXEL_BORG_KEY:-}" ] +then + echo "You need to set the PIXEL_BORG_KEY in your environment before running this script (see admin-log/pixel/03-borg.txt)" + exit 1 +fi + +case "${1:-}" in + -*) + echo "Usage: $0 [host/group [ansible-playbook options...]]" >&2 + exit 1 + ;; +esac + +target=${1:-spec} +if [ "$#" -gt 0 ]; then + shift +fi + +ansible-playbook \ + --extra-vars "pixel_borg_key={{ lookup('env', 'PIXEL_BORG_KEY') }}" \ + --inventory inventories/default \ + --limit "$target" \ + --user root \ + playbooks/pixel-borg.yml \ + "$@" +mv borg-repokey/*/home/borg/borg-repo.key . +rm -rf borg-repokey/ +echo "Make sure to back up the borg-repo.key to admin-log/pixel/borg-repo.key" +exit 0 diff --git a/setup-pixel-borg.sh b/setup-pixel-borg.sh @@ -1,19 +0,0 @@ -#!/bin/bash - -set -eu - -if [ -z "${PIXEL_BORG_KEY:-}" ] -then - echo "You need to set the PIXEL_BORG_KEY in your environment before running this script (see admin-log/pixel/03-borg.txt)" - exit 1 -fi -ansible-playbook \ - --extra-vars "pixel_borg_key={{ lookup('env', 'PIXEL_BORG_KEY') }}" \ - --inventory inventories/default \ - --limit "${1:-spec}" \ - --user root \ - playbooks/pixel-borg.yml -mv borg-repokey/*/home/borg/borg-repo.key . -rm -rf borg-repokey/ -echo "Make sure to back up the borg-repo.key to admin-log/pixel/borg-repo.key" -exit 0 diff --git a/stage-activate.sh b/stage-activate diff --git a/start-borg-backups b/start-borg-backups @@ -0,0 +1,28 @@ +#!/bin/bash + +set -eu + +if [ -z "${BORG_PASSPHRASE:-}" ] +then + echo "You need to set the BORG_PASSPHRASE in your environment before running this script!" + exit 1 +fi + +if [ -z "${1:-}" ] +then + echo "Usage: $0 <host/group> [ansible-playbook options...]" >&2 + exit 1 +fi + +target=$1 +shift + +ansible-playbook \ + --verbose \ + --extra-vars "borg_passphrase={{ lookup('env', 'BORG_PASSPHRASE') }}" \ + --inventory inventories/default \ + --limit "$target" \ + playbooks/borg-start.yml \ + "$@" + +exit 0 diff --git a/start-borg-backups.sh b/start-borg-backups.sh @@ -1,23 +0,0 @@ -#!/bin/bash - -set -eu - -if [ -z "${BORG_PASSPHRASE:-}" ] -then - echo "You need to set the BORG_PASSPHRASE in your environment before running this script!" - exit 1 -fi - -if [ -z "${1:-}" ] -then - echo "Call with 'spec' or another host/group to select target" - exit 1 -fi -ansible-playbook \ - --verbose \ - --extra-vars "borg_passphrase={{ lookup('env', 'BORG_PASSPHRASE') }}" \ - --inventory inventories/default \ - --limit "${1:-spec}" \ - playbooks/borg-start.yml - -exit 0 diff --git a/test.sh b/test