commit ddcd2db9a580dfbe9f1da76a8d683144ff76a1a5
parent 2f9e597398dc78519401c365051415b3217d2f5b
Author: Florian Dold <dold@taler.net>
Date: Thu, 3 Sep 2026 00:28:16 +0200
regional-currency-ng: add destructive exchange reset playbook
Diffstat:
4 files changed, 232 insertions(+), 0 deletions(-)
diff --git a/regional-currency-ng/ChangeLog b/regional-currency-ng/ChangeLog
@@ -1,3 +1,7 @@
+Thu Sep 3 2026
+ Added a guarded playbook that completely resets and reprovisions an
+ exchange on testing deployments.
+
Wed Sep 2 2026
Added an optional, default-enabled landing page on the deployment's
main domain with links to the bank, merchant backend, and exchange.
diff --git a/regional-currency-ng/README.md b/regional-currency-ng/README.md
@@ -143,6 +143,37 @@ tune `regional_currency_bank_token_rate`,
Merchant instances and their credentials must be created separately.
+Dangerously reset a test exchange
+---------------------------------
+
+The destructive reset playbook deletes the exchange database, all offline and
+online exchange keys, and the generated denomination configuration. It then
+uses the normal regional-currency role to provision a new exchange and update
+the merchant with the new exchange master public key. Existing coins, reserves,
+and exchange-related payment state become permanently unusable.
+
+The playbook refuses inventories where
+`regional_currency_testing_deployment` is not `true`. Limit each invocation to
+the intended target:
+
+```console
+$ ansible-playbook -i inventory dangerously-reset-exchange.yml --limit HOST
+```
+
+For each host, enter the displayed domain-specific confirmation phrase. The
+playbook processes hosts one at a time so one confirmation cannot authorize a
+different target accidentally.
+
+The reset preserves the LibEuFin bank database, the exchange bank account and
+token, the merchant database, TLS state, and the recorded deployment identity.
+Because the fresh exchange database has no previous wirewatch position, it may
+observe historical incoming transfers from the preserved bank account again.
+This playbook is therefore suitable only for disposable testing deployments.
+
+If normal reprovisioning fails after deletion, correct the reported problem and
+run `site.yml` to finish reconciling the deployment. The deleted test data and
+keys cannot be recovered by the playbooks.
+
The `diagnose.sh` and `list-incoming.sh` scripts are server-side diagnostic
utilities, not deployment or upgrade entry points. All deployment and package
upgrade work is performed by `ansible-playbook` from the operator machine.
diff --git a/regional-currency-ng/dangerously-reset-exchange.yml b/regional-currency-ng/dangerously-reset-exchange.yml
@@ -0,0 +1,16 @@
+---
+- name: Dangerously reset and reprovision a test exchange
+ hosts: regional_currency
+ become: true
+ gather_facts: true
+ serial: 1
+
+ tasks:
+ - name: Dangerously reset the exchange
+ ansible.builtin.import_role:
+ name: regional_currency
+ tasks_from: dangerously-reset-exchange.yml
+
+ - name: Reprovision the regional currency
+ ansible.builtin.import_role:
+ name: regional_currency
diff --git a/regional-currency-ng/roles/regional_currency/tasks/dangerously-reset-exchange.yml b/regional-currency-ng/roles/regional_currency/tasks/dangerously-reset-exchange.yml
@@ -0,0 +1,181 @@
+---
+- name: Require a testing deployment for exchange reset
+ ansible.builtin.assert:
+ that:
+ - regional_currency_testing_deployment | bool
+ fail_msg: >-
+ Refusing to reset the exchange because this inventory is not marked as a
+ testing deployment.
+
+- name: Read the configured exchange database
+ ansible.builtin.command:
+ argv:
+ - taler-exchange-config
+ - -s
+ - exchangedb-postgres
+ - -o
+ - CONFIG
+ register: regional_currency_exchange_reset_database
+ changed_when: false
+
+- name: Read the configured offline key paths
+ become: true
+ become_user: taler-exchange-offline
+ ansible.builtin.command:
+ argv:
+ - taler-exchange-config
+ - -s
+ - exchange-offline
+ - -f
+ - -o
+ - "{{ item.option }}"
+ loop:
+ - option: MASTER_PRIV_FILE
+ expected: /home/taler-exchange-offline/.local/share/taler/exchange/offline-keys/master.priv
+ - option: SECM_TOFU_FILE
+ expected: /home/taler-exchange-offline/.local/share/taler/exchange/offline-keys/secm_tofus.pub
+ register: regional_currency_exchange_reset_offline_paths
+ changed_when: false
+
+- name: Read the configured online key paths
+ ansible.builtin.command:
+ argv:
+ - taler-exchange-config
+ - -s
+ - "{{ item.section }}"
+ - -f
+ - -o
+ - "{{ item.option }}"
+ loop:
+ - section: taler-exchange-secmod-rsa
+ option: KEY_DIR
+ expected: /var/lib/taler-exchange/secmod-rsa/keys
+ - section: taler-exchange-secmod-rsa
+ option: SM_PRIV_KEY
+ expected: /var/lib/taler-exchange/secmod-rsa/secmod-private-key
+ - section: taler-exchange-secmod-cs
+ option: KEY_DIR
+ expected: /var/lib/taler-exchange/secmod-cs/keys
+ - section: taler-exchange-secmod-cs
+ option: SM_PRIV_KEY
+ expected: /var/lib/taler-exchange/secmod-cs/secmod-private-key
+ - section: taler-exchange-secmod-eddsa
+ option: KEY_DIR
+ expected: /var/lib/taler-exchange/secmod-eddsa/keys
+ - section: taler-exchange-secmod-eddsa
+ option: SM_PRIV_KEY
+ expected: /var/lib/taler-exchange/secmod-eddsa/secmod-private-key
+ register: regional_currency_exchange_reset_online_paths
+ changed_when: false
+
+- name: Validate the exchange database reset target
+ ansible.builtin.assert:
+ that:
+ - (regional_currency_exchange_reset_database.stdout | trim) == "postgres:///exchange"
+ fail_msg: >-
+ Refusing to reset an exchange database outside the role-managed
+ postgres:///exchange configuration.
+
+- name: Validate the offline key reset targets
+ ansible.builtin.assert:
+ that:
+ - (item.stdout | trim) == item.item.expected
+ fail_msg: >-
+ Refusing to delete unexpected offline key path {{ item.stdout | trim }}.
+ loop: "{{ regional_currency_exchange_reset_offline_paths.results }}"
+ loop_control:
+ label: "{{ item.item.option }}"
+
+- name: Validate the online key reset targets
+ ansible.builtin.assert:
+ that:
+ - (item.stdout | trim) == item.item.expected
+ fail_msg: >-
+ Refusing to delete unexpected online key path {{ item.stdout | trim }}.
+ loop: "{{ regional_currency_exchange_reset_online_paths.results }}"
+ loop_control:
+ label: "{{ item.item.section }} {{ item.item.option }}"
+
+- name: Confirm the destructive exchange reset
+ ansible.builtin.pause:
+ prompt: >-
+ This permanently deletes the exchange database, all exchange keys, and
+ all existing coins and reserves. Type
+ "DANGEROUSLY RESET EXCHANGE {{ regional_currency_domain }}" to continue
+ echo: true
+ register: regional_currency_exchange_reset_confirmation
+
+- name: Require the exact exchange reset confirmation
+ ansible.builtin.assert:
+ that:
+ - >-
+ regional_currency_exchange_reset_confirmation.user_input
+ == "DANGEROUSLY RESET EXCHANGE " ~ regional_currency_domain
+ fail_msg: Exchange reset cancelled because the confirmation did not match.
+
+- name: Disable and stop exchange offline maintenance
+ ansible.builtin.systemd_service:
+ name: taler-exchange-offline.timer
+ enabled: false
+ state: stopped
+
+- name: Stop exchange offline maintenance
+ ansible.builtin.systemd_service:
+ name: taler-exchange-offline.service
+ state: stopped
+
+- name: Stop the merchant during exchange reset
+ ansible.builtin.systemd_service:
+ name: taler-merchant.target
+ state: stopped
+
+- name: Stop the exchange
+ ansible.builtin.systemd_service:
+ name: taler-exchange.target
+ state: stopped
+
+- name: Drop the exchange database
+ become: true
+ become_user: postgres
+ ansible.builtin.command:
+ argv:
+ - dropdb
+ - --if-exists
+ - --force
+ - exchange
+ changed_when: true
+
+- name: Delete offline exchange key material
+ ansible.builtin.file:
+ path: "{{ item.stdout | trim }}"
+ state: absent
+ loop: "{{ regional_currency_exchange_reset_offline_paths.results }}"
+ loop_control:
+ label: "{{ item.item.option }}"
+
+- name: Delete online exchange key material
+ ansible.builtin.file:
+ path: "{{ item.stdout | trim }}"
+ state: absent
+ loop: "{{ regional_currency_exchange_reset_online_paths.results }}"
+ loop_control:
+ label: "{{ item.item.section }} {{ item.item.option }}"
+
+- name: Delete generated exchange configuration
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - /etc/taler-exchange/conf.d/setup.conf
+ - "/etc/taler-exchange/conf.d/{{ regional_currency_currency }}-coins.conf"
+ - /etc/taler-merchant/conf.d/setup.conf
+
+- name: Delete exchange provisioning markers
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "{{ regional_currency_state_directory }}/exchange-account-setup"
+ - "{{ regional_currency_state_directory }}/conversion-account-setup"
+ - "{{ regional_currency_state_directory }}/exchange-signed-coin-config"
+ - /var/lib/taler-exchange-offline-maintenance/fee-year