ansible-taler-exchange

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

commit 20099ef394c2d2da2b5dd141e78bcea12f2495b0
parent 46e5f24e02759407348cfbf8d85ef85843cbdee7
Author: Florian Dold <dold@taler.net>
Date:   Sat,  5 Sep 2026 20:12:41 +0200

Deployment: keep applications stopped until configuration succeeds

Discover active services and activation units directly from systemd.
Stop applications before package work and defer startup until
configuration, migrations and handlers finish. Leave applications
stopped on failure without removing boot enablement, and resume skipped
optional services.

Suppress application package starts and restore the administrator
service policy after system upgrades, including failures. Permit
unrelated OS service actions through the temporary upgrade policy.

Diffstat:
MREADME | 17+++++++++++++++++
Acontrib/tests/test_upgrade_policy.py | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplaybooks/setup.yml | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mroles/auditor/tasks/main.yml | 15+--------------
Mroles/challenger/tasks/post-exchange.yml | 7-------
Mroles/challenger/tasks/pre-exchange.yml | 61+++++++++++--------------------------------------------------
Mroles/common_packages/tasks/main.yml | 12+++++-------
Aroles/common_packages/tasks/upgrade.yml | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aroles/common_packages/templates/upgrade-policy-rc.d.j2 | 17+++++++++++++++++
Mroles/database/tasks/main.yml | 1+
Mroles/devtesting/tasks/enable.yml | 1+
Mroles/exchange/tasks/main.yml | 36++----------------------------------
Mroles/libeufin-nexus/tasks/main.yml | 30+-----------------------------
Aroles/start_services/tasks/main.yml | 39+++++++++++++++++++++++++++++++++++++++
Aroles/stop_services/defaults/main.yml | 3+++
Mroles/stop_services/tasks/main.yml | 84++++++++++++++++++++++++++++++-------------------------------------------------
Mroles/webserver/tasks/main.yml | 3+++
17 files changed, 377 insertions(+), 212 deletions(-)

