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:
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