From e0bf89d83d5bf97fe4d8cea3e79e75c59266e4d5 Mon Sep 17 00:00:00 2001 From: Andrea Dell'Amico Date: Sun, 23 Aug 2026 10:55:12 +0200 Subject: [PATCH] Fix the configuration template and the restart script. --- README.md | 14 ++++++- defaults/main.yml | 1 + tasks/mailman.yml | 9 +++++ ...ailman-verified-restart.service.systemd.j2 | 2 +- templates/mailman-verified-restart.sh.j2 | 37 ++++++++++++++++--- templates/mailman.cfg.j2 | 4 +- templates/mailman.service.systemd.j2 | 4 ++ 7 files changed, 60 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index aa566f0..1da497d 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,14 @@ collection is empty; lists with one or more existing rules are left untouched. ## Verified weekly restart The optional weekly restart performs separate stop and start operations. It -waits for both systemd and `mailman status` to confirm that Mailman has stopped -before starting it, then checks both signals again to confirm startup: +waits for systemd, `mailman status`, and the configured LMTP listener to confirm +that Mailman has stopped before starting it. It then requires the master and +the LMTP port to become available before considering startup successful: ```yaml mailman_enable_weekly_verified_restart: true mailman_weekly_verified_restart_on_calendar: 'Sun *-*-* 04:00:00' +mailman_service_stop_timeout: 30 mailman_weekly_verified_restart_stop_timeout: 120 mailman_weekly_verified_restart_start_timeout: 120 ``` @@ -41,3 +43,11 @@ mailman_weekly_verified_restart_start_timeout: 120 The timer is deliberately not persistent, so a missed run is not executed immediately after a server boot. Failures are recorded by systemd in the `mailman-verified-restart.service` journal. + +The managed `mailman.service` uses `KillMode=control-group` and an explicit +`TimeoutStopSec`. If Mailman's own stop command hangs, systemd terminates the +whole service cgroup and removes a stale master PID file. The weekly job still +verifies the real stopped state and does not start a second instance until the +master and LMTP listener are gone. +The normal role execution also waits for the configured LMTP port and fails if +the listener does not become available after starting or restarting Mailman. diff --git a/defaults/main.yml b/defaults/main.yml index 7378b13..a15e0cd 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -40,6 +40,7 @@ mailman_enable_daily_notifications: true # default and, when enabled, uses the managed host's local timezone. mailman_enable_weekly_verified_restart: false mailman_weekly_verified_restart_on_calendar: 'Sun *-*-* 04:00:00' +mailman_service_stop_timeout: 30 mailman_weekly_verified_restart_stop_timeout: 120 mailman_weekly_verified_restart_start_timeout: 120 diff --git a/tasks/mailman.yml b/tasks/mailman.yml index 3428204..50ec0e5 100644 --- a/tasks/mailman.yml +++ b/tasks/mailman.yml @@ -103,6 +103,15 @@ service: name=mailman state=restarted when: mailman_hyperkitty_install is changed + - name: Wait for the Mailman LMTP listener + ansible.builtin.wait_for: + host: '{{ mailman_lmtp_host }}' + port: '{{ mailman_lmtp_port }}' + state: started + sleep: 1 + timeout: '{{ mailman_weekly_verified_restart_start_timeout }}' + when: not ansible_check_mode + - name: Install the mailman and mailmansuite logrotate configurations template: src={{ item }}-logrotate.j2 dest=/etc/logrotate.d/{{ item }} owner=root mode=0444 with_items: diff --git a/templates/mailman-verified-restart.service.systemd.j2 b/templates/mailman-verified-restart.service.systemd.j2 index 305fd5d..d77f6d9 100644 --- a/templates/mailman-verified-restart.service.systemd.j2 +++ b/templates/mailman-verified-restart.service.systemd.j2 @@ -6,4 +6,4 @@ Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/mailman-verified-restart -TimeoutStartSec={{ (mailman_weekly_verified_restart_stop_timeout | int) + (mailman_weekly_verified_restart_start_timeout | int) + 30 }} +TimeoutStartSec={{ (mailman_service_stop_timeout | int) + (mailman_weekly_verified_restart_stop_timeout | int) + (mailman_weekly_verified_restart_start_timeout | int) + 30 }} diff --git a/templates/mailman-verified-restart.sh.j2 b/templates/mailman-verified-restart.sh.j2 index 94880d2..70daf55 100644 --- a/templates/mailman-verified-restart.sh.j2 +++ b/templates/mailman-verified-restart.sh.j2 @@ -5,6 +5,8 @@ set -uo pipefail readonly SERVICE='mailman.service' readonly MAILMAN='{{ mailman_bindir }}/mailman' readonly CONFIG='{{ mailman_conf_dir }}/mailman.cfg' +readonly LMTP_PORT={{ mailman_lmtp_port | int }} +readonly SYSTEMD_STOP_TIMEOUT={{ mailman_service_stop_timeout | int }} readonly STOP_TIMEOUT={{ mailman_weekly_verified_restart_stop_timeout | int }} readonly START_TIMEOUT={{ mailman_weekly_verified_restart_start_timeout | int }} @@ -13,16 +15,40 @@ log() printf 'mailman-verified-restart: %s\n' "$*" } +lmtp_is_listening() +{ + local sockets + local suffix=":${LMTP_PORT}" + + if ! sockets="$(ss -H -ltn)"; then + return 2 + fi + + awk -v suffix="${suffix}" ' + substr($4, length($4) - length(suffix) + 1) == suffix { + found = 1 + } + END { exit !found } + ' <<< "${sockets}" +} + mailman_is_stopped() { - ! systemctl is-active --quiet "${SERVICE}" && + local listener_status + + lmtp_is_listening + listener_status=$? + + [[ ${listener_status} -eq 1 ]] && + ! systemctl is-active --quiet "${SERVICE}" && ! "${MAILMAN}" -C "${CONFIG}" status >/dev/null 2>&1 } mailman_is_started() { systemctl is-active --quiet "${SERVICE}" && - "${MAILMAN}" -C "${CONFIG}" status >/dev/null 2>&1 + "${MAILMAN}" -C "${CONFIG}" status >/dev/null 2>&1 && + lmtp_is_listening } wait_for_state() @@ -41,9 +67,8 @@ wait_for_state() } log 'stopping Mailman' -if ! systemctl stop "${SERVICE}"; then - log 'ERROR: systemctl could not stop Mailman' - exit 1 +if ! timeout "$((SYSTEMD_STOP_TIMEOUT + 10))" systemctl stop "${SERVICE}"; then + log 'WARNING: systemctl stop did not complete successfully; verifying the actual state' fi if ! wait_for_state mailman_is_stopped "${STOP_TIMEOUT}"; then @@ -65,4 +90,4 @@ if ! wait_for_state mailman_is_started "${START_TIMEOUT}"; then exit 1 fi -log 'Mailman is active and its master process is running' +log "Mailman is active, its master is running and LMTP port ${LMTP_PORT} is listening" diff --git a/templates/mailman.cfg.j2 b/templates/mailman.cfg.j2 index 5ccfb33..e23ee21 100644 --- a/templates/mailman.cfg.j2 +++ b/templates/mailman.cfg.j2 @@ -242,7 +242,7 @@ header_checks: # The chain to jump to if any of the header patterns matches. This must be # the name of an existing chain such as 'discard', 'reject', 'hold', or # 'accept', otherwise 'hold' will be used. -jump_chain: "{{ mailman_antispam_chain_behaviour }}" +jump_chain: {{ mailman_antispam_chain_behaviour }} [runner.nntp] class: mailman.runners.nntp.NNTPRunner @@ -253,4 +253,4 @@ start: {{ mailman_start_nntp_runner }} class: mailman_hyperkitty.Archiver enable: yes configuration: {{ mailman_conf_dir }}/mailman-hyperkitty.cfg -{% endif %} \ No newline at end of file +{% endif %} diff --git a/templates/mailman.service.systemd.j2 b/templates/mailman.service.systemd.j2 index 0089dbf..9085288 100644 --- a/templates/mailman.service.systemd.j2 +++ b/templates/mailman.service.systemd.j2 @@ -10,6 +10,7 @@ Environment=PATH={{ mailman_bindir }} ExecStart={{ mailman_bindir }}/mailman -C {{ mailman_conf_dir }}/mailman.cfg start ExecReload={{ mailman_bindir }}/mailman -C {{ mailman_conf_dir }}/mailman.cfg restart ExecStop={{ mailman_bindir }}/mailman -C {{ mailman_conf_dir }}/mailman.cfg stop +ExecStopPost=/bin/rm -f {{ mailman_var_dir }}/master.pid Type=forking PIDFile={{ mailman_var_dir }}/master.pid SyslogIdentifier=mailman @@ -17,6 +18,9 @@ User={{ mailman_user }} Group={{ mailman_user }} Restart=on-failure RestartSec=5s +TimeoutStopSec={{ mailman_service_stop_timeout }}s +KillMode=control-group +SendSIGKILL=yes [Install] WantedBy=multi-user.target