diff --git a/README b/README @@ -332,6 +332,23 @@ Also run by the CI job in "contrib/ci/jobs/001-build". ## Deployment safety and recovery +Normal deployment still upgrades system packages. It stops existing application +services, targets, timers and sockets before package work, suppresses application +starts through Debian's policy-rc.d mechanism, and starts applications +only after all configuration and database migrations finish. Schedule deployment +as downtime. During the system-wide upgrade, the temporary policy denies Taler +service actions and delegates other actions to the existing administrator policy +(or permits them if none existed). Application package tasks use policy_rc_d: 101. +The original policy is restored even on failure. PostgreSQL and nginx are started +as needed during provisioning. + +A failed deployment leaves applications stopped without removing their boot +enablement. Inspect the failed task, correct the problem, then rerun deploy.sh. +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 +services before returning the host to service. + Setting dangerously_enable_devtesting to false revokes the managed login keys, sudo access and helper programs, terminates that account's sessions and disables the account. Its home-directory data is retained. Setting it true restores the diff --git a/contrib/tests/test_upgrade_policy.py b/contrib/tests/test_upgrade_policy.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Check the selective upgrade policy and its restoration on real apt failure.""" +from pathlib import Path +import shlex +import subprocess +import tempfile +import unittest + +from jinja2 import Environment + +TEMPLATE = Path(__file__).resolve().parents[2] / 'roles/common_packages/templates/upgrade-policy-rc.d.j2' + + +def verify_upgrade_policy_restoration(container, play): + # This callback is only invoked in test.sh'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 +chmod 755 /usr/sbin/taler-test-original-policy +ln -s taler-test-original-policy /usr/sbin/policy-rc.d +''') + tasks = [{'ansible.builtin.include_role': {'name': 'common_packages', 'tasks_from': 'upgrade'}}] + try: + play(tasks, check_mode=True) + assert container('readlink', '/usr/sbin/policy-rc.d').stdout.strip() == 'taler-test-original-policy' + play(tasks) + assert container('readlink', '/usr/sbin/policy-rc.d').stdout.strip() == 'taler-test-original-policy' + # fcntl (not flock) is the locking mechanism used by apt/dpkg. + container('systemd-run', '--unit=taler-upgrade-lock-test', 'python3', '-c', + 'import fcntl,time; f=open("/var/lib/dpkg/lock-frontend","w"); ' + 'fcntl.lockf(f,fcntl.LOCK_EX); open("/tmp/taler-upgrade-locked","w").close(); time.sleep(120)') + container('sh', '-eu', '-c', 'while [ ! -e /tmp/taler-upgrade-locked ]; do sleep .1; done') + play([{'block': tasks, 'module_defaults': {'ansible.builtin.apt': {'lock_timeout': 0}}}], + expect_failure=True) + assert container('readlink', '/usr/sbin/policy-rc.d').stdout.strip() == 'taler-test-original-policy' + assert container('/usr/sbin/policy-rc.d', 'ssh', 'restart', check=False).returncode == 0 + finally: + container('systemctl', 'stop', 'taler-upgrade-lock-test.service', check=False) + container('rm', '-f', '/usr/sbin/policy-rc.d', '/usr/sbin/taler-test-original-policy', '/tmp/taler-upgrade-locked') + print('PASS: selective policy restores the original symlink after success and apt failure', flush=True) + + +class UpgradePolicyTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix='taler-policy-test-') + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + + def policy(self, original=None): + backup = self.root / "original policy's file" + if original: + backup.write_text(original) + backup.chmod(0o700) + env = Environment() + env.filters['quote'] = shlex.quote + script = self.root / 'policy' + script.write_text(env.from_string(TEMPLATE.read_text()).render( + common_upgrade_original_policy={'stat': {'exists': original is not None}}, + common_upgrade_policy_backup={'path': str(backup)})) + return script + + def test_only_managed_applications_are_denied_without_an_existing_policy(self): + script = self.policy() + for service in ['taler-exchange.target', 'taler-helper-auditor-wire-credit.service', + 'libeufin-nexus', 'challenger-httpd', 'sms-challenger-httpd', + 'email-challenger-httpd', 'postal-challenger-httpd']: + self.assertEqual(subprocess.run(['sh', str(script), service, 'restart']).returncode, 101) + self.assertEqual(subprocess.run(['sh', str(script), 'ssh', 'restart']).returncode, 0) + self.assertEqual(subprocess.run(['sh', str(script), '--quiet', 'taler-exchange', 'start']).returncode, 101) + + def test_existing_policy_receives_other_actions_unchanged(self): + script = self.policy('#!/bin/sh\nprintf "%s\\n" "$@"\nexit 17\n') + result = subprocess.run(['sh', str(script), '--quiet', 'nginx', 'restart'], capture_output=True, text=True) + self.assertEqual(result.returncode, 17) + self.assertEqual(result.stdout, '--quiet\nnginx\nrestart\n') + self.assertEqual(subprocess.run(['sh', str(script), 'taler-exchange', 'start']).returncode, 101) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/playbooks/setup.yml b/playbooks/setup.yml @@ -68,23 +68,84 @@ - libeufin_nexus_ebics_system_id is defined quiet: true - roles: - - role: common_packages - - role: webserver - - role: database - - role: libeufin-nexus - - role: challenger - when: deploy_challenger | bool - postexchange: false - - role: exchange - - role: challenger - postexchange: true - when: deploy_challenger | bool - - role: auditor - when: deploy_auditor | bool - - role: devtesting + tasks: + - name: Deploy with applications stopped until configuration is complete + block: + - name: Stop existing applications before any package upgrades + ansible.builtin.include_role: + name: stop_services + vars: + stop_services_include_merchant: false + + - name: Remember previously active applications + ansible.builtin.set_fact: + deployment_previously_active_units: "{{ stop_services_active_units }}" + + - name: Configure common_packages + ansible.builtin.include_role: + name: common_packages + + - name: Configure webserver + ansible.builtin.include_role: + name: webserver + + - name: Configure database + ansible.builtin.include_role: + name: database + + - name: Configure libeufin-nexus + ansible.builtin.include_role: + name: libeufin-nexus + + - name: Configure challenger + ansible.builtin.include_role: + name: challenger + when: deploy_challenger | bool + vars: + postexchange: false + + - name: Configure exchange + ansible.builtin.include_role: + name: exchange + + - name: Configure challenger after exchange + ansible.builtin.include_role: + name: challenger + when: deploy_challenger | bool + vars: + postexchange: true + + - name: Configure auditor + ansible.builtin.include_role: + name: auditor + when: deploy_auditor | bool + + - name: Configure devtesting + ansible.builtin.include_role: + name: devtesting + + - name: Apply pending configuration handlers before starting applications + ansible.builtin.meta: flush_handlers + + - name: Start configured applications + ansible.builtin.include_role: + name: start_services + + - name: Run post-deployment sanity checks + ansible.builtin.include_role: + name: post_deployment_checks + + rescue: + - name: Leave applications stopped after a deployment failure + ansible.builtin.include_role: + name: stop_services + vars: + stop_services_include_merchant: false + when: not ansible_check_mode - post_tasks: - - name: Run post-deployment sanity checks - ansible.builtin.include_role: - name: post_deployment_checks + - name: Report the failed deployment + ansible.builtin.fail: + msg: >- + Deployment failed at {{ ansible_failed_task.name }}. + {{ 'Check mode made no service changes.' if ansible_check_mode else + 'Applications remain stopped; inspect the preceding error and correct it before redeploying.' }} diff --git a/roles/auditor/tasks/main.yml b/roles/auditor/tasks/main.yml @@ -2,15 +2,9 @@ - name: Get the list of services service_facts: -- name: Ensure taler-auditor service is stopped before upgrading - systemd: - name: taler-auditor.target - state: stopped - enabled: false - when: '"taler-auditor.target" in ansible_facts["services"]' - - name: Install Taler auditor package apt: + policy_rc_d: 101 name: - taler-auditor state: latest @@ -140,10 +134,3 @@ chdir: /tmp become: true become_user: taler-exchange-httpd - -- name: Ensure taler-auditor service is enabled and started - service: - daemon_reload: true - name: taler-auditor.target - state: started - enabled: true diff --git a/roles/challenger/tasks/post-exchange.yml b/roles/challenger/tasks/post-exchange.yml @@ -36,10 +36,3 @@ owner: taler-exchange-httpd group: taler-exchange-kyc mode: "0644" - -- name: Ensure taler-exchange service is is restarted with new configuration - service: - daemon_reload: true - name: taler-exchange.target - state: restarted - enabled: true diff --git a/roles/challenger/tasks/pre-exchange.yml b/roles/challenger/tasks/pre-exchange.yml @@ -1,37 +1,7 @@ --- -- name: Populate service facts - service_facts: - -- name: Ensure default challenger service is stopped - ansible.builtin.systemd: - name: challenger-httpd - state: stopped - enabled: false - when: '"challenger-httpd.service" in ansible_facts["services"]' - -- name: Ensure SMS challenger service is stopped before we upgrade - ansible.builtin.systemd: - name: sms-challenger - state: stopped - enabled: false - when: '"sms-challenger.service" in ansible_facts["services"]' - -- name: Ensure email challenger service is stopped before we upgrade - ansible.builtin.systemd: - name: email-challenger - state: stopped - enabled: false - when: '"email-challenger.service" in ansible_facts["services"]' - -- name: Ensure postal challenger service is stopped before we upgrade - ansible.builtin.systemd: - name: postal-challenger - state: stopped - enabled: false - when: '"postal-challenger.service" in ansible_facts["services"]' - - name: Install Challenger packages (and dependencies) ansible.builtin.apt: + policy_rc_d: 101 name: - challenger-httpd - taler-challenger-helpers @@ -40,6 +10,16 @@ state: latest when: ansible_facts["os_family"] == 'Debian' +- name: Populate service facts + service_facts: + +- name: Ensure default challenger service is stopped + ansible.builtin.systemd: + name: challenger-httpd + state: stopped + enabled: false + when: '"challenger-httpd.service" in ansible_facts["services"]' + - name: Ensure group "challenger-sms" exists ansible.builtin.group: name: challenger-sms @@ -343,25 +323,6 @@ group: root mode: "0644" -- name: Ensure SMS challenger service is enabled and started - ansible.builtin.systemd: - daemon_reload: true - name: sms-challenger-httpd - state: restarted - enabled: true - -- name: Ensure email challenger service is enabled and started - ansible.builtin.systemd: - name: email-challenger-httpd - state: restarted - enabled: true - -- name: Ensure postal challenger service is enabled and started - ansible.builtin.systemd: - name: postal-challenger-httpd - state: restarted - enabled: true - - name: Generate challenger nginx configuration files (tls config) ansible.builtin.template: src: templates/etc/nginx/conf.d/challenger-tls.conf.inc diff --git a/roles/common_packages/tasks/main.yml b/roles/common_packages/tasks/main.yml @@ -14,6 +14,7 @@ - name: Install packages required by Ansible apt: + policy_rc_d: 101 update_cache: true name: - python3-debian @@ -63,17 +64,13 @@ group: root mode: "0644" -- name: Deploy current base distro - apt: - state: latest - update_cache: true - autoclean: true - autoremove: true - upgrade: safe +- name: Upgrade the base distribution while keeping applications stopped + ansible.builtin.include_tasks: upgrade.yml when: ansible_facts["os_family"] == 'Debian' - name: Install Taler dependencies on Debian/Ubuntu apt: + policy_rc_d: 101 name: - curl - jq @@ -88,6 +85,7 @@ - name: Install robocop if sanction lists are in use apt: + policy_rc_d: 101 name: - robocop state: latest diff --git a/roles/common_packages/tasks/upgrade.yml b/roles/common_packages/tasks/upgrade.yml @@ -0,0 +1,84 @@ +--- +# Unlike a blanket policy_rc_d: 101, this policy still permits unrelated OS +# daemons to perform their normal package upgrade actions. +- name: Reset temporary upgrade policy state + ansible.builtin.set_fact: + common_upgrade_policy_backup: {} + common_upgrade_policy_move: {} + common_upgrade_policy_install: {} + +- name: Upgrade system packages with application starts denied + block: + - name: Allocate a backup beside the existing policy to preserve relative symlinks + ansible.builtin.tempfile: + state: file + path: /usr/sbin + prefix: .taler-upgrade-policy- + register: common_upgrade_policy_backup + when: not ansible_check_mode + + - name: Inspect the existing package service policy + ansible.builtin.stat: + path: /usr/sbin/policy-rc.d + follow: false + register: common_upgrade_original_policy + when: not ansible_check_mode + + - name: Preserve the existing package service policy + ansible.builtin.command: + argv: + - mv + - -- + - /usr/sbin/policy-rc.d + - "{{ common_upgrade_policy_backup.path }}" + register: common_upgrade_policy_move + when: + - not ansible_check_mode + - common_upgrade_original_policy.stat.exists + + - name: Install temporary application service policy + ansible.builtin.template: + src: upgrade-policy-rc.d.j2 + dest: /usr/sbin/policy-rc.d + owner: root + group: root + mode: '0755' + register: common_upgrade_policy_install + when: not ansible_check_mode + + - name: Deploy current base distro + ansible.builtin.apt: + state: latest + update_cache: true + autoclean: true + autoremove: true + upgrade: safe + + always: + - name: Restore the original package service policy + ansible.builtin.command: + argv: + - mv + - -- + - "{{ common_upgrade_policy_backup.path }}" + - /usr/sbin/policy-rc.d + when: + - not ansible_check_mode + - common_upgrade_policy_move.rc | default(-1) == 0 + + - name: Remove the temporary policy when none existed before + ansible.builtin.file: + path: /usr/sbin/policy-rc.d + state: absent + when: + - not ansible_check_mode + - common_upgrade_policy_move.rc | default(-1) != 0 + - common_upgrade_policy_install is changed + + - name: Remove the unused backup placeholder + ansible.builtin.file: + path: "{{ common_upgrade_policy_backup.path }}" + state: absent + when: + - not ansible_check_mode + - common_upgrade_policy_backup.path is defined diff --git a/roles/common_packages/templates/upgrade-policy-rc.d.j2 b/roles/common_packages/templates/upgrade-policy-rc.d.j2 @@ -0,0 +1,17 @@ +#!/bin/sh +# Keep managed applications stopped during the system-wide upgrade. Delegate +# other service actions to the administrator's policy, if one was installed. +service_name=$1 +if [ "$service_name" = --quiet ]; then + service_name=$2 +fi +case "$service_name" in + taler-exchange*|taler-auditor*|taler-helper-auditor*|libeufin-nexus*|challenger-httpd*|sms-challenger-httpd*|email-challenger-httpd*|postal-challenger-httpd*) + exit 101 + ;; +esac +{% if common_upgrade_original_policy.stat.exists %} +exec {{ common_upgrade_policy_backup.path | quote }} "$@" +{% else %} +exit 0 +{% endif %} diff --git a/roles/database/tasks/main.yml b/roles/database/tasks/main.yml @@ -3,6 +3,7 @@ - name: Install PostgreSQL on Debian/Ubuntu ansible.builtin.apt: + policy_rc_d: 101 name: - postgresql - python3-psycopg2 diff --git a/roles/devtesting/tasks/enable.yml b/roles/devtesting/tasks/enable.yml @@ -4,6 +4,7 @@ - name: Install devtesting dependencies apt: + policy_rc_d: 101 name: - python3-click - taler-harness diff --git a/roles/exchange/tasks/main.yml b/roles/exchange/tasks/main.yml @@ -2,22 +2,9 @@ - name: Get the list of services service_facts: -- name: Ensure taler-exchange service is stopped before we upgrade - ansible.builtin.systemd: - name: taler-exchange.target - state: stopped - enabled: false - when: '"taler-exchange.target" in ansible_facts["services"]' - -- name: Ensure sanctionscheck service is stopped before we upgrade - ansible.builtin.systemd: - name: taler-exchange-sanctionscheck - state: stopped - enabled: false - when: '"taler-exchange-sanctionscheck.service" in ansible_facts["services"]' - - name: Install latest Taler exchange package ansible.builtin.apt: + policy_rc_d: 101 name: - taler-exchange - taler-exchange-typst @@ -27,6 +14,7 @@ - name: Install pdftk ansible.builtin.apt: + policy_rc_d: 101 name: - pdftk state: latest @@ -214,16 +202,6 @@ ansible.builtin.include_role: name: "exchange_{{ deployment_kind }}" -# FIXME: Implement this as handler, so it's only -# done when necessary. -- name: Ensure taler-exchange service is enabled and restarted - service: - daemon_reload: true - name: taler-exchange.target - state: restarted - enabled: true - -# Setup sanction list - name: Check if local sanction list file exists delegate_to: localhost run_once: true @@ -250,13 +228,3 @@ mode: "0644" when: sanction_list is defined notify: sanctions-reset - -- name: Ensure sanctionscheck service is restarted after the upgrade - ansible.builtin.systemd: - daemon_reload: true - name: taler-exchange-sanctionscheck - state: started - enabled: true - when: - - '"taler-exchange-sanctionscheck.service" in ansible_facts["services"]' - - sanction_list is defined diff --git a/roles/libeufin-nexus/tasks/main.yml b/roles/libeufin-nexus/tasks/main.yml @@ -2,22 +2,9 @@ - name: Get the list of services service_facts: -- name: Ensure libeufin-nexus service is stopped before we upgrade - systemd: - name: libeufin-nexus.target - state: stopped - enabled: false - when: '"libeufin-nexus.target" in ansible_facts["services"]' - -- name: Ensure libeufin-nexus-httpd service is stopped before we upgrade - service: - name: libeufin-nexus-httpd.service - state: stopped - enabled: false - when: '"libeufin-nexus-httpd.service" in ansible_facts["services"]' - - name: Install libeufin-nexus package apt: + policy_rc_d: 101 name: - libeufin-nexus state: latest @@ -175,21 +162,6 @@ cmd: libeufin-nexus ebics-setup when: use_ebics -- name: Ensure libeufin-nexus target is enabled and started - service: - daemon_reload: true - name: libeufin-nexus.target - state: started - enabled: true - when: use_ebics - -- name: Ensure libeufin-nexus-httpd service is enabled and started - service: - daemon_reload: true - name: libeufin-nexus-httpd.service - state: started - enabled: true - - name: Place login script for libeufin-nexus-import technical user ansible.builtin.copy: src: usr/local/bin/libeufin-nexus-import.sh diff --git a/roles/start_services/tasks/main.yml b/roles/start_services/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Refresh systemd after all package and unit changes + ansible.builtin.systemd: + daemon_reload: true + +- name: Start the configured application units + ansible.builtin.systemd: + name: "{{ item.unit }}" + state: started + enabled: true + loop: + - { unit: libeufin-nexus-httpd.service, wanted: true } + - { unit: libeufin-nexus.target, wanted: "{{ use_ebics | bool }}" } + - { unit: sms-challenger-httpd.service, wanted: "{{ deploy_challenger | bool }}" } + - { unit: email-challenger-httpd.service, wanted: "{{ deploy_challenger | bool }}" } + - { unit: postal-challenger-httpd.service, wanted: "{{ deploy_challenger | bool }}" } + - { unit: taler-exchange.target, wanted: true } + - { unit: taler-auditor.target, wanted: "{{ deploy_auditor | bool }}" } + - { unit: taler-exchange-sanctionscheck.service, wanted: "{{ sanction_list is defined }}" } + loop_control: + label: "{{ item.unit }}" + when: item.wanted | bool + +- name: Keep automatic bank communication disabled in manual mode + ansible.builtin.systemd: + name: libeufin-nexus.target + enabled: false + when: not (use_ebics | bool) + +- name: Restore previously active optional units and activation sources + ansible.builtin.systemd: + name: "{{ item }}" + state: started + loop: "{{ deployment_previously_active_units | default([]) }}" + when: >- + (not (deploy_auditor | bool) and item is match('^(taler-auditor|taler-helper-auditor)')) + or (not (deploy_challenger | bool) and item is match('^(challenger-httpd|sms-challenger-httpd|email-challenger-httpd|postal-challenger-httpd)')) + or (item is search('[.](timer|socket)$') + and not (item is match('^libeufin-nexus') and not (use_ebics | bool))) diff --git a/roles/stop_services/defaults/main.yml b/roles/stop_services/defaults/main.yml @@ -0,0 +1,3 @@ +--- +# Reboot also stops a co-located merchant. Setup manages only its own applications. +stop_services_include_merchant: true diff --git a/roles/stop_services/tasks/main.yml b/roles/stop_services/tasks/main.yml @@ -1,53 +1,33 @@ --- -# Stop all Taler services - -- name: Get the list of services - service_facts: - -- name: Stop exchange - systemd: - name: taler-exchange.target - state: stopped - when: '"taler-exchange.target" in ansible_facts["services"]' - -- name: Stop merchant - systemd: - name: taler-merchant.target - state: stopped - when: '"taler-merchant.target" in ansible_facts["services"]' - -- name: Stop postal-challenger - systemd: - name: postal-challenger-httpd.service - state: stopped - when: '"postal-challenger-httpd.service" in ansible_facts["services"]' - -- name: Stop sms-challenger - systemd: - name: sms-challenger-httpd.service - state: stopped - when: '"sms-challenger-httpd.service" in ansible_facts["services"]' - -- name: Stop email-challenger - systemd: - name: email-challenger-httpd.service - state: stopped - when: '"email-challenger-httpd.service" in ansible_facts["services"]' - -- name: Stop auditor - systemd: - name: taler-auditor.target - state: stopped - when: '"taler-auditor.target" in ansible_facts["services"]' - -- name: Stop libeufin-nexus - systemd: - name: libeufin-nexus.target - state: stopped - when: '"libeufin-nexus.target" in ansible_facts["services"]' - -- name: Stop libeufin-nexus-httpd - systemd: - name: libeufin-nexus-httpd.service - state: stopped - when: '"libeufin-nexus-httpd.service" in ansible_facts["services"]' +# service_facts lists services, not targets. Query systemd itself, including +# timers and sockets which could reactivate a service during a migration. +- name: Discover active systemd units + ansible.builtin.command: + argv: + - systemctl + - list-units + - --state=active,activating,reloading,deactivating + - --plain + - --no-legend + - --no-pager + register: stop_services_systemd_units + changed_when: false + check_mode: false + +- name: Record application units to stop + ansible.builtin.set_fact: + stop_services_active_units: >- + {{ stop_services_systemd_units.stdout_lines + | map('split') | map('first') + | select('match', '^(taler-exchange.*|taler-auditor.*|taler-helper-auditor.*|libeufin-nexus.*|challenger-httpd|sms-challenger-httpd|email-challenger-httpd|postal-challenger-httpd' + ~ ('|taler-merchant.*' if stop_services_include_merchant | bool else '') + ~ ')[.](service|target|timer|socket)$') | list }} + +- name: Stop application units without changing boot enablement + ansible.builtin.systemd: + name: "{{ item }}" + state: stopped + # Stop activation sources before the services they can start. + loop: >- + {{ (stop_services_active_units | select('search', '[.](timer|socket)$') | list) + + (stop_services_active_units | reject('search', '[.](timer|socket)$') | list) }} diff --git a/roles/webserver/tasks/main.yml b/roles/webserver/tasks/main.yml @@ -3,6 +3,7 @@ - name: Install Nginx apt: + policy_rc_d: 101 name: nginx state: present update_cache: true @@ -10,6 +11,7 @@ - name: Install certbot base package apt: + policy_rc_d: 101 name: certbot state: present update_cache: true @@ -17,6 +19,7 @@ - name: Install certbot nginx plugin apt: + policy_rc_d: 101 name: python3-certbot-nginx state: present update_cache: true