Add selective Postfix SRS installation and management
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
|||||||
|
# Selektives Sender Rewriting Scheme (SRS)
|
||||||
|
|
||||||
|
**Stand:** 23. Oktober 2026
|
||||||
|
**Projekt:** `mailsystem` – Debian 13, Postfix, PostfixAdmin/PostgreSQL
|
||||||
|
**Status:** Integration auf `e.mx.oopen.de` erfolgreich getestet; Resttests ausstehend
|
||||||
|
|
||||||
|
## Ziel und Geltungsbereich
|
||||||
|
|
||||||
|
Das System soll den SMTP-Envelope-Absender **nur bei tatsaechlichen externen
|
||||||
|
Weiterleitungen ueber virtuelle Mail-Aliase** mittels SRS umschreiben.
|
||||||
|
Andere Nachrichten sollen ihren Envelope-Absender behalten. Virtuelle
|
||||||
|
Aliase werden unter anderem ueber PostfixAdmin und PostgreSQL verwaltet.
|
||||||
|
|
||||||
|
Bei einer Weiterleitung kann die urspruengliche Absenderdomain den SPF-Test
|
||||||
|
am Zielserver nicht bestehen: Der weiterleitende Mailserver ist normalerweise
|
||||||
|
nicht fuer diese Domain autorisiert. SRS ersetzt deshalb den SMTP-Envelope-
|
||||||
|
Absender durch eine signierte Adresse der eigenen SRS-Domain. Der sichtbare
|
||||||
|
`From:`-Header wird dadurch nicht veraendert; DKIM und DMARC muessen
|
||||||
|
unabhaengig betrachtet werden.
|
||||||
|
|
||||||
|
## Technischer Aufbau
|
||||||
|
|
||||||
|
- **Regulaerer Postfix:** Annahme, AmaViS-Pruefung, virtuelle Aliasaufloesung.
|
||||||
|
- **`srs-forward` (Pipe-Dienst):** Uebergabe an `srs-forward.py` und
|
||||||
|
selektive Entscheidung ueber externe Alias-Weiterleitungen.
|
||||||
|
- **PostSRSd 2.4.1:** SRS-Umschreibung und Rueckuebersetzung ueber Socketmap.
|
||||||
|
- **Zweite Postfix-Instanz:** SMTP-Reinjektion und anschliessende Zustellung.
|
||||||
|
- **Reverse-Komponenten:** Annahme und Verarbeitung eingehender SRS-Ruecklaeufer
|
||||||
|
waehrend des aktiven SRS-Betriebs.
|
||||||
|
|
||||||
|
Waehrend SRS **aktiviert** ist, wird `default_transport = srs-forward`
|
||||||
|
verwendet. Deshalb durchlaufen auch Nachrichten ohne SRS-Bedarf die eigene
|
||||||
|
Entscheidungs- und Reinjektionsstrecke. **Nur die eigentliche SRS-Umschreibung
|
||||||
|
ist selektiv.** Dieses betriebliche Risiko ist weiterhin Teil der Architektur;
|
||||||
|
der getestete Rueckbau beseitigt diese Abhaengigkeit im deaktivierten Zustand.
|
||||||
|
|
||||||
|
### Lokale Endpunkte
|
||||||
|
|
||||||
|
| Endpunkt | Funktion |
|
||||||
|
| --- | --- |
|
||||||
|
| `127.0.0.1:10003` | PostSRSd-Socketmap |
|
||||||
|
| `127.0.0.1:10004` | Reverse-Socketmap |
|
||||||
|
| `127.0.0.1:10028` | Reinjektion in die SRS-Delivery-Instanz |
|
||||||
|
| `127.0.0.1:10029` | Reverse-SMTP-Eingang fuer Ruecklaeufer |
|
||||||
|
|
||||||
|
### Wichtige Dateien und Dienste
|
||||||
|
|
||||||
|
- `conf/srs.conf` – lokale SRS-Domain; **nicht** unveraendert als allgemeine
|
||||||
|
Vorlage auf andere Hosts uebertragen.
|
||||||
|
- `conf/srs.conf.sample` – Konfigurationsbeispiel.
|
||||||
|
- `/etc/postfix-srs/state.conf` – bei der Installation gespeicherter
|
||||||
|
urspruenglicher Postfix-Zustand; nach Deaktivierung beibehalten.
|
||||||
|
- `/etc/postsrsd-srs/secret` – geheimer SRS-Schluessel; vor fremdem Zugriff
|
||||||
|
schuetzen und beim Update erhalten.
|
||||||
|
- `/etc/postfix-srs/delivery/` – Konfiguration der zweiten Postfix-Instanz.
|
||||||
|
- `/var/spool/postfix-srs-delivery/` – Queue der zweiten Instanz.
|
||||||
|
- `/usr/local/postsrsd-<version>` und `/usr/local/postsrsd` – versionierte
|
||||||
|
Installation und Symlink.
|
||||||
|
- Dienste: `postsrsd-srs.service`, `srs-reverse-map.service`,
|
||||||
|
`postfix@srs-delivery.service`.
|
||||||
|
|
||||||
|
## Betriebszustaende
|
||||||
|
|
||||||
|
**Installiert, aber DISABLED:** SRS-Dateien und der persistente Zustand
|
||||||
|
bleiben erhalten. Der regulaere Postfix verwendet weder `srs-forward`
|
||||||
|
noch die laufenden SRS-Dienste.
|
||||||
|
|
||||||
|
**ENABLED:** Die Postfix-Hauptkonfiguration enthaelt die verwalteten
|
||||||
|
SRS-Bloecke, die drei SRS-Dienste laufen und die Einbindung ist aktiv.
|
||||||
|
|
||||||
|
**Inkonsistenter Zustand:** Die Verwaltungsskripte sollen nicht stillschweigend
|
||||||
|
korrigieren, sondern mit Diagnose abbrechen.
|
||||||
|
|
||||||
|
## Zweiphasige Deaktivierung
|
||||||
|
|
||||||
|
`deactivate-srs.sh` stellt den urspruenglichen Postfix-Zustand wieder her.
|
||||||
|
Vorher muss die SRS-Delivery-Queue leer sein.
|
||||||
|
|
||||||
|
1. **Neue SRS-Uebergaben beenden:** Urspruengliche `main.cf`-Parameter und
|
||||||
|
Empfangseinstellungen wiederherstellen; den alten `srs-forward`-Pipe-Dienst
|
||||||
|
in `master.cf` vorerst belassen; alten Queue-Manager erfassen und den
|
||||||
|
regulaeren Postfix neu laden.
|
||||||
|
2. **Alte Arbeit auslaufen lassen:** Auf den alten Queue-Manager, erkannte
|
||||||
|
`srs-forward`-Prozesse und eine leere SRS-Delivery-Queue warten (maximal
|
||||||
|
180 Sekunden). Die Queue wird wiederholt kontrolliert.
|
||||||
|
3. **Abschalten:** `srs-forward` aus `master.cf` und SRS-systemd-Abhaengigkeiten
|
||||||
|
entfernen; Postfix erneut pruefen und neu laden; Queue erneut pruefen;
|
||||||
|
SRS-Dienste stoppen und deaktivieren; Endzustand verifizieren.
|
||||||
|
|
||||||
|
Kann die Queue nicht geleert oder nicht gelesen werden, oder schlaegt ein
|
||||||
|
Schritt fehl, bricht das Skript ab und **versucht**, den vorherigen aktiven
|
||||||
|
Zustand zurueckzusetzen. Automatischer Rollback ist kein Ersatz fuer
|
||||||
|
Backups und muss fuer Fehlerfaelle noch integriert getestet werden.
|
||||||
|
|
||||||
|
**Wichtiger Unterschied:** Die zwei Phasen dienen der geordneten Umschaltung.
|
||||||
|
Auch bei anfangs leerer SRS-Queue werden beide Phasen durchlaufen.
|
||||||
|
|
||||||
|
### Bewusst akzeptierte Grenze: verspaetete Ruecklaeufer
|
||||||
|
|
||||||
|
Ein Ruecklaeufer an eine bereits versendete SRS-Adresse kann erst Tage nach
|
||||||
|
der Deaktivierung eintreffen. Eine solche Nachricht ist bei der Abschaltung
|
||||||
|
noch nicht in der Queue und kann nicht abgewartet werden.
|
||||||
|
|
||||||
|
**Projektentscheidung:** Nach vollstaendiger Deaktivierung muessen solche
|
||||||
|
spaeten SRS-Ruecklaeufer nicht mehr zugestellt oder rueckuebersetzt werden.
|
||||||
|
Es gibt absichtlich keinen Reverse-only-Modus und keine Uebergangsfrist.
|
||||||
|
Vorrang haben die sichere Behandlung bereits angenommener Nachrichten und
|
||||||
|
ein funktionierender, von SRS unabhaengiger regulaerer Postfix-Betrieb.
|
||||||
|
|
||||||
|
## Nachgewiesene Integrationstests vom 23. Oktober 2026
|
||||||
|
|
||||||
|
Auf `e.mx.oopen.de` mit der SRS-Domain `srs.e.mx.oopen.de`:
|
||||||
|
|
||||||
|
- `install-update-srs.sh --check`: DNS-/SPF- und Installationsvorpruefung OK.
|
||||||
|
- `install-update-srs.sh --install`: PostSRSd 2.4.1 gebaut; Upstream-CTest OK;
|
||||||
|
Installation ohne automatische Aktivierung.
|
||||||
|
- `activate-srs.sh`: Zustand `DISABLED` erkannt; Aktivierung nach Bestaetigung
|
||||||
|
und abschliessende Pruefungen erfolgreich.
|
||||||
|
- Externe virtuelle Weiterleitung `alias@henschelbuch.de` nach
|
||||||
|
`ckubu@oopen.de`: PostSRSd protokollierte die Umschreibung eines GMX-
|
||||||
|
Envelope-Absenders nach `SRS0=...@srs.e.mx.oopen.de`; der empfangende
|
||||||
|
SMTP-Server bestaetigte die Uebernahme (`250 2.0.0`).
|
||||||
|
- `deactivate-srs.sh`: Beide Reloads, Drain-Pruefung, Entfernung des
|
||||||
|
Pipe-Dienstes, Abschaltung aller SRS-Dienste und Verifikation erfolgreich.
|
||||||
|
- Dieselbe externe Weiterleitung nach der Deaktivierung: Regulaerer
|
||||||
|
`postfix/smtp` stellte mit unveraendertem Envelope-Absender an den
|
||||||
|
naechsten SMTP-Server zu (`250 2.0.0`). Die regulaere Postfix-Queue war leer.
|
||||||
|
|
||||||
|
Die genannten SMTP-Ergebnisse belegen die Annahme durch den naechsten
|
||||||
|
Mailserver, nicht automatisch die endgueltige Zustellung ins Postfach.
|
||||||
|
|
||||||
|
**Noch offen:** Erzwungener Rollback-Integrationstest und Deaktivierung
|
||||||
|
unter anhaltender Zustelllast. Eine uneingeschraenkte Freigabe fuer
|
||||||
|
Produktivsysteme wird hieraus nicht abgeleitet.
|
||||||
|
|
||||||
|
## Bekannte Hinweise
|
||||||
|
|
||||||
|
In der Testumgebung erschienen unabhaengige Postfix-Dateiberechtigungswarnungen
|
||||||
|
und Warnungen zur Verwendung von `nobody` durch den Reverse-Map-Dienst bzw.
|
||||||
|
zur fehlenden seccomp-Sandbox von PostSRSd. Diese Meldungen sind bei einer
|
||||||
|
spaeteren Wartung gesondert zu untersuchen. Sie wurden nicht als Nachweis
|
||||||
|
fuer eine fehlerhafte SRS-Zustellung gewertet.
|
||||||
|
|
||||||
|
Fuer Befehle zur Installation, Diagnose und Deaktivierung siehe
|
||||||
|
[README-install-SRS.md](README-install-SRS.md). Der Stand vom 20. Oktober
|
||||||
|
bleibt unter [`archive/selective-srs-2026/`](archive/selective-srs-2026/)
|
||||||
|
unveraendert erhalten.
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# Selektives SRS – Installation und Betrieb
|
||||||
|
|
||||||
|
**Stand:** 23. Oktober 2026
|
||||||
|
**Geltungsbereich:** Aktuelle Skripte dieses Repositories; Debian 13/Postfix
|
||||||
|
**Status:** Erfolgreicher End-to-End-Test auf `e.mx.oopen.de`; Fehlerfalltests offen
|
||||||
|
|
||||||
|
Siehe auch [README-SRS.md](README-SRS.md) fuer Architektur,
|
||||||
|
Abhaengigkeiten und bewusst akzeptierte Grenzen.
|
||||||
|
|
||||||
|
## 1. Voraussetzungen
|
||||||
|
|
||||||
|
Die regulaere Postfix-Installation muss funktionsfaehig sein. Virtuelle
|
||||||
|
Alias-Weiterleitungen und AmaViS-Reinjektion muessen bekannt und getestet
|
||||||
|
sein. Die SRS-Domain braucht passende oeffentlich aufloesbare DNS-Eintraege.
|
||||||
|
Die Installationspruefung kontrolliert die autoritativen Nameserver sowie
|
||||||
|
SPF und MX im vorgesehenen Setup.
|
||||||
|
|
||||||
|
Die Hostkonfiguration liegt in `conf/srs.conf`; als Ausgangspunkt dient
|
||||||
|
`conf/srs.conf.sample`. Beim Aufruf wird die gewaehlte SRS-Domain angezeigt
|
||||||
|
und bestaetigt. Fuer den Testserver wurde `srs.e.mx.oopen.de` verwendet;
|
||||||
|
diese Domain ist **kein** universeller Standard fuer andere Hosts.
|
||||||
|
|
||||||
|
Die Skripte werden in diesem Repository als `root` ausgefuehrt. Vor
|
||||||
|
Installation oder Aktivierung Backups und einen Wartungs-/Rueckbauplan
|
||||||
|
bereithalten. Keines der Skripte ersetzt die Kontrolle des Mailflusses.
|
||||||
|
|
||||||
|
## 2. Lesende Installationspruefung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /usr/local/src/mailsystem || exit 1
|
||||||
|
./install-update-srs.sh --help
|
||||||
|
./install-update-srs.sh --check
|
||||||
|
```
|
||||||
|
|
||||||
|
`--check` fragt nach der SRS-Domain, prueft Voraussetzungen und inventarisiert
|
||||||
|
die Installation, ohne den Zustand des Systems zu veraendern. Fehlende
|
||||||
|
DNS-Konfiguration zuerst korrigieren, nicht die Vorpruefung umgehen.
|
||||||
|
|
||||||
|
## 3. Installation oder Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /usr/local/src/mailsystem || exit 1
|
||||||
|
./install-update-srs.sh --install
|
||||||
|
```
|
||||||
|
|
||||||
|
Standardversion: PostSRSd `2.4.1`. Fuer eine andere Version steht
|
||||||
|
`--version X.Y.Z` zur Verfuegung. Der Ablauf enthaelt eine Bestaetigung
|
||||||
|
fuer die Vorbereitung und eine gesonderte Bestaetigung fuer die Installation
|
||||||
|
der vorbereiteten Dateien. Unter anderem werden PostSRSd gebaut, die
|
||||||
|
Upstream-Tests ausgefuehrt, Dateien und Dienste vorbereitet und ein Backup
|
||||||
|
angelegt. Aktive SRS-Integration verhindert laut Installer-Logik ein Update.
|
||||||
|
|
||||||
|
**Wichtig:** `--install` aktiviert **nicht** die selektive SRS-Verarbeitung
|
||||||
|
im regulaeren Postfix und startet die SRS-Dienste nicht. Die Anzeige des
|
||||||
|
Backup-Pfads ist aufzubewahren. Das SRS-Secret bei Updates unveraendert
|
||||||
|
erhalten, damit fruehere SRS-Adressen ueberpruefbar bleiben.
|
||||||
|
|
||||||
|
## 4. Selektives SRS aktivieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /usr/local/src/mailsystem || exit 1
|
||||||
|
./activate-srs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Skript prueft die Installation, Laufzeitdienste, Ports und den bisherigen
|
||||||
|
Postfix-Zustand. Bei `DISABLED` zeigt es eine Zusammenfassung und fordert
|
||||||
|
eine ausdrueckliche Bestaetigung. Erst danach werden Sicherungen erstellt,
|
||||||
|
die verwalteten Konfigurationsbloecke eingesetzt, Dienste gestartet und
|
||||||
|
Postfix neu geladen. Bei inkonsistentem Zustand nicht manuell fortfahren,
|
||||||
|
sondern Ursache klaeren.
|
||||||
|
|
||||||
|
### Kontrolle nach Aktivierung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl is-active postfix.service
|
||||||
|
systemctl is-active postsrsd-srs.service srs-reverse-map.service postfix@srs-delivery.service
|
||||||
|
postconf -h default_transport
|
||||||
|
postconf -M srs-forward/unix
|
||||||
|
postqueue -j
|
||||||
|
postqueue -c /etc/postfix-srs/delivery -j
|
||||||
|
```
|
||||||
|
|
||||||
|
Der aktivierte Transport ist `srs-forward`. Die SRS-Delivery-Instanz ist
|
||||||
|
gesondert zu kontrollieren. Ein erfolgreicher Service-Start beweist noch
|
||||||
|
keine erfolgreiche externe Weiterleitung: Eine reale Testnachricht an eine
|
||||||
|
virtuelle Alias-Adresse mit externem Ziel ist erforderlich. Im Journal
|
||||||
|
sollten die PostSRSd-Umschreibung, die Reinjektion in die zweite Instanz
|
||||||
|
und die SMTP-Uebergabe an den naechsten Mailserver nachvollziehbar sein.
|
||||||
|
|
||||||
|
## 5. Zweiphasig deaktivieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /usr/local/src/mailsystem || exit 1
|
||||||
|
./deactivate-srs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Deaktivierung ist interaktiv und prueft vor der Bestaetigung, dass
|
||||||
|
der Zustand `ENABLED` konsistent und die SRS-Delivery-Queue **leer** ist.
|
||||||
|
|
||||||
|
Nach Bestaetigung:
|
||||||
|
|
||||||
|
1. Urspruengliche `main.cf`-Parameter und Empfangseinstellungen
|
||||||
|
wiederherstellen; `srs-forward` zunaechst in `master.cf` belassen;
|
||||||
|
alten Queue-Manager erfassen und Postfix neu laden.
|
||||||
|
2. Auf Beendigung des alten Queue-Managers und erkennbarer
|
||||||
|
`srs-forward`-Arbeit sowie auf leere SRS-Delivery-Queue warten
|
||||||
|
(maximal 180 Sekunden).
|
||||||
|
3. Pipe-Dienst und systemd-Abhaengigkeiten entfernen; erneut pruefen
|
||||||
|
und laden; Queue erneut pruefen; alle drei SRS-Dienste stoppen und
|
||||||
|
deaktivieren; Endzustand verifizieren.
|
||||||
|
|
||||||
|
Wenn beim Start bereits Nachrichten in der SRS-Delivery-Queue stehen,
|
||||||
|
verweigert das Skript die Deaktivierung. Wird die Queue waehrend der
|
||||||
|
Umschaltung nicht leer oder kann nicht gelesen werden, fuehrt das zu
|
||||||
|
Abbruch und einem **versuchten** Rollback. Wartende Mails werden nicht
|
||||||
|
absichtlich geloescht.
|
||||||
|
|
||||||
|
Die Installation, Binaries und `/etc/postfix-srs/state.conf` bleiben
|
||||||
|
nach erfolgreicher Deaktivierung fuer eine spaetere Aktivierung erhalten.
|
||||||
|
|
||||||
|
**Verspaetete SRS-Ruecklaeufer:** Nach vollstaendiger Deaktivierung koennen
|
||||||
|
spaet eintreffende Bounces an SRS-Adressen nicht mehr wie im aktiven Betrieb
|
||||||
|
verarbeitet werden. Das ist eine bewusste Projektentscheidung; ein
|
||||||
|
Reverse-only-Modus ist nicht vorgesehen. Der normale Postfix-Betrieb hat
|
||||||
|
Vorrang. Details: [README-SRS.md](README-SRS.md).
|
||||||
|
|
||||||
|
### Kontrolle nach Deaktivierung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl is-active postfix.service
|
||||||
|
systemctl is-active postsrsd-srs.service srs-reverse-map.service postfix@srs-delivery.service
|
||||||
|
systemctl is-enabled postsrsd-srs.service srs-reverse-map.service postfix@srs-delivery.service
|
||||||
|
postconf -h default_transport
|
||||||
|
postconf -M srs-forward/unix 2>/dev/null || true
|
||||||
|
postqueue -j
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle SRS-Dienste sollen `inactive` und `disabled` sein; der Pipe-Dienst
|
||||||
|
soll fehlen. Auf dem Testserver war `default_transport` danach wieder
|
||||||
|
`smtp` (impliziter Standard). Die urspruenglichen, nicht explizit gesetzten
|
||||||
|
Parameter sind nicht mehr Teil der lokalen `main.cf`.
|
||||||
|
|
||||||
|
**Keine vermeintliche Null-Queue erzwingen:** Bei gestoppter zweiter Instanz
|
||||||
|
kann `postqueue -c ... -j` mit einer `showq`-Warnung scheitern. Das
|
||||||
|
Kommando nicht blind mit `wc -l` zu einem angeblichen Erfolg zusammenfassen.
|
||||||
|
Der sichere Leerstand wird waehrend der Deaktivierung bei laufender Instanz
|
||||||
|
geprueft. Bei Bedarf die Queue-Dateien offline und mit Bedacht inspizieren.
|
||||||
|
|
||||||
|
### Wichtiger Funktionstest nach der Deaktivierung
|
||||||
|
|
||||||
|
Eine Nachricht erneut an dieselbe externe virtuelle Alias-Weiterleitung
|
||||||
|
senden. Im Mail-Log muss der regulaere `postfix/smtp`-Transport und der
|
||||||
|
urspruengliche Envelope-Absender sichtbar sein. Auch eine lokale
|
||||||
|
Zustellung sollte unveraendert funktionieren. Die erfolgreiche SMTP-Uebergabe
|
||||||
|
an den naechsten Mailserver ist noch kein Beweis fuer den Postfacheingang.
|
||||||
|
|
||||||
|
## 6. Log- und Queue-Diagnose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl --since "15 minutes ago" --no-pager | grep -Ei 'srs-forward|postfix-srs-delivery|postsrsd|SRS0='
|
||||||
|
|
||||||
|
postqueue -j
|
||||||
|
# Nur waehrend die zweite Instanz laeuft:
|
||||||
|
postqueue -c /etc/postfix-srs/delivery -j
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei Fehlern keine Dateien aus der SRS-Queue entfernen, um die Deaktivierung
|
||||||
|
zu erzwingen. Zuerst Ursache und enthaltene Nachrichten klaeren. Die
|
||||||
|
Backup-Verzeichnisse unter `/root/postfix-srs-activation-*` bzw.
|
||||||
|
`/root/postfix-srs-deactivation-*` zusammen mit den Journalmeldungen sichern.
|
||||||
|
|
||||||
|
## 7. Getesteter Stand und ausstehende Pruefungen
|
||||||
|
|
||||||
|
Am **23. Oktober 2026** auf `e.mx.oopen.de` erfolgreich getestet:
|
||||||
|
Installationsvorpruefung, Installation mit Upstream-CTest, Aktivierung,
|
||||||
|
eine echte externe Alias-Weiterleitung mit SRS, zweiphasige Deaktivierung,
|
||||||
|
anschliessend dieselbe Weiterleitung ohne SRS. Beide Postfix-Queues waren
|
||||||
|
zum Zeitpunkt der abschliessenden Kontrolle leer.
|
||||||
|
|
||||||
|
**Noch nicht praktisch nachgewiesen:** Ein absichtlich ausgeloester
|
||||||
|
Fehler mit vollstaendigem Rollback und die Deaktivierung unter
|
||||||
|
anhaltender Zustelllast. Die isolierten Mock-Tests ersetzen diese
|
||||||
|
Integrationstests nicht.
|
||||||
|
|
||||||
|
## 8. Historische Dokumentation
|
||||||
|
|
||||||
|
Die ausfuehrlichen Texte und frueheren Entwicklungsnotizen vom
|
||||||
|
20. Oktober 2026 verbleiben unveraendert in
|
||||||
|
[`archive/selective-srs-2026/`](archive/selective-srs-2026/).
|
||||||
|
Sie sind keine aktuelle Installationsanleitung.
|
||||||
|
|
||||||
|
## Oeffentlicher MX-Hostname und SRS-Domain
|
||||||
|
|
||||||
|
Der lokale Systemhostname (`hostname -f`) kann vom oeffentlichen
|
||||||
|
MX-Hostnamen abweichen.
|
||||||
|
|
||||||
|
Der Installer fragt deshalb den oeffentlichen MX-Hostnamen separat ab.
|
||||||
|
Als Vorgabe verwendet er `hostname -f`. Bei einer Erstinstallation wird
|
||||||
|
die SRS-Domain aus `srs.<MX-Hostname>` vorgeschlagen.
|
||||||
|
|
||||||
|
Beispiel fuer `mail.interventionistische-linke.org`:
|
||||||
|
|
||||||
|
_MX_HOST="mail.interventionistische-linke.org"
|
||||||
|
_SRS_DOMAIN="srs.mail.interventionistische-linke.org"
|
||||||
|
|
||||||
|
Beide Werte werden in `conf/srs.conf` gespeichert.
|
||||||
|
|
||||||
|
Bei einer bestehenden SRS-Konfiguration bleiben die bereits
|
||||||
|
konfigurierten Werte erhalten. Fehlt lediglich `_MX_HOST`, wird dieser
|
||||||
|
Parameter bei einem Wartungsupdate ergaenzt.
|
||||||
|
|
||||||
|
Erforderliche DNS-Eintraege fuer das Beispiel:
|
||||||
|
|
||||||
|
srs.mail.interventionistische-linke.org. IN MX 10 mail.interventionistische-linke.org.
|
||||||
|
srs.mail.interventionistische-linke.org. IN TXT "v=spf1 mx -all"
|
||||||
|
|
||||||
|
Der Installer prueft die oeffentlichen DNS-Eintraege ueber die
|
||||||
|
autoritativen Nameserver.
|
||||||
|
|
||||||
|
Mit `./install-update-srs.sh --check` kann die System- und
|
||||||
|
DNS-Vorpruefung ohne Aenderung der Installation ausgefuehrt werden.
|
||||||
@@ -30,22 +30,40 @@ Ergaenzende Dateien liegen unter anderem in:
|
|||||||
Weitere Informationen enthalten die vorhandenen
|
Weitere Informationen enthalten die vorhandenen
|
||||||
README-Dateien, insbesondere README.install.
|
README-Dateien, insbesondere README.install.
|
||||||
|
|
||||||
|
## Selektives SRS fuer Postfix
|
||||||
|
|
||||||
|
Die Entwicklung der selektiven SRS-Loesung wurde am 21. Oktober 2026
|
||||||
|
wieder aufgenommen. Die aktuellen Skripte liegen im Hauptverzeichnis:
|
||||||
|
|
||||||
|
- `install-update-srs.sh` – Installation und Aktualisierung
|
||||||
|
- `activate-srs.sh` – SRS-Integration explizit aktivieren
|
||||||
|
- `deactivate-srs.sh` – SRS-Integration kontrolliert deaktivieren
|
||||||
|
|
||||||
|
Aktuelle Dokumentation:
|
||||||
|
|
||||||
|
- [README-SRS.md](README-SRS.md) – Architektur, Grenzen und Teststand
|
||||||
|
- [README-install-SRS.md](README-install-SRS.md) – Installation und Betrieb
|
||||||
|
|
||||||
|
Am 23. Oktober 2026 wurde auf `e.mx.oopen.de` ein vollstaendiger
|
||||||
|
Integrationstest erfolgreich durchgefuehrt: Installation, Aktivierung,
|
||||||
|
externe virtuelle Alias-Weiterleitung mit SRS, zweiphasige Deaktivierung
|
||||||
|
und dieselbe Weiterleitung ohne SRS. Der erzwungene Rollback und die
|
||||||
|
Deaktivierung bei anhaltender Zustelllast sind noch nicht praktisch
|
||||||
|
getestet. Eine uneingeschraenkte Produktivfreigabe wird damit nicht
|
||||||
|
behauptet.
|
||||||
|
|
||||||
## Archivierte Entwicklungen
|
## Archivierte Entwicklungen
|
||||||
|
|
||||||
Abgeschlossene oder eingestellte Entwicklungsarbeiten
|
Abgeschlossene oder eingestellte historische Entwicklungsstaende
|
||||||
werden unter archive/ aufbewahrt.
|
werden unter `archive/` aufbewahrt.
|
||||||
|
|
||||||
### Selective SRS (2026)
|
### Historischer SRS-Stand (20. Oktober 2026)
|
||||||
|
|
||||||
Die im Oktober 2026 entwickelte selektive SRS-Loesung
|
Der damalige Entwicklungsstand wurde am 20. Oktober archiviert. Die
|
||||||
wurde nach abschliessender Architekturpruefung
|
Archivtexte beschreiben bewusst die damalige Einstellung und sind keine
|
||||||
eingestellt und von den Mailservern entfernt.
|
aktuelle Betriebsanleitung:
|
||||||
|
|
||||||
Der vollstaendige historische Entwicklungsstand ist
|
[Selective SRS 2026 – historisches Archiv](archive/selective-srs-2026/README.md)
|
||||||
hier dokumentiert:
|
|
||||||
|
|
||||||
[Selective SRS 2026](archive/selective-srs-2026/README.md)
|
Fuer aktuelle Installationen und Betriebsablaeufe ausschliesslich die
|
||||||
|
oben verlinkte Dokumentation und die Skripte im Hauptverzeichnis verwenden.
|
||||||
Die archivierten SRS-Skripte sind nicht fuer den
|
|
||||||
produktiven Einsatz freigegeben und duerfen nicht
|
|
||||||
unveraendert zur Installation verwendet werden.
|
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ master_service_option_value() {
|
|||||||
|
|
||||||
|
|
||||||
master_srs_forward_present() {
|
master_srs_forward_present() {
|
||||||
|
local master_contents
|
||||||
|
|
||||||
|
# Read failures must not be indistinguishable from an absent service.
|
||||||
|
master_contents="$(< "$POSTFIX_MASTER_CF")" || return 2
|
||||||
|
|
||||||
awk '
|
awk '
|
||||||
/^[[:space:]]*#/ { next }
|
/^[[:space:]]*#/ { next }
|
||||||
|
|
||||||
@@ -133,15 +138,19 @@ master_srs_forward_present() {
|
|||||||
exit 0
|
exit 0
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
' "$POSTFIX_MASTER_CF"
|
' <<< "$master_contents"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
port_is_listening() {
|
port_is_listening() {
|
||||||
local host="$1"
|
local host="$1"
|
||||||
local port="$2"
|
local port="$2"
|
||||||
|
local sockets rc
|
||||||
|
|
||||||
|
# Capture the status of ss separately: without this, an ss failure
|
||||||
|
# can look like an empty socket listing.
|
||||||
|
sockets="$(ss -ltnH 2>> "$log_file")" || return 2
|
||||||
|
|
||||||
ss -ltnH 2>/dev/null |
|
|
||||||
awk -v host="$host" -v port="$port" '
|
awk -v host="$host" -v port="$port" '
|
||||||
{
|
{
|
||||||
local_address=$4
|
local_address=$4
|
||||||
@@ -156,7 +165,12 @@ port_is_listening() {
|
|||||||
END {
|
END {
|
||||||
exit(found ? 0 : 1)
|
exit(found ? 0 : 1)
|
||||||
}
|
}
|
||||||
'
|
' <<< "$sockets"
|
||||||
|
rc=$?
|
||||||
|
case "$rc" in
|
||||||
|
0|1) return "$rc" ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -165,13 +179,62 @@ service_exists() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
read_srs_service_state() {
|
||||||
|
local output line key value
|
||||||
|
local load_state="" active_state="" enabled_state=""
|
||||||
|
|
||||||
|
SRS_SERVICE_ACTIVE_STATE=""
|
||||||
|
SRS_SERVICE_ENABLED_STATE=""
|
||||||
|
|
||||||
|
output="$(systemctl show "$1" --no-pager \
|
||||||
|
-p LoadState -p ActiveState -p UnitFileState 2>> "$log_file")" || return 2
|
||||||
|
|
||||||
|
while IFS= read -r line ; do
|
||||||
|
key="${line%%=*}"
|
||||||
|
value="${line#*=}"
|
||||||
|
[[ "$line" == *=* ]] || return 2
|
||||||
|
|
||||||
|
case "$key" in
|
||||||
|
LoadState)
|
||||||
|
[[ -z "$load_state" ]] || return 2
|
||||||
|
load_state="$value"
|
||||||
|
;;
|
||||||
|
ActiveState)
|
||||||
|
[[ -z "$active_state" ]] || return 2
|
||||||
|
active_state="$value"
|
||||||
|
;;
|
||||||
|
UnitFileState)
|
||||||
|
[[ -z "$enabled_state" ]] || return 2
|
||||||
|
enabled_state="$value"
|
||||||
|
;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
done <<< "$output"
|
||||||
|
|
||||||
|
[[ "$load_state" == "loaded" ]] || return 2
|
||||||
|
case "$active_state" in
|
||||||
|
active|inactive) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
case "$enabled_state" in
|
||||||
|
enabled|enabled-runtime|disabled) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
SRS_SERVICE_ACTIVE_STATE="$active_state"
|
||||||
|
SRS_SERVICE_ENABLED_STATE="$enabled_state"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
service_active() {
|
service_active() {
|
||||||
systemctl is-active --quiet "$1"
|
read_srs_service_state "$1" || return 2
|
||||||
|
[[ "$SRS_SERVICE_ACTIVE_STATE" == "active" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
service_enabled_state() {
|
service_enabled_state() {
|
||||||
systemctl is-enabled "$1" 2>/dev/null || true
|
read_srs_service_state "$1" || return 2
|
||||||
|
printf '%s\n' "$SRS_SERVICE_ENABLED_STATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -592,49 +655,90 @@ restore_systemd_dropins() {
|
|||||||
|
|
||||||
snapshot_service_states() {
|
snapshot_service_states() {
|
||||||
local service
|
local service
|
||||||
|
local -A snapshot_active=() snapshot_enabled=()
|
||||||
|
|
||||||
for service in \
|
for service in \
|
||||||
"$POSTSRSD_SERVICE" \
|
"$POSTSRSD_SERVICE" \
|
||||||
"$SRS_REVERSE_MAP_SERVICE" \
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
"$SRS_DELIVERY_SERVICE"
|
"$SRS_DELIVERY_SERVICE"
|
||||||
do
|
do
|
||||||
if service_active "$service" ; then
|
if ! read_srs_service_state "$service" ; then
|
||||||
SERVICE_WAS_ACTIVE["$service"]="true"
|
warn "Cannot determine a supported systemd state for '${service}'."
|
||||||
else
|
return 1
|
||||||
SERVICE_WAS_ACTIVE["$service"]="false"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SERVICE_WAS_ENABLED["$service"]="$(service_enabled_state "$service")"
|
# The caller has classified this installation as DISABLED. Reject
|
||||||
|
# a state transition since that check rather than capturing an
|
||||||
|
# unexpectedly started/enabled service as the rollback baseline.
|
||||||
|
if [[ "$SRS_SERVICE_ACTIVE_STATE" != "inactive" ||
|
||||||
|
"$SRS_SERVICE_ENABLED_STATE" != "disabled" ]]; then
|
||||||
|
warn "SRS service '${service}' changed state before activation."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
snapshot_active["$service"]="false"
|
||||||
|
snapshot_enabled["$service"]="$SRS_SERVICE_ENABLED_STATE"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Only publish a complete, validated snapshot.
|
||||||
|
for service in \
|
||||||
|
"$POSTSRSD_SERVICE" \
|
||||||
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
|
"$SRS_DELIVERY_SERVICE"
|
||||||
|
do
|
||||||
|
SERVICE_WAS_ACTIVE["$service"]="${snapshot_active[$service]}"
|
||||||
|
SERVICE_WAS_ENABLED["$service"]="${snapshot_enabled[$service]}"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
restore_service_states() {
|
restore_service_states() {
|
||||||
local service
|
local service
|
||||||
local restore_failed=false
|
local restore_failed=false
|
||||||
local enabled_state
|
local enabled_state active_state
|
||||||
|
|
||||||
|
# Validate the entire snapshot before changing *any* unit.
|
||||||
|
for service in \
|
||||||
|
"$POSTSRSD_SERVICE" \
|
||||||
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
|
"$SRS_DELIVERY_SERVICE"
|
||||||
|
do
|
||||||
|
enabled_state="${SERVICE_WAS_ENABLED[$service]:-}"
|
||||||
|
active_state="${SERVICE_WAS_ACTIVE[$service]:-}"
|
||||||
|
case "$enabled_state" in
|
||||||
|
enabled|enabled-runtime|disabled) ;;
|
||||||
|
*) warn "Invalid saved enable state for '${service}': '${enabled_state}'."; return 1 ;;
|
||||||
|
esac
|
||||||
|
case "$active_state" in
|
||||||
|
true|false) ;;
|
||||||
|
*) warn "Invalid saved active state for '${service}': '${active_state}'."; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
for service in \
|
for service in \
|
||||||
"$POSTSRSD_SERVICE" \
|
"$POSTSRSD_SERVICE" \
|
||||||
"$SRS_REVERSE_MAP_SERVICE" \
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
"$SRS_DELIVERY_SERVICE"
|
"$SRS_DELIVERY_SERVICE"
|
||||||
do
|
do
|
||||||
if [[ "${SERVICE_WAS_ACTIVE[$service]:-false}" == "true" ]]; then
|
enabled_state="${SERVICE_WAS_ENABLED[$service]}"
|
||||||
systemctl start "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
|
||||||
else
|
|
||||||
systemctl stop "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
enabled_state="${SERVICE_WAS_ENABLED[$service]:-}"
|
|
||||||
|
|
||||||
case "$enabled_state" in
|
case "$enabled_state" in
|
||||||
enabled|enabled-runtime)
|
enabled)
|
||||||
systemctl enable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
systemctl enable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
;;
|
;;
|
||||||
|
enabled-runtime)
|
||||||
|
systemctl enable --runtime "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
|
;;
|
||||||
disabled)
|
disabled)
|
||||||
systemctl disable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
systemctl disable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ "${SERVICE_WAS_ACTIVE[$service]}" == "true" ]]; then
|
||||||
|
systemctl start "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
|
else
|
||||||
|
systemctl stop "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
! $restore_failed
|
! $restore_failed
|
||||||
@@ -708,6 +812,11 @@ rollback_activation() {
|
|||||||
echo_failed
|
echo_failed
|
||||||
rollback_failed=true
|
rollback_failed=true
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echononl "Check primary Postfix service '${POSTFIX_SERVICE}' is active.."
|
||||||
|
echo_failed
|
||||||
|
warn "Primary Postfix is inactive; restored configuration could not be reloaded."
|
||||||
|
rollback_failed=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $rollback_failed ; then
|
if $rollback_failed ; then
|
||||||
@@ -870,6 +979,9 @@ blank_line
|
|||||||
|
|
||||||
services_exist=true
|
services_exist=true
|
||||||
services_active=true
|
services_active=true
|
||||||
|
services_enabled=true
|
||||||
|
services_disabled=true
|
||||||
|
services_status_known=true
|
||||||
|
|
||||||
for _service in \
|
for _service in \
|
||||||
"$POSTSRSD_SERVICE" \
|
"$POSTSRSD_SERVICE" \
|
||||||
@@ -884,26 +996,42 @@ do
|
|||||||
echo_failed
|
echo_failed
|
||||||
services_exist=false
|
services_exist=false
|
||||||
services_active=false
|
services_active=false
|
||||||
|
services_enabled=false
|
||||||
|
services_disabled=false
|
||||||
|
services_status_known=false
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if service_active "$_service" ; then
|
echononl "Read systemd state of '${_service}'.."
|
||||||
echononl "Check systemd service '${_service}' runtime state (active).."
|
if ! read_srs_service_state "$_service" ; then
|
||||||
echo_ok
|
echo_failed
|
||||||
else
|
services_status_known=false
|
||||||
echononl "Check systemd service '${_service}' runtime state (inactive).."
|
|
||||||
echo_ok
|
|
||||||
services_active=false
|
services_active=false
|
||||||
|
services_enabled=false
|
||||||
|
services_disabled=false
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo_ok
|
||||||
|
|
||||||
|
if [[ "$SRS_SERVICE_ACTIVE_STATE" != "active" ]]; then
|
||||||
|
services_active=false
|
||||||
|
fi
|
||||||
|
if [[ "$SRS_SERVICE_ENABLED_STATE" == "disabled" ]]; then
|
||||||
|
services_enabled=false
|
||||||
|
fi
|
||||||
|
if [[ "$SRS_SERVICE_ACTIVE_STATE" != "inactive" ||
|
||||||
|
"$SRS_SERVICE_ENABLED_STATE" != "disabled" ]]; then
|
||||||
|
services_disabled=false
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echononl "Check main Postfix service '${POSTFIX_SERVICE}' is active.."
|
echononl "Check main Postfix service '${POSTFIX_SERVICE}' is active.."
|
||||||
|
primary_postfix_active=false
|
||||||
if service_active "$POSTFIX_SERVICE" ; then
|
if systemctl is-active --quiet "$POSTFIX_SERVICE" ; then
|
||||||
echo_ok
|
echo_ok
|
||||||
|
primary_postfix_active=true
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
services_active=false
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@@ -916,16 +1044,31 @@ echo -e "\033[37m\033[1mCheck selective SRS listeners..\033[m"
|
|||||||
blank_line
|
blank_line
|
||||||
|
|
||||||
listeners_ok=true
|
listeners_ok=true
|
||||||
|
listeners_absent=true
|
||||||
|
listeners_status_known=true
|
||||||
|
|
||||||
while IFS='|' read -r _label _host _port ; do
|
while IFS='|' read -r _label _host _port ; do
|
||||||
if port_is_listening "$_host" "$_port" ; then
|
port_is_listening "$_host" "$_port"
|
||||||
|
_port_rc=$?
|
||||||
|
case "$_port_rc" in
|
||||||
|
0)
|
||||||
echononl "Check ${_label} state on ${_host}:${_port} (listening).."
|
echononl "Check ${_label} state on ${_host}:${_port} (listening).."
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
listeners_absent=false
|
||||||
|
;;
|
||||||
|
1)
|
||||||
echononl "Check ${_label} state on ${_host}:${_port} (inactive).."
|
echononl "Check ${_label} state on ${_host}:${_port} (inactive).."
|
||||||
echo_ok
|
echo_ok
|
||||||
listeners_ok=false
|
listeners_ok=false
|
||||||
fi
|
;;
|
||||||
|
*)
|
||||||
|
echononl "Check ${_label} state on ${_host}:${_port} (query failed).."
|
||||||
|
echo_failed
|
||||||
|
listeners_ok=false
|
||||||
|
listeners_absent=false
|
||||||
|
listeners_status_known=false
|
||||||
|
;;
|
||||||
|
esac
|
||||||
done <<EOF
|
done <<EOF
|
||||||
PostSRSd socketmap|${POSTSRSD_SOCKET_HOST}|${POSTSRSD_SOCKET_PORT}
|
PostSRSd socketmap|${POSTSRSD_SOCKET_HOST}|${POSTSRSD_SOCKET_PORT}
|
||||||
SRS reverse socketmap|${SRS_REVERSE_SOCKET_HOST}|${SRS_REVERSE_SOCKET_PORT}
|
SRS reverse socketmap|${SRS_REVERSE_SOCKET_HOST}|${SRS_REVERSE_SOCKET_PORT}
|
||||||
@@ -933,6 +1076,13 @@ SRS reinjection listener|${SRS_REINJECT_HOST}|${SRS_REINJECT_PORT}
|
|||||||
SRS reverse listener|${SRS_REVERSE_HOST}|${SRS_REVERSE_PORT}
|
SRS reverse listener|${SRS_REVERSE_HOST}|${SRS_REVERSE_PORT}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
dropins_absent=true
|
||||||
|
for _dropin in "$POSTFIX_SRS_DROPIN_FILE" "$SRS_DELIVERY_DROPIN_FILE" ; do
|
||||||
|
if [[ -e "$_dropin" || -L "$_dropin" ]]; then
|
||||||
|
dropins_absent=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Determine main.cf state
|
# Determine main.cf state
|
||||||
@@ -1055,17 +1205,27 @@ case "$_master_marker_rc" in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
master_srs_forward_ok=false
|
master_srs_forward_ok=false
|
||||||
|
master_srs_forward_known=true
|
||||||
smtp_override_ok=false
|
smtp_override_ok=false
|
||||||
amavis_override_ok=false
|
amavis_override_ok=false
|
||||||
|
|
||||||
echononl "Validate 'srs-forward' pipe service state.."
|
echononl "Validate 'srs-forward' pipe service state.."
|
||||||
if master_srs_forward_present ; then
|
master_srs_forward_present
|
||||||
|
_master_service_rc=$?
|
||||||
|
case "$_master_service_rc" in
|
||||||
|
0)
|
||||||
echo_ok
|
echo_ok
|
||||||
master_srs_forward_ok=true
|
master_srs_forward_ok=true
|
||||||
else
|
;;
|
||||||
|
1)
|
||||||
# Absent is valid in the stored pre-SRS state.
|
# Absent is valid in the stored pre-SRS state.
|
||||||
echo_ok
|
echo_ok
|
||||||
fi
|
;;
|
||||||
|
*)
|
||||||
|
echo_failed
|
||||||
|
master_srs_forward_known=false
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
echononl "Validate port 25 receive_override_options state.."
|
echononl "Validate port 25 receive_override_options state.."
|
||||||
if [[ "$(master_service_option_value smtp receive_override_options)" == "$smtp_active_value" ]]; then
|
if [[ "$(master_service_option_value smtp receive_override_options)" == "$smtp_active_value" ]]; then
|
||||||
@@ -1203,6 +1363,7 @@ srs_state="inconsistent"
|
|||||||
|
|
||||||
if [[ "$main_block_status" == "present" ]] &&
|
if [[ "$main_block_status" == "present" ]] &&
|
||||||
[[ "$master_block_status" != "invalid" ]] &&
|
[[ "$master_block_status" != "invalid" ]] &&
|
||||||
|
$master_srs_forward_known &&
|
||||||
$main_srs_values_ok &&
|
$main_srs_values_ok &&
|
||||||
$master_srs_forward_ok &&
|
$master_srs_forward_ok &&
|
||||||
$smtp_override_ok &&
|
$smtp_override_ok &&
|
||||||
@@ -1213,6 +1374,7 @@ fi
|
|||||||
|
|
||||||
if [[ "$main_block_status" == "absent" ]] &&
|
if [[ "$main_block_status" == "absent" ]] &&
|
||||||
[[ "$master_block_status" == "absent" ]] &&
|
[[ "$master_block_status" == "absent" ]] &&
|
||||||
|
$master_srs_forward_known &&
|
||||||
$pre_srs_main_ok &&
|
$pre_srs_main_ok &&
|
||||||
$pre_srs_master_ok &&
|
$pre_srs_master_ok &&
|
||||||
! $master_srs_forward_ok
|
! $master_srs_forward_ok
|
||||||
@@ -1222,7 +1384,11 @@ fi
|
|||||||
|
|
||||||
if $installation_ok &&
|
if $installation_ok &&
|
||||||
$services_exist &&
|
$services_exist &&
|
||||||
|
$services_status_known &&
|
||||||
$services_active &&
|
$services_active &&
|
||||||
|
$services_enabled &&
|
||||||
|
$primary_postfix_active &&
|
||||||
|
$listeners_status_known &&
|
||||||
$listeners_ok
|
$listeners_ok
|
||||||
then
|
then
|
||||||
srs_runtime_ready=true
|
srs_runtime_ready=true
|
||||||
@@ -1236,7 +1402,15 @@ fi
|
|||||||
|
|
||||||
if $srs_integration_enabled && $srs_runtime_ready ; then
|
if $srs_integration_enabled && $srs_runtime_ready ; then
|
||||||
srs_state="enabled"
|
srs_state="enabled"
|
||||||
elif $srs_integration_disabled ; then
|
elif $srs_integration_disabled &&
|
||||||
|
$services_exist &&
|
||||||
|
$services_status_known &&
|
||||||
|
$services_disabled &&
|
||||||
|
$primary_postfix_active &&
|
||||||
|
$listeners_status_known &&
|
||||||
|
$listeners_absent &&
|
||||||
|
$dropins_absent
|
||||||
|
then
|
||||||
srs_state="disabled"
|
srs_state="disabled"
|
||||||
else
|
else
|
||||||
srs_state="inconsistent"
|
srs_state="inconsistent"
|
||||||
@@ -1364,13 +1538,20 @@ fi
|
|||||||
# Prepare rollback
|
# Prepare rollback
|
||||||
# ----------
|
# ----------
|
||||||
|
|
||||||
snapshot_service_states
|
echononl "Snapshot disabled SRS service states.."
|
||||||
|
if snapshot_service_states ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal "Cannot safely snapshot disabled SRS services. Activation was not attempted."
|
||||||
|
fi
|
||||||
|
|
||||||
ACTIVATION_BACKUP_DIR="/root/postfix-srs-activation-${backup_date}"
|
ACTIVATION_BACKUP_DIR="/root/postfix-srs-activation-${backup_date}"
|
||||||
|
|
||||||
echononl "Create activation backup directory '${ACTIVATION_BACKUP_DIR}'.."
|
echononl "Create activation backup directory.."
|
||||||
if mkdir -m 0700 "$ACTIVATION_BACKUP_DIR" > "$log_file" 2>&1 ; then
|
if mkdir -m 0700 "$ACTIVATION_BACKUP_DIR" > "$log_file" 2>&1 ; then
|
||||||
echo_ok
|
echo_ok
|
||||||
|
printf ' Directory: %s\n' "$ACTIVATION_BACKUP_DIR"
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
fatal "$(cat "$log_file")"
|
fatal "$(cat "$log_file")"
|
||||||
@@ -1400,6 +1581,14 @@ else
|
|||||||
fatal "$(cat "$log_file")"
|
fatal "$(cat "$log_file")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echononl "Recheck primary Postfix service '${POSTFIX_SERVICE}' is active.."
|
||||||
|
if systemctl is-active --quiet "$POSTFIX_SERVICE" ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal "Primary Postfix became inactive. Activation was not attempted."
|
||||||
|
fi
|
||||||
|
|
||||||
ACTIVATION_CHANGES_STARTED=true
|
ACTIVATION_CHANGES_STARTED=true
|
||||||
|
|
||||||
|
|
||||||
@@ -1639,7 +1828,10 @@ for _service in \
|
|||||||
do
|
do
|
||||||
echononl "Verify systemd service '${_service}' is active.."
|
echononl "Verify systemd service '${_service}' is active.."
|
||||||
|
|
||||||
if service_active "$_service" ; then
|
if read_srs_service_state "$_service" &&
|
||||||
|
[[ "$SRS_SERVICE_ACTIVE_STATE" == "active" ]] &&
|
||||||
|
[[ "$SRS_SERVICE_ENABLED_STATE" != "disabled" ]]
|
||||||
|
then
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
@@ -1647,6 +1839,30 @@ do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
for _endpoint in \
|
||||||
|
"PostSRSd socketmap|${POSTSRSD_SOCKET_HOST}|${POSTSRSD_SOCKET_PORT}" \
|
||||||
|
"SRS reverse socketmap|${SRS_REVERSE_SOCKET_HOST}|${SRS_REVERSE_SOCKET_PORT}" \
|
||||||
|
"SRS reinjection listener|${SRS_REINJECT_HOST}|${SRS_REINJECT_PORT}" \
|
||||||
|
"SRS reverse listener|${SRS_REVERSE_HOST}|${SRS_REVERSE_PORT}"
|
||||||
|
do
|
||||||
|
IFS='|' read -r _label _host _port <<< "$_endpoint"
|
||||||
|
echononl "Verify ${_label} remains listening on ${_host}:${_port}.."
|
||||||
|
if port_is_listening "$_host" "$_port" ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
verification_ok=false
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echononl "Verify primary Postfix remains active.."
|
||||||
|
if systemctl is-active --quiet "$POSTFIX_SERVICE" ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
verification_ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
echononl "Final Postfix configuration check.."
|
echononl "Final Postfix configuration check.."
|
||||||
if postfix check > "$log_file" 2>&1 ; then
|
if postfix check > "$log_file" 2>&1 ; then
|
||||||
echo_ok
|
echo_ok
|
||||||
@@ -2,6 +2,15 @@
|
|||||||
# - Selective SRS configuration
|
# - Selective SRS configuration
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
|
# Public MX hostname used as the MX target of the SRS domain.
|
||||||
|
# The installer proposes `hostname -f`; another public FQDN may be selected.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
#
|
||||||
|
# _MX_HOST="mail.example.org"
|
||||||
|
#
|
||||||
|
_MX_HOST=""
|
||||||
|
|
||||||
# Domain used as envelope sender domain for SRS rewritten messages.
|
# Domain used as envelope sender domain for SRS rewritten messages.
|
||||||
#
|
#
|
||||||
# DNS/SPF for this domain must authorize the outgoing mail server.
|
# DNS/SPF for this domain must authorize the outgoing mail server.
|
||||||
@@ -47,6 +47,9 @@ SRS_DELIVERY_DROPIN_WAS_PRESENT=false
|
|||||||
declare -A SERVICE_WAS_ACTIVE
|
declare -A SERVICE_WAS_ACTIVE
|
||||||
declare -A SERVICE_WAS_ENABLED
|
declare -A SERVICE_WAS_ENABLED
|
||||||
|
|
||||||
|
# Queue manager processes active before the first (phase-1) reload.
|
||||||
|
SRS_OLD_QMGR_PIDS=()
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Local helper functions
|
# Local helper functions
|
||||||
@@ -112,6 +115,11 @@ master_service_option_value() {
|
|||||||
|
|
||||||
|
|
||||||
master_srs_forward_present() {
|
master_srs_forward_present() {
|
||||||
|
local master_contents
|
||||||
|
|
||||||
|
# Read failures must not be indistinguishable from an absent service.
|
||||||
|
master_contents="$(< "$POSTFIX_MASTER_CF")" || return 2
|
||||||
|
|
||||||
awk '
|
awk '
|
||||||
/^[[:space:]]*#/ { next }
|
/^[[:space:]]*#/ { next }
|
||||||
|
|
||||||
@@ -133,15 +141,19 @@ master_srs_forward_present() {
|
|||||||
exit 0
|
exit 0
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
' "$POSTFIX_MASTER_CF"
|
' <<< "$master_contents"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
port_is_listening() {
|
port_is_listening() {
|
||||||
local host="$1"
|
local host="$1"
|
||||||
local port="$2"
|
local port="$2"
|
||||||
|
local sockets rc
|
||||||
|
|
||||||
|
# Capture the status of ss separately: without this, an ss failure
|
||||||
|
# can look like an empty socket listing.
|
||||||
|
sockets="$(ss -ltnH 2>> "$log_file")" || return 2
|
||||||
|
|
||||||
ss -ltnH 2>/dev/null |
|
|
||||||
awk -v host="$host" -v port="$port" '
|
awk -v host="$host" -v port="$port" '
|
||||||
{
|
{
|
||||||
local_address=$4
|
local_address=$4
|
||||||
@@ -156,7 +168,12 @@ port_is_listening() {
|
|||||||
END {
|
END {
|
||||||
exit(found ? 0 : 1)
|
exit(found ? 0 : 1)
|
||||||
}
|
}
|
||||||
'
|
' <<< "$sockets"
|
||||||
|
rc=$?
|
||||||
|
case "$rc" in
|
||||||
|
0|1) return "$rc" ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -165,24 +182,77 @@ service_exists() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# One validated systemd snapshot per call. Neither a failed query nor an
|
||||||
|
# unsupported/transient state may be mistaken for inactive or disabled.
|
||||||
|
# Returns: 0 = understood; 2 = unknown, unsupported, or query error.
|
||||||
|
# Results are set in SRS_SERVICE_ACTIVE_STATE and SRS_SERVICE_ENABLED_STATE.
|
||||||
|
read_srs_service_state() {
|
||||||
|
local output line key value
|
||||||
|
local load_state="" active_state="" enabled_state=""
|
||||||
|
|
||||||
|
SRS_SERVICE_ACTIVE_STATE=""
|
||||||
|
SRS_SERVICE_ENABLED_STATE=""
|
||||||
|
|
||||||
|
output="$(systemctl show "$1" --no-pager \
|
||||||
|
-p LoadState -p ActiveState -p UnitFileState 2>> "$log_file")" || return 2
|
||||||
|
|
||||||
|
while IFS= read -r line ; do
|
||||||
|
key="${line%%=*}"
|
||||||
|
value="${line#*=}"
|
||||||
|
[[ "$line" == *=* ]] || return 2
|
||||||
|
|
||||||
|
case "$key" in
|
||||||
|
LoadState)
|
||||||
|
[[ -z "$load_state" ]] || return 2
|
||||||
|
load_state="$value"
|
||||||
|
;;
|
||||||
|
ActiveState)
|
||||||
|
[[ -z "$active_state" ]] || return 2
|
||||||
|
active_state="$value"
|
||||||
|
;;
|
||||||
|
UnitFileState)
|
||||||
|
[[ -z "$enabled_state" ]] || return 2
|
||||||
|
enabled_state="$value"
|
||||||
|
;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
done <<< "$output"
|
||||||
|
|
||||||
|
[[ "$load_state" == "loaded" ]] || return 2
|
||||||
|
case "$active_state" in
|
||||||
|
active|inactive) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
case "$enabled_state" in
|
||||||
|
enabled|enabled-runtime|disabled) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
SRS_SERVICE_ACTIVE_STATE="$active_state"
|
||||||
|
SRS_SERVICE_ENABLED_STATE="$enabled_state"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Returns: 0 = active/enabled, 1 = inactive/disabled, 2 = query error.
|
||||||
service_active() {
|
service_active() {
|
||||||
systemctl is-active --quiet "$1"
|
read_srs_service_state "$1" || return 2
|
||||||
|
[[ "$SRS_SERVICE_ACTIVE_STATE" == "active" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
service_enabled_state() {
|
service_enabled_state() {
|
||||||
systemctl is-enabled "$1" 2>/dev/null || true
|
read_srs_service_state "$1" || return 2
|
||||||
|
printf '%s\n' "$SRS_SERVICE_ENABLED_STATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
service_enabled() {
|
service_enabled() {
|
||||||
case "$(service_enabled_state "$1")" in
|
read_srs_service_state "$1" || return 2
|
||||||
enabled|enabled-runtime)
|
case "$SRS_SERVICE_ENABLED_STATE" in
|
||||||
return 0
|
enabled|enabled-runtime) return 0 ;;
|
||||||
;;
|
disabled) return 1 ;;
|
||||||
*)
|
*) return 2 ;;
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,14 +523,11 @@ if begin_idx >= end_idx:
|
|||||||
|
|
||||||
new_lines = lines[:begin_idx] + lines[end_idx + 1:]
|
new_lines = lines[:begin_idx] + lines[end_idx + 1:]
|
||||||
|
|
||||||
# Remove one excessive blank line left at the join point.
|
# activate-srs.sh appends exactly one separator line before the
|
||||||
while (
|
# managed block. Remove only that separator (also when the block was at
|
||||||
begin_idx > 0
|
# EOF), preserving every blank line that existed before activation.
|
||||||
and begin_idx < len(new_lines)
|
if begin_idx > 0 and lines[begin_idx - 1] == "\n":
|
||||||
and new_lines[begin_idx - 1].strip() == ""
|
del new_lines[begin_idx - 1]
|
||||||
and new_lines[begin_idx].strip() == ""
|
|
||||||
):
|
|
||||||
del new_lines[begin_idx]
|
|
||||||
|
|
||||||
st = os.stat(path)
|
st = os.stat(path)
|
||||||
directory = os.path.dirname(path)
|
directory = os.path.dirname(path)
|
||||||
@@ -729,45 +796,86 @@ restore_systemd_dropins() {
|
|||||||
|
|
||||||
snapshot_service_states() {
|
snapshot_service_states() {
|
||||||
local service
|
local service
|
||||||
|
local -A snapshot_active=() snapshot_enabled=()
|
||||||
|
|
||||||
for service in \
|
for service in \
|
||||||
"$POSTSRSD_SERVICE" \
|
"$POSTSRSD_SERVICE" \
|
||||||
"$SRS_REVERSE_MAP_SERVICE" \
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
"$SRS_DELIVERY_SERVICE"
|
"$SRS_DELIVERY_SERVICE"
|
||||||
do
|
do
|
||||||
if service_active "$service" ; then
|
if ! read_srs_service_state "$service" ; then
|
||||||
SERVICE_WAS_ACTIVE["$service"]="true"
|
warn "Cannot determine a supported systemd state for '${service}'."
|
||||||
else
|
return 1
|
||||||
SERVICE_WAS_ACTIVE["$service"]="false"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SERVICE_WAS_ENABLED["$service"]="$(service_enabled_state "$service")"
|
# The caller has classified this installation as ENABLED. Reject
|
||||||
|
# a state transition since that check rather than capturing an
|
||||||
|
# unexpectedly stopped/disabled service as the rollback baseline.
|
||||||
|
if [[ "$SRS_SERVICE_ACTIVE_STATE" != "active" ||
|
||||||
|
"$SRS_SERVICE_ENABLED_STATE" == "disabled" ]]; then
|
||||||
|
warn "SRS service '${service}' changed state before deactivation."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
snapshot_active["$service"]="true"
|
||||||
|
snapshot_enabled["$service"]="$SRS_SERVICE_ENABLED_STATE"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Only publish a complete, validated snapshot.
|
||||||
|
for service in \
|
||||||
|
"$POSTSRSD_SERVICE" \
|
||||||
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
|
"$SRS_DELIVERY_SERVICE"
|
||||||
|
do
|
||||||
|
SERVICE_WAS_ACTIVE["$service"]="${snapshot_active[$service]}"
|
||||||
|
SERVICE_WAS_ENABLED["$service"]="${snapshot_enabled[$service]}"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
restore_service_states() {
|
restore_service_states() {
|
||||||
local service
|
local service
|
||||||
local restore_failed=false
|
local restore_failed=false
|
||||||
local enabled_state
|
local enabled_state active_state
|
||||||
|
|
||||||
|
# Validate the entire snapshot before changing *any* unit.
|
||||||
for service in \
|
for service in \
|
||||||
"$POSTSRSD_SERVICE" \
|
"$POSTSRSD_SERVICE" \
|
||||||
"$SRS_REVERSE_MAP_SERVICE" \
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
"$SRS_DELIVERY_SERVICE"
|
"$SRS_DELIVERY_SERVICE"
|
||||||
do
|
do
|
||||||
enabled_state="${SERVICE_WAS_ENABLED[$service]:-}"
|
enabled_state="${SERVICE_WAS_ENABLED[$service]:-}"
|
||||||
|
active_state="${SERVICE_WAS_ACTIVE[$service]:-}"
|
||||||
|
case "$enabled_state" in
|
||||||
|
enabled|enabled-runtime|disabled) ;;
|
||||||
|
*) warn "Invalid saved enable state for '${service}': '${enabled_state}'."; return 1 ;;
|
||||||
|
esac
|
||||||
|
case "$active_state" in
|
||||||
|
true|false) ;;
|
||||||
|
*) warn "Invalid saved active state for '${service}': '${active_state}'."; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
for service in \
|
||||||
|
"$POSTSRSD_SERVICE" \
|
||||||
|
"$SRS_REVERSE_MAP_SERVICE" \
|
||||||
|
"$SRS_DELIVERY_SERVICE"
|
||||||
|
do
|
||||||
|
enabled_state="${SERVICE_WAS_ENABLED[$service]}"
|
||||||
|
|
||||||
case "$enabled_state" in
|
case "$enabled_state" in
|
||||||
enabled|enabled-runtime)
|
enabled)
|
||||||
systemctl enable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
systemctl enable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
;;
|
;;
|
||||||
|
enabled-runtime)
|
||||||
|
systemctl enable --runtime "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
|
;;
|
||||||
disabled)
|
disabled)
|
||||||
systemctl disable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
systemctl disable "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "${SERVICE_WAS_ACTIVE[$service]:-false}" == "true" ]]; then
|
if [[ "${SERVICE_WAS_ACTIVE[$service]}" == "true" ]]; then
|
||||||
systemctl start "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
systemctl start "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
else
|
else
|
||||||
systemctl stop "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
systemctl stop "$service" > /dev/null 2>> "$log_file" || restore_failed=true
|
||||||
@@ -789,6 +897,130 @@ srs_delivery_queue_is_empty() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ----------
|
||||||
|
# Deactivation phase 1: wait for old routing work to finish.
|
||||||
|
# The old qmgr process(es) must exit after the main Postfix reload.
|
||||||
|
# Only then can we assume that no further delivery requests using the
|
||||||
|
# former default_transport=srs-forward will be scheduled by that qmgr.
|
||||||
|
# Also wait for all remaining srs-forward pipe workers, including idle
|
||||||
|
# ones, before removing the service from master.cf.
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
capture_srs_old_queue_managers() {
|
||||||
|
local master_pid pid ppid comm process_list
|
||||||
|
|
||||||
|
process_list="$(ps -eo pid=,ppid=,comm=)" || return 1
|
||||||
|
|
||||||
|
master_pid="$(systemctl show "$POSTFIX_SERVICE" -p MainPID --value 2>> "$log_file")" || return 1
|
||||||
|
[[ "$master_pid" =~ ^[1-9][0-9]*$ ]] || return 1
|
||||||
|
kill -0 "$master_pid" 2>> "$log_file" || return 1
|
||||||
|
|
||||||
|
SRS_OLD_QMGR_PIDS=()
|
||||||
|
while read -r pid ppid comm ; do
|
||||||
|
if [[ "$ppid" == "$master_pid" && "$comm" == 'qmgr' ]]; then
|
||||||
|
SRS_OLD_QMGR_PIDS+=("$pid")
|
||||||
|
fi
|
||||||
|
done <<< "$process_list"
|
||||||
|
|
||||||
|
# Fail closed if the expected running queue manager cannot be identified.
|
||||||
|
(( ${#SRS_OLD_QMGR_PIDS[@]} > 0 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
srs_old_queue_manager_is_running() {
|
||||||
|
local pid comm
|
||||||
|
|
||||||
|
for pid in "${SRS_OLD_QMGR_PIDS[@]}" ; do
|
||||||
|
[[ -d "/proc/$pid" ]] || continue
|
||||||
|
|
||||||
|
if ! comm="$(ps -p "$pid" -o comm= 2>/dev/null)" ; then
|
||||||
|
# The process may have exited between the two checks.
|
||||||
|
[[ -d "/proc/$pid" ]] && return 2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# An unexpected process identity must not count as success.
|
||||||
|
[[ "$comm" == 'qmgr' ]] || return 2
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
srs_forward_worker_is_running() {
|
||||||
|
local process_list
|
||||||
|
|
||||||
|
process_list="$(ps -eo comm=,args=)" || return 2
|
||||||
|
|
||||||
|
printf '%s\n' "$process_list" | awk '
|
||||||
|
$1 == "pipe" && /srs-forward/ { found=1 }
|
||||||
|
$1 ~ /^python([0-9.]*)?$/ && /\/usr\/local\/sbin\/srs-forward[.]py/ { found=1 }
|
||||||
|
END { exit(found ? 0 : 1) }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_srs_forward_drain() {
|
||||||
|
local deadline=$((SECONDS + 180))
|
||||||
|
local _workers_rc _queue_rc _qmgr_rc
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
srs_old_queue_manager_is_running
|
||||||
|
_qmgr_rc=$?
|
||||||
|
case "$_qmgr_rc" in
|
||||||
|
0) sleep 2; continue ;;
|
||||||
|
1) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
srs_forward_worker_is_running
|
||||||
|
_workers_rc=$?
|
||||||
|
case "$_workers_rc" in
|
||||||
|
0) sleep 2; continue ;;
|
||||||
|
1) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
srs_delivery_queue_is_empty
|
||||||
|
_queue_rc=$?
|
||||||
|
case "$_queue_rc" in
|
||||||
|
0) ;;
|
||||||
|
1) sleep 2; continue ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# A second sample helps avoid treating a transient empty queue
|
||||||
|
# as quiescence; it is not a substitute for waiting for old qmgr.
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
srs_old_queue_manager_is_running
|
||||||
|
_qmgr_rc=$?
|
||||||
|
case "$_qmgr_rc" in
|
||||||
|
0) continue ;;
|
||||||
|
1) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
srs_forward_worker_is_running
|
||||||
|
_workers_rc=$?
|
||||||
|
case "$_workers_rc" in
|
||||||
|
0) continue ;;
|
||||||
|
1) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
srs_delivery_queue_is_empty
|
||||||
|
_queue_rc=$?
|
||||||
|
case "$_queue_rc" in
|
||||||
|
0) return 0 ;;
|
||||||
|
1) ;;
|
||||||
|
*) return 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
rollback_deactivation() {
|
rollback_deactivation() {
|
||||||
local rollback_failed=false
|
local rollback_failed=false
|
||||||
|
|
||||||
@@ -848,6 +1080,11 @@ rollback_deactivation() {
|
|||||||
echo_failed
|
echo_failed
|
||||||
rollback_failed=true
|
rollback_failed=true
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echononl "Check primary Postfix service '${POSTFIX_SERVICE}' is active.."
|
||||||
|
echo_failed
|
||||||
|
warn "Primary Postfix is inactive; restored configuration could not be reloaded."
|
||||||
|
rollback_failed=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $rollback_failed ; then
|
if $rollback_failed ; then
|
||||||
@@ -1016,6 +1253,8 @@ blank_line
|
|||||||
services_exist=true
|
services_exist=true
|
||||||
services_active=true
|
services_active=true
|
||||||
services_enabled=true
|
services_enabled=true
|
||||||
|
services_inactive=true
|
||||||
|
services_disabled=true
|
||||||
|
|
||||||
for _service in \
|
for _service in \
|
||||||
"$POSTSRSD_SERVICE" \
|
"$POSTSRSD_SERVICE" \
|
||||||
@@ -1035,34 +1274,50 @@ do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echononl "Check systemd service '${_service}' is active.."
|
echononl "Check systemd service '${_service}' is active.."
|
||||||
if service_active "$_service" ; then
|
service_active "$_service"
|
||||||
echo_ok
|
_service_rc=$?
|
||||||
else
|
case "$_service_rc" in
|
||||||
echo_inactive
|
0) echo_ok; services_inactive=false ;;
|
||||||
services_active=false
|
1) echo_inactive; services_active=false ;;
|
||||||
fi
|
*) fatal "Cannot read valid active state for '${_service}'. No changes were made." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
if service_enabled "$_service" ; then
|
service_enabled "$_service"
|
||||||
|
_service_rc=$?
|
||||||
|
case "$_service_rc" in
|
||||||
|
0)
|
||||||
echononl "Check systemd service '${_service}' enable state (enabled).."
|
echononl "Check systemd service '${_service}' enable state (enabled).."
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
services_disabled=false
|
||||||
|
;;
|
||||||
|
1)
|
||||||
echononl "Check systemd service '${_service}' enable state (disabled).."
|
echononl "Check systemd service '${_service}' enable state (disabled).."
|
||||||
echo_ok
|
echo_ok
|
||||||
services_enabled=false
|
services_enabled=false
|
||||||
fi
|
;;
|
||||||
|
*) fatal "Cannot read valid enable state for '${_service}'. No changes were made." ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
listeners_active=true
|
listeners_active=true
|
||||||
|
listeners_inactive=true
|
||||||
|
|
||||||
while IFS='|' read -r _label _host _port ; do
|
while IFS='|' read -r _label _host _port ; do
|
||||||
if port_is_listening "$_host" "$_port" ; then
|
port_is_listening "$_host" "$_port"
|
||||||
|
_port_rc=$?
|
||||||
|
case "$_port_rc" in
|
||||||
|
0)
|
||||||
echononl "Check ${_label} state on ${_host}:${_port} (listening).."
|
echononl "Check ${_label} state on ${_host}:${_port} (listening).."
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
listeners_inactive=false
|
||||||
|
;;
|
||||||
|
1)
|
||||||
echononl "Check ${_label} state on ${_host}:${_port} (inactive).."
|
echononl "Check ${_label} state on ${_host}:${_port} (inactive).."
|
||||||
echo_ok
|
echo_ok
|
||||||
listeners_active=false
|
listeners_active=false
|
||||||
fi
|
;;
|
||||||
|
*) fatal "Cannot inspect listener ${_label} on ${_host}:${_port}. No changes were made." ;;
|
||||||
|
esac
|
||||||
done <<EOF
|
done <<EOF
|
||||||
PostSRSd socketmap|${POSTSRSD_SOCKET_HOST}|${POSTSRSD_SOCKET_PORT}
|
PostSRSd socketmap|${POSTSRSD_SOCKET_HOST}|${POSTSRSD_SOCKET_PORT}
|
||||||
SRS reverse socketmap|${SRS_REVERSE_SOCKET_HOST}|${SRS_REVERSE_SOCKET_PORT}
|
SRS reverse socketmap|${SRS_REVERSE_SOCKET_HOST}|${SRS_REVERSE_SOCKET_PORT}
|
||||||
@@ -1074,10 +1329,13 @@ if systemd_dropins_match_expected ; then
|
|||||||
echononl "Check systemd SRS dependency drop-ins state (present).."
|
echononl "Check systemd SRS dependency drop-ins state (present).."
|
||||||
echo_ok
|
echo_ok
|
||||||
systemd_dropins_active=true
|
systemd_dropins_active=true
|
||||||
else
|
elif [[ ! -e "$POSTFIX_SRS_DROPIN_FILE" && ! -L "$POSTFIX_SRS_DROPIN_FILE" &&
|
||||||
|
! -e "$SRS_DELIVERY_DROPIN_FILE" && ! -L "$SRS_DELIVERY_DROPIN_FILE" ]]; then
|
||||||
echononl "Check systemd SRS dependency drop-ins state (absent).."
|
echononl "Check systemd SRS dependency drop-ins state (absent).."
|
||||||
echo_ok
|
echo_ok
|
||||||
systemd_dropins_active=false
|
systemd_dropins_active=false
|
||||||
|
else
|
||||||
|
fatal "SRS dependency drop-ins are present but incomplete or invalid. No changes were made."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@@ -1183,13 +1441,13 @@ smtp_override_ok=false
|
|||||||
amavis_override_ok=false
|
amavis_override_ok=false
|
||||||
|
|
||||||
echononl "Validate 'srs-forward' pipe service state.."
|
echononl "Validate 'srs-forward' pipe service state.."
|
||||||
if master_srs_forward_present ; then
|
master_srs_forward_present
|
||||||
echo_ok
|
_master_rc=$?
|
||||||
master_srs_forward_ok=true
|
case "$_master_rc" in
|
||||||
else
|
0) echo_ok; master_srs_forward_ok=true ;;
|
||||||
# Absent is valid in the stored pre-SRS state.
|
1) echo_ok ;; # Absent is valid in the stored pre-SRS state.
|
||||||
echo_ok
|
*) fatal "Cannot inspect the srs-forward master.cf service. No changes were made." ;;
|
||||||
fi
|
esac
|
||||||
|
|
||||||
echononl "Validate port 25 receive_override_options state.."
|
echononl "Validate port 25 receive_override_options state.."
|
||||||
if [[ "$(master_service_option_value smtp receive_override_options)" == "$smtp_active_value" ]]; then
|
if [[ "$(master_service_option_value smtp receive_override_options)" == "$smtp_active_value" ]]; then
|
||||||
@@ -1352,9 +1610,9 @@ then
|
|||||||
runtime_enabled=true
|
runtime_enabled=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! $services_active &&
|
if $services_inactive &&
|
||||||
! $services_enabled &&
|
$services_disabled &&
|
||||||
! $listeners_active &&
|
$listeners_inactive &&
|
||||||
! $systemd_dropins_active
|
! $systemd_dropins_active
|
||||||
then
|
then
|
||||||
runtime_disabled=true
|
runtime_disabled=true
|
||||||
@@ -1503,17 +1761,37 @@ if ! ask_yes_no "einverstanden" "no" ; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ----------
|
||||||
|
# Require a running primary Postfix before making any changes
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
echononl "Check primary Postfix service '${POSTFIX_SERVICE}' is active.."
|
||||||
|
if systemctl is-active --quiet "$POSTFIX_SERVICE" ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal "Primary Postfix service '${POSTFIX_SERVICE}' is inactive. Deactivation was not attempted."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Prepare rollback
|
# Prepare rollback
|
||||||
# ----------
|
# ----------
|
||||||
|
|
||||||
snapshot_service_states
|
echononl "Snapshot previous SRS service states.."
|
||||||
|
if snapshot_service_states ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal "Could not snapshot all SRS systemd service states. Deactivation was not attempted."
|
||||||
|
fi
|
||||||
|
|
||||||
DEACTIVATION_BACKUP_DIR="/root/postfix-srs-deactivation-${backup_date}"
|
DEACTIVATION_BACKUP_DIR="/root/postfix-srs-deactivation-${backup_date}"
|
||||||
|
|
||||||
echononl "Create deactivation backup directory '${DEACTIVATION_BACKUP_DIR}'.."
|
echononl "Create deactivation backup directory.."
|
||||||
if mkdir -m 0700 "$DEACTIVATION_BACKUP_DIR" > "$log_file" 2>&1 ; then
|
if mkdir -m 0700 "$DEACTIVATION_BACKUP_DIR" > "$log_file" 2>&1 ; then
|
||||||
echo_ok
|
echo_ok
|
||||||
|
printf ' Directory: %s\n' "$DEACTIVATION_BACKUP_DIR"
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
fatal "$(cat "$log_file")"
|
fatal "$(cat "$log_file")"
|
||||||
@@ -1617,33 +1895,10 @@ if [[ "$srs_alias_status" == 'managed' ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Restore master.cf
|
# Restore smtpd receive override settings for normal Postfix routing.
|
||||||
|
# Keep srs-forward service until old queue-manager work is drained.
|
||||||
# ----------
|
# ----------
|
||||||
|
|
||||||
if [[ "$master_block_status" == "present" ]]; then
|
|
||||||
echononl "Remove managed SRS block from '${POSTFIX_MASTER_CF}'.."
|
|
||||||
|
|
||||||
if remove_managed_block \
|
|
||||||
"$POSTFIX_MASTER_CF" \
|
|
||||||
"$SRS_MASTER_CF_BEGIN_MARKER" \
|
|
||||||
"$SRS_MASTER_CF_END_MARKER" > "$log_file" 2>&1
|
|
||||||
then
|
|
||||||
echo_ok
|
|
||||||
else
|
|
||||||
echo_failed
|
|
||||||
fatal_with_deactivation_rollback "Could not remove managed SRS block from '${POSTFIX_MASTER_CF}'."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echononl "Remove legacy unmarked 'srs-forward' service.."
|
|
||||||
|
|
||||||
if remove_legacy_srs_forward_service > "$log_file" 2>&1 ; then
|
|
||||||
echo_ok
|
|
||||||
else
|
|
||||||
echo_failed
|
|
||||||
fatal_with_deactivation_rollback "Could not safely remove legacy unmarked 'srs-forward' service."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echononl "Restore pre-SRS port 25 receive_override_options.."
|
echononl "Restore pre-SRS port 25 receive_override_options.."
|
||||||
if update_master_service_option \
|
if update_master_service_option \
|
||||||
smtp \
|
smtp \
|
||||||
@@ -1682,18 +1937,12 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
|
||||||
# Remove systemd SRS dependencies
|
|
||||||
# ----------
|
|
||||||
|
|
||||||
echononl "Remove systemd SRS dependency drop-ins.."
|
|
||||||
if remove_srs_systemd_dropins > "$log_file" 2>&1 ; then
|
|
||||||
echo_ok
|
|
||||||
else
|
|
||||||
echo_failed
|
|
||||||
fatal_with_deactivation_rollback "Could not remove systemd SRS dependency drop-ins. $(cat "$log_file")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
# ----------
|
||||||
|
# Phase 1: leave srs-forward in master.cf until old routing work
|
||||||
|
# has finished. Removing it before the reload could terminate
|
||||||
|
# running pipe workers abruptly.
|
||||||
|
# ----------
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Validate and reload main Postfix
|
# Validate and reload main Postfix
|
||||||
@@ -1707,6 +1956,14 @@ else
|
|||||||
fatal_with_deactivation_rollback "Postfix configuration check failed. $(cat "$log_file")"
|
fatal_with_deactivation_rollback "Postfix configuration check failed. $(cat "$log_file")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echononl "Capture active Postfix queue manager before reload.."
|
||||||
|
if capture_srs_old_queue_managers ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback "Could not identify the old Postfix queue manager. Refusing unsafe deactivation."
|
||||||
|
fi
|
||||||
|
|
||||||
echononl "Reload main Postfix service '${POSTFIX_SERVICE}'.."
|
echononl "Reload main Postfix service '${POSTFIX_SERVICE}'.."
|
||||||
if systemctl reload "$POSTFIX_SERVICE" > "$log_file" 2>&1 ; then
|
if systemctl reload "$POSTFIX_SERVICE" > "$log_file" 2>&1 ; then
|
||||||
echo_ok
|
echo_ok
|
||||||
@@ -1716,6 +1973,91 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ----------
|
||||||
|
# Phase 1: drain all old routing work before removing the pipe service.
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
echononl "Wait for old SRS queue-manager and pipe work to drain (max. 180s).."
|
||||||
|
if wait_for_srs_forward_drain ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback \
|
||||||
|
"Old SRS work or SRS delivery queue has not drained safely; SRS remains enabled after rollback."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# ----------
|
||||||
|
# Phase 2: remove the now-unused pipe service and SRS dependencies.
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
# ----------
|
||||||
|
# Restore master.cf
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
if [[ "$master_block_status" == "present" ]]; then
|
||||||
|
echononl "Remove managed SRS block from '${POSTFIX_MASTER_CF}'.."
|
||||||
|
|
||||||
|
if remove_managed_block \
|
||||||
|
"$POSTFIX_MASTER_CF" \
|
||||||
|
"$SRS_MASTER_CF_BEGIN_MARKER" \
|
||||||
|
"$SRS_MASTER_CF_END_MARKER" > "$log_file" 2>&1
|
||||||
|
then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback "Could not remove managed SRS block from '${POSTFIX_MASTER_CF}'."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echononl "Remove legacy unmarked 'srs-forward' service.."
|
||||||
|
|
||||||
|
if remove_legacy_srs_forward_service > "$log_file" 2>&1 ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback "Could not safely remove legacy unmarked 'srs-forward' service."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ----------
|
||||||
|
# Remove systemd SRS dependencies
|
||||||
|
# ----------
|
||||||
|
|
||||||
|
echononl "Remove systemd SRS dependency drop-ins.."
|
||||||
|
if remove_srs_systemd_dropins > "$log_file" 2>&1 ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback "Could not remove systemd SRS dependency drop-ins. $(cat "$log_file")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echononl "Check final pre-SRS Postfix configuration.."
|
||||||
|
if postfix check > "$log_file" 2>&1 ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback "Final Postfix configuration check failed. $(cat "$log_file")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echononl "Reload main Postfix after removing SRS pipe service.."
|
||||||
|
if systemctl reload "$POSTFIX_SERVICE" > "$log_file" 2>&1 ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback "Final Postfix reload failed. $(cat "$log_file")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echononl "Verify SRS delivery queue remains empty before service shutdown.."
|
||||||
|
if srs_delivery_queue_is_empty ; then
|
||||||
|
echo_ok
|
||||||
|
else
|
||||||
|
echo_failed
|
||||||
|
fatal_with_deactivation_rollback \
|
||||||
|
"SRS delivery queue is nonempty or unreadable after final reload. Refusing shutdown."
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
# ----------
|
# ----------
|
||||||
# Stop and disable SRS services
|
# Stop and disable SRS services
|
||||||
# ----------
|
# ----------
|
||||||
@@ -1788,7 +2130,9 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echononl "Verify 'srs-forward' service is absent.."
|
echononl "Verify 'srs-forward' service is absent.."
|
||||||
if ! master_srs_forward_present ; then
|
master_srs_forward_present
|
||||||
|
_master_rc=$?
|
||||||
|
if [[ "$_master_rc" -eq 1 ]]; then
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
@@ -1842,7 +2186,8 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echononl "Verify systemd SRS dependency drop-ins are absent.."
|
echononl "Verify systemd SRS dependency drop-ins are absent.."
|
||||||
if [[ ! -e "$POSTFIX_SRS_DROPIN_FILE" && ! -e "$SRS_DELIVERY_DROPIN_FILE" ]]; then
|
if [[ ! -e "$POSTFIX_SRS_DROPIN_FILE" && ! -L "$POSTFIX_SRS_DROPIN_FILE" &&
|
||||||
|
! -e "$SRS_DELIVERY_DROPIN_FILE" && ! -L "$SRS_DELIVERY_DROPIN_FILE" ]]; then
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
@@ -1856,7 +2201,9 @@ for _service in \
|
|||||||
do
|
do
|
||||||
echononl "Verify systemd service '${_service}' is inactive.."
|
echononl "Verify systemd service '${_service}' is inactive.."
|
||||||
|
|
||||||
if ! service_active "$_service" ; then
|
service_active "$_service"
|
||||||
|
_service_rc=$?
|
||||||
|
if [[ "$_service_rc" -eq 1 ]]; then
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
@@ -1865,7 +2212,9 @@ do
|
|||||||
|
|
||||||
echononl "Verify systemd service '${_service}' is disabled.."
|
echononl "Verify systemd service '${_service}' is disabled.."
|
||||||
|
|
||||||
if ! service_enabled "$_service" ; then
|
service_enabled "$_service"
|
||||||
|
_service_rc=$?
|
||||||
|
if [[ "$_service_rc" -eq 1 ]]; then
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
@@ -1883,7 +2232,9 @@ do
|
|||||||
|
|
||||||
echononl "Verify ${_label} is inactive.."
|
echononl "Verify ${_label} is inactive.."
|
||||||
|
|
||||||
if ! port_is_listening "$_host" "$_port" ; then
|
port_is_listening "$_host" "$_port"
|
||||||
|
_port_rc=$?
|
||||||
|
if [[ "$_port_rc" -eq 1 ]]; then
|
||||||
echo_ok
|
echo_ok
|
||||||
else
|
else
|
||||||
echo_failed
|
echo_failed
|
||||||
@@ -554,6 +554,7 @@ load_srs_config() {
|
|||||||
echononl "Read SRS configuration '${SRS_CONF_FILE}'.."
|
echononl "Read SRS configuration '${SRS_CONF_FILE}'.."
|
||||||
|
|
||||||
unset _SRS_DOMAIN
|
unset _SRS_DOMAIN
|
||||||
|
unset _MX_HOST
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
source "$SRS_CONF_FILE"
|
source "$SRS_CONF_FILE"
|
||||||
@@ -573,6 +574,15 @@ load_srs_config() {
|
|||||||
fatal "Invalid SRS domain '${_SRS_DOMAIN}'."
|
fatal "Invalid SRS domain '${_SRS_DOMAIN}'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Legacy installations may lack _MX_HOST. It is used by the installer
|
||||||
|
# only; activate/deactivate must remain compatible with older srs.conf files.
|
||||||
|
if [[ -n "${_MX_HOST+x}" ]] &&
|
||||||
|
{ [[ ! "$_MX_HOST" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]] ||
|
||||||
|
(( ${#_MX_HOST} > 253 )); }; then
|
||||||
|
echo_failed
|
||||||
|
fatal "Invalid public MX hostname '${_MX_HOST}' in '${SRS_CONF_FILE}'."
|
||||||
|
fi
|
||||||
|
|
||||||
SRS_CONFIG_LOADED=true
|
SRS_CONFIG_LOADED=true
|
||||||
echo_ok
|
echo_ok
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,9 @@ declare -a CREATED_CONFIG_DIRS=()
|
|||||||
# during rollback: nonempty directories could contain mail or logs.
|
# during rollback: nonempty directories could contain mail or logs.
|
||||||
declare -a NEW_POSTFIX_RUNTIME_DIRS=()
|
declare -a NEW_POSTFIX_RUNTIME_DIRS=()
|
||||||
SELECTED_SRS_DOMAIN=''
|
SELECTED_SRS_DOMAIN=''
|
||||||
|
SELECTED_MX_HOST=''
|
||||||
CONFIG_DOMAIN_HINT=''
|
CONFIG_DOMAIN_HINT=''
|
||||||
|
CONFIG_MX_HINT=''
|
||||||
CMAKE_PACKAGE_REQUIRED=false
|
CMAKE_PACKAGE_REQUIRED=false
|
||||||
APT_CMAKE_SIMULATION=''
|
APT_CMAKE_SIMULATION=''
|
||||||
|
|
||||||
@@ -172,10 +174,10 @@ Usage: install-update-srs.sh --check
|
|||||||
install-update-srs.sh --install [--version X.Y.Z]
|
install-update-srs.sh --install [--version X.Y.Z]
|
||||||
install-update-srs.sh --help
|
install-update-srs.sh --help
|
||||||
|
|
||||||
--check Ask for SRS domain, then read-only inventory; no lock files,
|
--check Ask for public MX host and SRS domain, then read-only inventory; no lock files,
|
||||||
package installation,
|
package installation,
|
||||||
Postfix operations, service changes or filesystem writes.
|
Postfix operations, service changes or filesystem writes.
|
||||||
--install Ask for SRS domain first, run all read-only preflight checks,
|
--install Ask for public MX host and SRS domain, run read-only preflight checks,
|
||||||
then request approval before installing missing Debian CMake
|
then request approval before installing missing Debian CMake
|
||||||
build packages (only for version builds) and preparing installation.
|
build packages (only for version builds) and preparing installation.
|
||||||
Supports inactive integration updates even if PostSRSd is current;
|
Supports inactive integration updates even if PostSRSd is current;
|
||||||
@@ -210,50 +212,88 @@ done
|
|||||||
|
|
||||||
NEW_PREFIX="/usr/local/postsrsd-${POSTSRSD_VERSION}"
|
NEW_PREFIX="/usr/local/postsrsd-${POSTSRSD_VERSION}"
|
||||||
|
|
||||||
# A local configuration may provide a *display-only* prompt default.
|
# Existing config values are only used as display hints before the full
|
||||||
# Do not source shell code before checking its root ownership/permissions.
|
# root-owned, non-group-writable config is validated by load_srs_config().
|
||||||
read_config_domain_hint() {
|
# Never source local configuration merely to display a prompt default.
|
||||||
local line candidate
|
read_config_domain_hints() {
|
||||||
|
local line candidate name
|
||||||
CONFIG_DOMAIN_HINT=''
|
CONFIG_DOMAIN_HINT=''
|
||||||
|
CONFIG_MX_HINT=''
|
||||||
[[ -f "$SRS_CONF_FILE" && -r "$SRS_CONF_FILE" ]] || return 0
|
[[ -f "$SRS_CONF_FILE" && -r "$SRS_CONF_FILE" ]] || return 0
|
||||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
[[ "$line" =~ ^[[:space:]]*_SRS_DOMAIN[[:space:]]*= ]] || continue
|
if [[ "$line" =~ ^[[:space:]]*_SRS_DOMAIN[[:space:]]*= ]]; then
|
||||||
|
name='srs'
|
||||||
|
elif [[ "$line" =~ ^[[:space:]]*_MX_HOST[[:space:]]*= ]]; then
|
||||||
|
name='mx'
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
candidate="${line#*=}"
|
candidate="${line#*=}"
|
||||||
candidate="${candidate%%#*}"
|
candidate="${candidate%%#*}"
|
||||||
candidate="${candidate//[[:space:]]/}"
|
candidate="${candidate//[[:space:]]/}"
|
||||||
candidate="${candidate//\"/}"
|
candidate="${candidate//\"/}"
|
||||||
candidate="${candidate//\'/}"
|
candidate="${candidate//\'/}"
|
||||||
if [[ "$candidate" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]]; then
|
if [[ "$candidate" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]] &&
|
||||||
|
(( ${#candidate} <= 253 )); then
|
||||||
|
if [[ "$name" == 'mx' ]]; then
|
||||||
|
CONFIG_MX_HINT="${candidate,,}"
|
||||||
|
else
|
||||||
CONFIG_DOMAIN_HINT="${candidate,,}"
|
CONFIG_DOMAIN_HINT="${candidate,,}"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
done < "$SRS_CONF_FILE"
|
done < "$SRS_CONF_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
# First user interaction, before any installation/prerequisite checks.
|
# First user interaction, before any installation/prerequisite checks.
|
||||||
# The shared Git script never has a server-specific domain hardcoded.
|
# An existing configured SRS domain is never derived again from a new MX host.
|
||||||
prompt_srs_domain() {
|
prompt_srs_domain() {
|
||||||
local answer='' host_fqdn=''
|
local answer='' host_fqdn='' mx_default='' srs_default=''
|
||||||
read_config_domain_hint
|
read_config_domain_hints
|
||||||
|
|
||||||
# For a first installation, suggest srs.<hostname -f>.
|
|
||||||
# A configured SRS domain always takes precedence.
|
|
||||||
if [[ -z "$CONFIG_DOMAIN_HINT" ]]; then
|
|
||||||
host_fqdn="$(hostname -f 2>/dev/null || true)"
|
host_fqdn="$(hostname -f 2>/dev/null || true)"
|
||||||
host_fqdn="${host_fqdn,,}"
|
host_fqdn="${host_fqdn,,}"
|
||||||
if [[ "$host_fqdn" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?[.])+[a-z]{2,63}$ ]] &&
|
if [[ "$host_fqdn" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]] &&
|
||||||
(( ${#host_fqdn} <= 249 )); then
|
(( ${#host_fqdn} <= 253 )); then
|
||||||
CONFIG_DOMAIN_HINT="srs.${host_fqdn}"
|
mx_default="${CONFIG_MX_HINT:-$host_fqdn}"
|
||||||
fi
|
else
|
||||||
|
mx_default="$CONFIG_MX_HINT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo 'Selective SRS - domain selection'
|
echo 'Selective SRS - domain selection'
|
||||||
echo
|
echo
|
||||||
echo " Host..............................: $(hostname -f 2>/dev/null || hostname)"
|
echo " System hostname....................: ${host_fqdn:-unknown}"
|
||||||
echo ' Enter the envelope sender domain for SRS addresses.'
|
echo ' Enter the public mail exchanger and SRS envelope sender domain.'
|
||||||
echo
|
echo
|
||||||
while true; do
|
while true; do
|
||||||
if [[ -n "$CONFIG_DOMAIN_HINT" ]]; then
|
if [[ -n "$mx_default" ]]; then
|
||||||
printf ' SRS domain [%s]: ' "$CONFIG_DOMAIN_HINT" >&2
|
printf ' Public MX hostname [%s]: ' "$mx_default" >&2
|
||||||
|
else
|
||||||
|
printf ' Public MX hostname: ' >&2
|
||||||
|
fi
|
||||||
|
if ! IFS= read -r answer; then
|
||||||
|
echo 'Input ended before an MX hostname was selected.' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
[[ -n "$answer" ]] || answer="$mx_default"
|
||||||
|
answer="${answer,,}"
|
||||||
|
if [[ "$answer" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]] &&
|
||||||
|
(( ${#answer} <= 253 )); then
|
||||||
|
SELECTED_MX_HOST="$answer"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo ' [ invalid ] Enter a public FQDN, e.g. mail.example.org.' >&2
|
||||||
|
done
|
||||||
|
echo " Selected MX hostname..............: $SELECTED_MX_HOST"
|
||||||
|
echo
|
||||||
|
|
||||||
|
srs_default="$CONFIG_DOMAIN_HINT"
|
||||||
|
if [[ -z "$srs_default" ]] && (( ${#SELECTED_MX_HOST} <= 249 )); then
|
||||||
|
srs_default="srs.${SELECTED_MX_HOST}"
|
||||||
|
fi
|
||||||
|
while true; do
|
||||||
|
if [[ -n "$srs_default" ]]; then
|
||||||
|
printf ' SRS domain [%s]: ' "$srs_default" >&2
|
||||||
else
|
else
|
||||||
printf ' SRS domain: ' >&2
|
printf ' SRS domain: ' >&2
|
||||||
fi
|
fi
|
||||||
@@ -261,13 +301,14 @@ prompt_srs_domain() {
|
|||||||
echo 'Input ended before an SRS domain was selected.' >&2
|
echo 'Input ended before an SRS domain was selected.' >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
[[ -n "$answer" ]] || answer="$CONFIG_DOMAIN_HINT"
|
[[ -n "$answer" ]] || answer="$srs_default"
|
||||||
answer="${answer,,}"
|
answer="${answer,,}"
|
||||||
if [[ "$answer" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]]; then
|
if [[ "$answer" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]] &&
|
||||||
|
(( ${#answer} <= 253 )); then
|
||||||
SELECTED_SRS_DOMAIN="$answer"
|
SELECTED_SRS_DOMAIN="$answer"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
echo ' [ invalid ] Enter a full DNS domain, e.g. srs.example.org.' >&2
|
echo ' [ invalid ] Enter a full DNS domain, e.g. srs.mail.example.org.' >&2
|
||||||
done
|
done
|
||||||
echo " Selected SRS domain...............: $SELECTED_SRS_DOMAIN"
|
echo " Selected SRS domain...............: $SELECTED_SRS_DOMAIN"
|
||||||
echo
|
echo
|
||||||
@@ -299,7 +340,7 @@ ask_installer_confirmation() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ask for the domain before --check and --install inspect prerequisites.
|
# Ask for MX host and SRS domain before --check and --install inspect prerequisites.
|
||||||
prompt_srs_domain
|
prompt_srs_domain
|
||||||
|
|
||||||
check_templates() {
|
check_templates() {
|
||||||
@@ -341,14 +382,13 @@ verify_srs_domain_dns() {
|
|||||||
# from interrupting the formatted DNS report. Any unexpected diagnostic
|
# from interrupting the formatted DNS report. Any unexpected diagnostic
|
||||||
# remains blocking: only existing unused main.cf parameters are classified
|
# remains blocking: only existing unused main.cf parameters are classified
|
||||||
# as unrelated/nonblocking. Deduplicate warnings repeated by postconf.
|
# as unrelated/nonblocking. Deduplicate warnings repeated by postconf.
|
||||||
python3 - "$SRS_DNS_CHECKER" "$SELECTED_SRS_DOMAIN" <<'PYDNS'
|
python3 - "$SRS_DNS_CHECKER" "$SELECTED_SRS_DOMAIN" "$SELECTED_MX_HOST" <<'PYDNS'
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
checker, domain = sys.argv[1:]
|
checker, domain, mx_host = sys.argv[1:]
|
||||||
parameters = (
|
parameters = (
|
||||||
('myhostname', '--hostname'),
|
|
||||||
('smtp_bind_address', '--ipv4'),
|
('smtp_bind_address', '--ipv4'),
|
||||||
('smtp_bind_address6', '--ipv6'),
|
('smtp_bind_address6', '--ipv6'),
|
||||||
)
|
)
|
||||||
@@ -391,7 +431,7 @@ if blocking:
|
|||||||
print(' Public SRS DNS preflight...........: NOT RUN')
|
print(' Public SRS DNS preflight...........: NOT RUN')
|
||||||
result_code = 1
|
result_code = 1
|
||||||
else:
|
else:
|
||||||
args = ['python3', checker, '--domain', domain]
|
args = ['python3', checker, '--domain', domain, '--hostname', mx_host]
|
||||||
for _name, option in parameters:
|
for _name, option in parameters:
|
||||||
args.extend([option, values[option]])
|
args.extend([option, values[option]])
|
||||||
result = subprocess.run(args, capture_output=True, text=True, check=False)
|
result = subprocess.run(args, capture_output=True, text=True, check=False)
|
||||||
@@ -443,6 +483,7 @@ read_only_check() {
|
|||||||
echo 'Selective SRS installer - read-only inventory'
|
echo 'Selective SRS installer - read-only inventory'
|
||||||
echo " Host..............................: $(hostname -f)"
|
echo " Host..............................: $(hostname -f)"
|
||||||
echo " Requested PostSRSd version........: ${POSTSRSD_VERSION}"
|
echo " Requested PostSRSd version........: ${POSTSRSD_VERSION}"
|
||||||
|
echo " Selected MX hostname..............: ${SELECTED_MX_HOST}"
|
||||||
echo " Selected SRS domain...............: ${SELECTED_SRS_DOMAIN}"
|
echo " Selected SRS domain...............: ${SELECTED_SRS_DOMAIN}"
|
||||||
echo " Repository........................: ${working_dir}"
|
echo " Repository........................: ${working_dir}"
|
||||||
echo
|
echo
|
||||||
@@ -495,6 +536,11 @@ read_only_check() {
|
|||||||
elif [[ -n "$CONFIG_DOMAIN_HINT" && "$CONFIG_DOMAIN_HINT" != "$SELECTED_SRS_DOMAIN" ]]; then
|
elif [[ -n "$CONFIG_DOMAIN_HINT" && "$CONFIG_DOMAIN_HINT" != "$SELECTED_SRS_DOMAIN" ]]; then
|
||||||
printf ' [ conflict ] Existing config domain: %s\n' "$CONFIG_DOMAIN_HINT"
|
printf ' [ conflict ] Existing config domain: %s\n' "$CONFIG_DOMAIN_HINT"
|
||||||
fi
|
fi
|
||||||
|
if [[ -f "$SRS_CONF_FILE" && -z "$CONFIG_MX_HINT" ]]; then
|
||||||
|
printf ' [ legacy ] Existing config has no _MX_HOST; maintenance-update will add it.\n'
|
||||||
|
elif [[ -n "$CONFIG_MX_HINT" && "$CONFIG_MX_HINT" != "$SELECTED_MX_HOST" ]]; then
|
||||||
|
printf ' [ conflict ] Existing MX hostname: %s\n' "$CONFIG_MX_HINT"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE"; do
|
for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE"; do
|
||||||
systemctl cat "$unit" >/dev/null 2>&1 && printf ' [ present ] %s\n' "$unit" || printf ' [ absent ] %s\n' "$unit"
|
systemctl cat "$unit" >/dev/null 2>&1 && printf ' [ present ] %s\n' "$unit" || printf ' [ absent ] %s\n' "$unit"
|
||||||
@@ -636,10 +682,14 @@ if [[ -f "$SRS_CONF_FILE" ]]; then
|
|||||||
_SRS_DOMAIN="${_SRS_DOMAIN,,}"
|
_SRS_DOMAIN="${_SRS_DOMAIN,,}"
|
||||||
[[ "$_SRS_DOMAIN" == "$SELECTED_SRS_DOMAIN" ]] ||
|
[[ "$_SRS_DOMAIN" == "$SELECTED_SRS_DOMAIN" ]] ||
|
||||||
fatal "Chosen domain '$SELECTED_SRS_DOMAIN' conflicts with existing '$SRS_CONF_FILE' ($_SRS_DOMAIN). Domain migration is not automatic."
|
fatal "Chosen domain '$SELECTED_SRS_DOMAIN' conflicts with existing '$SRS_CONF_FILE' ($_SRS_DOMAIN). Domain migration is not automatic."
|
||||||
|
if [[ -n "${_MX_HOST:-}" && "${_MX_HOST,,}" != "$SELECTED_MX_HOST" ]]; then
|
||||||
|
fatal "Chosen MX host '$SELECTED_MX_HOST' conflicts with existing '$SRS_CONF_FILE' ($_MX_HOST). Update the configuration intentionally; migration is not automatic."
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
[[ ! -e "$SRS_CONF_FILE" && ! -L "$SRS_CONF_FILE" ]] ||
|
[[ ! -e "$SRS_CONF_FILE" && ! -L "$SRS_CONF_FILE" ]] ||
|
||||||
fatal "SRS configuration path '$SRS_CONF_FILE' is not a regular file."
|
fatal "SRS configuration path '$SRS_CONF_FILE' is not a regular file."
|
||||||
_SRS_DOMAIN="$SELECTED_SRS_DOMAIN"
|
_SRS_DOMAIN="$SELECTED_SRS_DOMAIN"
|
||||||
|
_MX_HOST="$SELECTED_MX_HOST"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fatal_rollback() {
|
fatal_rollback() {
|
||||||
@@ -981,6 +1031,7 @@ fi
|
|||||||
blank_line
|
blank_line
|
||||||
echo " Mode..............................: ${INSTALL_MODE}"
|
echo " Mode..............................: ${INSTALL_MODE}"
|
||||||
echo " Host..............................: $(hostname -f)"
|
echo " Host..............................: $(hostname -f)"
|
||||||
|
echo " Public MX hostname................: ${SELECTED_MX_HOST}"
|
||||||
echo " SRS domain........................: ${_SRS_DOMAIN}"
|
echo " SRS domain........................: ${_SRS_DOMAIN}"
|
||||||
echo " Current version path..............: ${PREVIOUS_LINK:-absent}"
|
echo " Current version path..............: ${PREVIOUS_LINK:-absent}"
|
||||||
echo " Target version path...............: ${NEW_PREFIX}"
|
echo " Target version path...............: ${NEW_PREFIX}"
|
||||||
@@ -1062,10 +1113,24 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
render_templates "${STAGE_DIR}/rendered" || fatal_rollback 'Cannot render SRS configuration templates.'
|
render_templates "${STAGE_DIR}/rendered" || fatal_rollback 'Cannot render SRS configuration templates.'
|
||||||
|
# Newly installed sites record both names. For older sites without _MX_HOST,
|
||||||
|
# stage an append-only compatibility migration, preserving all existing content.
|
||||||
|
# The installed file is touched only after the deployment confirmation, and
|
||||||
|
# snapshot_file provides a rollback of the original configuration.
|
||||||
if [[ ! -f "$SRS_CONF_FILE" ]]; then
|
if [[ ! -f "$SRS_CONF_FILE" ]]; then
|
||||||
printf '# Local SRS domain - created by install-update-srs.sh; do not commit.\n_SRS_DOMAIN="%s"\n' \
|
printf '# Local SRS configuration - created by install-update-srs.sh; do not commit.\n_MX_HOST="%s"\n_SRS_DOMAIN="%s"\n' \
|
||||||
"$SELECTED_SRS_DOMAIN" > "${STAGE_DIR}/rendered/srs.conf" ||
|
"$SELECTED_MX_HOST" "$SELECTED_SRS_DOMAIN" > "${STAGE_DIR}/rendered/srs.conf" ||
|
||||||
fatal_rollback 'Cannot prepare local conf/srs.conf.'
|
fatal_rollback 'Cannot prepare local conf/srs.conf.'
|
||||||
|
elif [[ -z "${_MX_HOST+x}" ]]; then
|
||||||
|
python3 - "$SRS_CONF_FILE" "${STAGE_DIR}/rendered/srs.conf" "$SELECTED_MX_HOST" <<'PY_MX_MIGRATE' || fatal_rollback 'Cannot stage legacy SRS configuration migration.'
|
||||||
|
import pathlib, re, sys
|
||||||
|
source, destination, mx_host = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]), sys.argv[3]
|
||||||
|
content = source.read_bytes()
|
||||||
|
if re.search(rb'(?m)^\s*_MX_HOST\s*=', content):
|
||||||
|
raise SystemExit('Legacy config contains unexpected _MX_HOST assignment')
|
||||||
|
separator = b'' if not content or content.endswith(b'\n') else b'\n'
|
||||||
|
destination.write_bytes(content + separator + b'\n# Public mail exchanger for SRS return messages\n_MX_HOST="' + mx_host.encode('ascii') + b'"\n')
|
||||||
|
PY_MX_MIGRATE
|
||||||
fi
|
fi
|
||||||
cp "$SRS_TEMPLATE_DIR/delivery-master.cf" "${STAGE_DIR}/rendered/delivery-master.cf" || fatal_rollback 'Cannot stage delivery master.cf.'
|
cp "$SRS_TEMPLATE_DIR/delivery-master.cf" "${STAGE_DIR}/rendered/delivery-master.cf" || fatal_rollback 'Cannot stage delivery master.cf.'
|
||||||
|
|
||||||
@@ -1091,6 +1156,9 @@ CHANGES_STARTED=true
|
|||||||
trap 'on_unexpected_exit "$?"' EXIT
|
trap 'on_unexpected_exit "$?"' EXIT
|
||||||
if [[ ! -f "$SRS_CONF_FILE" ]]; then
|
if [[ ! -f "$SRS_CONF_FILE" ]]; then
|
||||||
install_regular_file "${STAGE_DIR}/rendered/srs.conf" "$SRS_CONF_FILE" 0644
|
install_regular_file "${STAGE_DIR}/rendered/srs.conf" "$SRS_CONF_FILE" 0644
|
||||||
|
elif [[ -z "${_MX_HOST+x}" ]]; then
|
||||||
|
# Preserve the previous permissions for migrated legacy configs.
|
||||||
|
install_regular_file "${STAGE_DIR}/rendered/srs.conf" "$SRS_CONF_FILE" "$(stat -c '%a' "$SRS_CONF_FILE")"
|
||||||
fi
|
fi
|
||||||
# Keep the secret directory private, as on mx.warenform.de. An existing
|
# Keep the secret directory private, as on mx.warenform.de. An existing
|
||||||
# directory with weaker permissions must be reviewed manually, not silently
|
# directory with weaker permissions must be reviewed manually, not silently
|
||||||
@@ -1158,7 +1226,8 @@ CHANGES_STARTED=false
|
|||||||
trap - EXIT SIGHUP SIGINT SIGTERM
|
trap - EXIT SIGHUP SIGINT SIGTERM
|
||||||
|
|
||||||
blank_line
|
blank_line
|
||||||
info "SRS ${INSTALL_MODE} completed (PostSRSd ${POSTSRSD_VERSION}). Backup: ${BACKUP_DIR}"
|
info "SRS ${INSTALL_MODE} completed (PostSRSd ${POSTSRSD_VERSION})."
|
||||||
|
printf ' Backup: %s\n' "$BACKUP_DIR"
|
||||||
info 'Selective SRS is NOT activated by this installer.'
|
info 'Selective SRS is NOT activated by this installer.'
|
||||||
info "To activate after review, use: ${working_dir}/activate-srs.sh"
|
info "To activate after review, use: ${working_dir}/activate-srs.sh"
|
||||||
clean_up 0
|
clean_up 0
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Isolated status, preflight, snapshot and rollback tests for activate-srs.sh.
|
||||||
|
|
||||||
|
All systemctl and ss operations are mocked. No real services or Postfix
|
||||||
|
configuration are modified. Run from the repository root:
|
||||||
|
python3 tests/test_activation_status_mock.py
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SOURCE = Path(__file__).resolve().parents[1] / 'activate-srs.sh'
|
||||||
|
TEXT = SOURCE.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def function(name):
|
||||||
|
match = re.search(rf'^{re.escape(name)}\(\) \{{\n.*?^\}}', TEXT, re.M | re.S)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f'Missing function {name}')
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
|
||||||
|
FUNCTIONS = '\n'.join(function(n) for n in (
|
||||||
|
'master_srs_forward_present', 'port_is_listening', 'service_exists',
|
||||||
|
'read_srs_service_state', 'service_active', 'service_enabled_state',
|
||||||
|
'snapshot_service_states', 'restore_service_states', 'rollback_activation',
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def section(start, end):
|
||||||
|
return TEXT[TEXT.index(start):TEXT.index(end, TEXT.index(start))]
|
||||||
|
|
||||||
|
|
||||||
|
PREFLIGHT = section('services_exist=true\n', '# ----------\n# Determine main.cf state')
|
||||||
|
CLASSIFICATION = section('srs_integration_enabled=false\n', '# ----------\n# Summary')
|
||||||
|
|
||||||
|
|
||||||
|
class ActivationMockTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
tmp = tempfile.TemporaryDirectory(prefix='srs-activation-status-')
|
||||||
|
self.addCleanup(tmp.cleanup)
|
||||||
|
self.root = Path(tmp.name)
|
||||||
|
self.master = self.root / 'master.cf'
|
||||||
|
self.master.write_text('smtp inet n - y - - smtpd\n')
|
||||||
|
self.calls = self.root / 'calls'
|
||||||
|
self.log = self.root / 'log'
|
||||||
|
self.drop1 = self.root / 'postfix.srs.conf'
|
||||||
|
self.drop2 = self.root / 'delivery.srs.conf'
|
||||||
|
|
||||||
|
def run_shell(self, commands, *, extra='', include_funcs=True):
|
||||||
|
sh = f'''#!/bin/bash
|
||||||
|
set -u
|
||||||
|
POSTFIX_MASTER_CF={str(self.master)!r}
|
||||||
|
POSTFIX_SRS_DROPIN_FILE={str(self.drop1)!r}
|
||||||
|
SRS_DELIVERY_DROPIN_FILE={str(self.drop2)!r}
|
||||||
|
POSTSRSD_SERVICE=postsrsd-srs.service
|
||||||
|
SRS_REVERSE_MAP_SERVICE=srs-reverse-map.service
|
||||||
|
SRS_DELIVERY_SERVICE=postfix@srs-delivery.service
|
||||||
|
POSTFIX_SERVICE=postfix.service
|
||||||
|
POSTSRSD_SOCKET_HOST=127.0.0.1
|
||||||
|
POSTSRSD_SOCKET_PORT=10003
|
||||||
|
SRS_REVERSE_SOCKET_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_SOCKET_PORT=10004
|
||||||
|
SRS_REINJECT_HOST=127.0.0.1
|
||||||
|
SRS_REINJECT_PORT=10028
|
||||||
|
SRS_REVERSE_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_PORT=10029
|
||||||
|
CALLS={str(self.calls)!r}
|
||||||
|
log_file={str(self.log)!r}
|
||||||
|
ACTIVATION_CHANGES_STARTED=true
|
||||||
|
ACTIVATION_BACKUP_DIR={str(self.root)!r}
|
||||||
|
declare -A SERVICE_WAS_ACTIVE=() SERVICE_WAS_ENABLED=()
|
||||||
|
blank_line() {{ :; }}
|
||||||
|
warn() {{ :; }}
|
||||||
|
error() {{ :; }}
|
||||||
|
info() {{ :; }}
|
||||||
|
echononl() {{ :; }}
|
||||||
|
echo_ok() {{ :; }}
|
||||||
|
echo_failed() {{ :; }}
|
||||||
|
postfix() {{ return 0; }}
|
||||||
|
restore_systemd_dropins() {{ return 0; }}
|
||||||
|
systemctl() {{
|
||||||
|
printf 'systemctl %s\\n' "$*" >> "$CALLS"
|
||||||
|
case "$1" in
|
||||||
|
cat) return 0 ;;
|
||||||
|
show)
|
||||||
|
[[ "${{SHOW_FAIL_UNIT:-}}" == "$2" ]] && return 4
|
||||||
|
if [[ "${{BAD_STATE_UNIT:-}}" == "$2" ]]; then
|
||||||
|
printf 'LoadState=loaded\\nActiveState=failed\\nUnitFileState=enabled\\n'
|
||||||
|
elif [[ "${{ACTIVE_UNIT:-}}" == "$2" ]]; then
|
||||||
|
printf 'LoadState=loaded\\nActiveState=active\\nUnitFileState=enabled\\n'
|
||||||
|
elif [[ "${{ENABLED_UNIT:-}}" == "$2" ]]; then
|
||||||
|
printf 'LoadState=loaded\\nActiveState=inactive\\nUnitFileState=enabled\\n'
|
||||||
|
else
|
||||||
|
printf '%s\\n' "${{SHOW_DATA:-$'LoadState=loaded\\nActiveState=inactive\\nUnitFileState=disabled'}}"
|
||||||
|
fi ;;
|
||||||
|
is-active) [[ "${{MAIN_INACTIVE:-0}}" != 1 ]] ;;
|
||||||
|
enable|disable|stop|start|reload)
|
||||||
|
[[ "${{FAIL_ACTION:-}}" != "$1" ]] ;;
|
||||||
|
*) return 88 ;;
|
||||||
|
esac
|
||||||
|
}}
|
||||||
|
ss() {{
|
||||||
|
[[ "${{SS_FAIL:-0}}" == 1 ]] && return 7
|
||||||
|
printf '%s\\n' "${{SS_DATA:-}}"
|
||||||
|
}}
|
||||||
|
{FUNCTIONS if include_funcs else ''}
|
||||||
|
{extra}
|
||||||
|
{commands}
|
||||||
|
'''
|
||||||
|
return subprocess.run(['bash','-c',sh],capture_output=True,text=True,timeout=10)
|
||||||
|
|
||||||
|
def check(self, commands, extra=''):
|
||||||
|
result = self.run_shell(commands, extra=extra)
|
||||||
|
self.assertEqual(result.returncode, 0, f'{result.stdout}\n{result.stderr}')
|
||||||
|
return result
|
||||||
|
|
||||||
|
def test_valid_disabled_preflight(self):
|
||||||
|
self.check(PREFLIGHT + '''
|
||||||
|
[[ "$services_disabled" == true && "$listeners_absent" == true && "$dropins_absent" == true && "$services_status_known" == true && "$primary_postfix_active" == true ]]
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_active_srs_service_is_not_disabled(self):
|
||||||
|
self.check(PREFLIGHT + '[[ "$services_disabled" == false ]]',
|
||||||
|
extra='ACTIVE_UNIT=postsrsd-srs.service')
|
||||||
|
|
||||||
|
def test_enabled_but_inactive_srs_service_is_not_disabled(self):
|
||||||
|
self.check(PREFLIGHT + '[[ "$services_disabled" == false ]]',
|
||||||
|
extra='ENABLED_UNIT=postsrsd-srs.service')
|
||||||
|
|
||||||
|
def test_systemctl_query_error_marks_unknown(self):
|
||||||
|
self.check(PREFLIGHT + '[[ "$services_status_known" == false && "$services_disabled" == false ]]',
|
||||||
|
extra='SHOW_FAIL_UNIT=srs-reverse-map.service')
|
||||||
|
|
||||||
|
def test_transient_systemctl_state_marks_unknown(self):
|
||||||
|
self.check(PREFLIGHT + '[[ "$services_status_known" == false ]]',
|
||||||
|
extra='BAD_STATE_UNIT=postsrsd-srs.service')
|
||||||
|
|
||||||
|
def test_active_port_not_disabled(self):
|
||||||
|
self.check(PREFLIGHT + '[[ "$listeners_absent" == false ]]',
|
||||||
|
extra="SS_DATA='LISTEN 0 5 127.0.0.1:10003 0.0.0.0:*'")
|
||||||
|
|
||||||
|
def test_ss_failure_marks_unknown(self):
|
||||||
|
self.check(PREFLIGHT + '[[ "$listeners_status_known" == false && "$listeners_absent" == false ]]',
|
||||||
|
extra='SS_FAIL=1')
|
||||||
|
|
||||||
|
def test_main_postfix_inactive_is_not_ready(self):
|
||||||
|
self.check(PREFLIGHT + '[[ "$primary_postfix_active" == false ]]',
|
||||||
|
extra='MAIN_INACTIVE=1')
|
||||||
|
|
||||||
|
def test_stray_dropin_not_disabled(self):
|
||||||
|
self.drop1.write_text('[Unit]\n')
|
||||||
|
self.check(PREFLIGHT + '[[ "$dropins_absent" == false ]]')
|
||||||
|
|
||||||
|
def test_broken_symlink_dropin_not_disabled(self):
|
||||||
|
self.drop1.symlink_to(self.root/'not-existing')
|
||||||
|
self.check(PREFLIGHT + '[[ "$dropins_absent" == false ]]')
|
||||||
|
|
||||||
|
def test_master_cf_unreadable_is_query_error(self):
|
||||||
|
self.master.unlink()
|
||||||
|
self.check('master_srs_forward_present; [[ $? -eq 2 ]]')
|
||||||
|
|
||||||
|
def test_master_cf_absent_and_present(self):
|
||||||
|
self.check('master_srs_forward_present; [[ $? -eq 1 ]]')
|
||||||
|
self.master.write_text('srs-forward unix - n n - - pipe\n argv=/usr/local/sbin/srs-forward.py ${sender}\n')
|
||||||
|
self.check('master_srs_forward_present; [[ $? -eq 0 ]]')
|
||||||
|
|
||||||
|
def test_atomic_snapshot_disabled(self):
|
||||||
|
self.check('''snapshot_service_states || exit 1
|
||||||
|
for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE" "$SRS_DELIVERY_SERVICE"; do
|
||||||
|
[[ "${SERVICE_WAS_ACTIVE[$unit]}" == false ]] || exit 2
|
||||||
|
[[ "${SERVICE_WAS_ENABLED[$unit]}" == disabled ]] || exit 3
|
||||||
|
done''')
|
||||||
|
self.check('''SHOW_FAIL_UNIT=srs-reverse-map.service
|
||||||
|
snapshot_service_states; [[ $? -eq 1 ]] || exit 1
|
||||||
|
[[ "${#SERVICE_WAS_ACTIVE[@]}" -eq 0 ]] || exit 2''')
|
||||||
|
|
||||||
|
def test_snapshot_rejects_changed_state(self):
|
||||||
|
self.check('''ACTIVE_UNIT=postsrsd-srs.service
|
||||||
|
snapshot_service_states; [[ $? -eq 1 ]] || exit 1
|
||||||
|
[[ "${#SERVICE_WAS_ENABLED[@]}" -eq 0 ]] || exit 2''')
|
||||||
|
|
||||||
|
def test_restore_rejects_invalid_snapshot_without_actions(self):
|
||||||
|
self.check('''for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE" "$SRS_DELIVERY_SERVICE"; do
|
||||||
|
SERVICE_WAS_ACTIVE[$unit]=false
|
||||||
|
SERVICE_WAS_ENABLED[$unit]=disabled
|
||||||
|
done
|
||||||
|
SERVICE_WAS_ENABLED[$SRS_REVERSE_MAP_SERVICE]=masked
|
||||||
|
restore_service_states; [[ $? -eq 1 ]]''')
|
||||||
|
self.assertFalse(self.calls.exists() and any(
|
||||||
|
part in self.calls.read_text() for part in ('systemctl start ', 'systemctl stop ', 'systemctl enable ', 'systemctl disable ')
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_restore_respects_enabled_runtime(self):
|
||||||
|
self.check('''for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE" "$SRS_DELIVERY_SERVICE"; do
|
||||||
|
SERVICE_WAS_ACTIVE[$unit]=false
|
||||||
|
SERVICE_WAS_ENABLED[$unit]=disabled
|
||||||
|
done
|
||||||
|
SERVICE_WAS_ACTIVE[$POSTSRSD_SERVICE]=true
|
||||||
|
SERVICE_WAS_ENABLED[$POSTSRSD_SERVICE]=enabled-runtime
|
||||||
|
restore_service_states''')
|
||||||
|
self.assertIn('systemctl enable --runtime postsrsd-srs.service',self.calls.read_text())
|
||||||
|
|
||||||
|
def test_rollback_inactive_primary_postfix_is_failure(self):
|
||||||
|
(self.root/'main.cf').write_text('before\n')
|
||||||
|
(self.root/'master.cf').write_text('before\n')
|
||||||
|
self.check('''POSTFIX_MAIN_CF=${ACTIVATION_BACKUP_DIR}/working-main.cf
|
||||||
|
POSTFIX_MASTER_CF=${ACTIVATION_BACKUP_DIR}/working-master.cf
|
||||||
|
for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE" "$SRS_DELIVERY_SERVICE"; do
|
||||||
|
SERVICE_WAS_ACTIVE[$unit]=false
|
||||||
|
SERVICE_WAS_ENABLED[$unit]=disabled
|
||||||
|
done
|
||||||
|
MAIN_INACTIVE=1
|
||||||
|
rollback_activation; [[ $? -eq 1 ]]''')
|
||||||
|
self.assertNotIn('systemctl reload postfix.service',self.calls.read_text())
|
||||||
|
|
||||||
|
def test_enabled_classification_still_works(self):
|
||||||
|
baseline = '''main_block_status=present
|
||||||
|
master_block_status=present
|
||||||
|
master_srs_forward_known=true
|
||||||
|
main_srs_values_ok=true
|
||||||
|
master_srs_forward_ok=true
|
||||||
|
smtp_override_ok=true
|
||||||
|
amavis_override_ok=true
|
||||||
|
pre_srs_main_ok=false
|
||||||
|
pre_srs_master_ok=false
|
||||||
|
installation_ok=true
|
||||||
|
services_exist=true
|
||||||
|
services_status_known=true
|
||||||
|
services_disabled=false
|
||||||
|
services_enabled=true
|
||||||
|
services_active=true
|
||||||
|
primary_postfix_active=true
|
||||||
|
listeners_status_known=true
|
||||||
|
listeners_absent=false
|
||||||
|
listeners_ok=true
|
||||||
|
dropins_absent=false
|
||||||
|
'''
|
||||||
|
self.check(CLASSIFICATION + '[[ "$srs_state" == enabled ]]', extra=baseline)
|
||||||
|
|
||||||
|
def test_disabled_classification_requires_every_prerequisite(self):
|
||||||
|
baseline = '''main_block_status=absent
|
||||||
|
master_block_status=absent
|
||||||
|
master_srs_forward_known=true
|
||||||
|
main_srs_values_ok=false
|
||||||
|
master_srs_forward_ok=false
|
||||||
|
smtp_override_ok=false
|
||||||
|
amavis_override_ok=false
|
||||||
|
pre_srs_main_ok=true
|
||||||
|
pre_srs_master_ok=true
|
||||||
|
installation_ok=true
|
||||||
|
services_exist=true
|
||||||
|
services_status_known=true
|
||||||
|
services_disabled=true
|
||||||
|
services_enabled=false
|
||||||
|
services_active=false
|
||||||
|
primary_postfix_active=true
|
||||||
|
listeners_status_known=true
|
||||||
|
listeners_absent=true
|
||||||
|
listeners_ok=false
|
||||||
|
dropins_absent=true
|
||||||
|
'''
|
||||||
|
self.check(CLASSIFICATION+'[[ "$srs_state" == disabled ]]',extra=baseline)
|
||||||
|
for name in ('services_status_known','services_disabled','primary_postfix_active',
|
||||||
|
'listeners_status_known','listeners_absent','dropins_absent',
|
||||||
|
'master_srs_forward_known'):
|
||||||
|
with self.subTest(name=name):
|
||||||
|
self.check(CLASSIFICATION+'[[ "$srs_state" == inconsistent ]]',
|
||||||
|
extra=baseline+f'{name}=false\n')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Isolated byte-exact managed-block removal tests; no Postfix services used.
|
||||||
|
|
||||||
|
Run from repository: python3 tests/test_deactivation_managed_block_mock.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SOURCE = Path(__file__).resolve().parents[1] / "deactivate-srs.sh"
|
||||||
|
NAME = "remove_managed_block"
|
||||||
|
BEGIN = "# BEGIN OOPEN-SRS-MANAGED"
|
||||||
|
END = "# END OOPEN-SRS-MANAGED"
|
||||||
|
BLOCK = "\n" + BEGIN + "\n# managed block\n" + END + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function():
|
||||||
|
source = SOURCE.read_text(encoding="utf-8")
|
||||||
|
m = re.search(rf"^{NAME}\(\) \{{\n.*?^\}}", source, re.M | re.S)
|
||||||
|
if not m:
|
||||||
|
raise RuntimeError(f"Missing {NAME}() in {SOURCE}")
|
||||||
|
return m.group(0)
|
||||||
|
|
||||||
|
|
||||||
|
class ManagedBlockMockTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.function = extract_function()
|
||||||
|
|
||||||
|
def run_remove(self, data, begin=BEGIN, end=END):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="srs-managed-block-") as temp:
|
||||||
|
path = Path(temp) / "main.cf"
|
||||||
|
path.write_bytes(data)
|
||||||
|
# Call the real Bash function, which executes its embedded Python.
|
||||||
|
script = (self.function + "\n" +
|
||||||
|
'remove_managed_block "$1" "$2" "$3"\n')
|
||||||
|
result = subprocess.run(
|
||||||
|
["bash", "-c", script, "bash", str(path), begin, end],
|
||||||
|
capture_output=True, text=True, timeout=10,
|
||||||
|
)
|
||||||
|
return result, path.read_bytes()
|
||||||
|
|
||||||
|
def test_exact_restoration_for_zero_to_five_existing_blank_lines(self):
|
||||||
|
for blanks in range(6):
|
||||||
|
with self.subTest(original_blank_lines=blanks):
|
||||||
|
original = b"myhostname = e.mx.oopen.de\n" + b"\n" * blanks
|
||||||
|
result, after = self.run_remove(original + BLOCK.encode())
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(after, original)
|
||||||
|
|
||||||
|
def test_empty_file_is_restored(self):
|
||||||
|
result, after = self.run_remove(BLOCK.encode())
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(after, b"")
|
||||||
|
|
||||||
|
def test_missing_begin_marker_does_not_modify_file(self):
|
||||||
|
original = b"myhostname = e.mx.oopen.de\n" + ("\n# wrong begin\n" + END + "\n").encode()
|
||||||
|
result, after = self.run_remove(original)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(after, original)
|
||||||
|
|
||||||
|
def test_duplicate_begin_marker_does_not_modify_file(self):
|
||||||
|
original = ("myhostname = e.mx.oopen.de\n" + BLOCK + BLOCK).encode()
|
||||||
|
result, after = self.run_remove(original)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(after, original)
|
||||||
|
|
||||||
|
def test_reversed_markers_does_not_modify_file(self):
|
||||||
|
original = (END + "\n" + BEGIN + "\n").encode()
|
||||||
|
result, after = self.run_remove(original)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(after, original)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Isolated tests of functions extracted from deactivate-srs.sh.
|
||||||
|
|
||||||
|
No mail services are started/stopped and no files outside TemporaryDirectory
|
||||||
|
are modified. Commands such as systemctl and postfix are shell mocks.
|
||||||
|
|
||||||
|
Run from the repository:
|
||||||
|
python3 tests/test_deactivation_rollback_mock.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SOURCE = Path(__file__).resolve().parents[1] / "deactivate-srs.sh"
|
||||||
|
FUNCTIONS = (
|
||||||
|
"rollback_deactivation",
|
||||||
|
"wait_for_srs_forward_drain",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_functions(source):
|
||||||
|
result = {}
|
||||||
|
for name in FUNCTIONS:
|
||||||
|
match = re.search(rf"^{re.escape(name)}\(\) \{{\n.*?^\}}", source, re.M | re.S)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Function {name}() not found in supplied deactivate-srs.sh")
|
||||||
|
result[name] = match.group(0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class IsolatedShellTest(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.functions = extract_functions(SOURCE.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory(prefix="srs-rollback-mock-")
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
self.base = Path(self.tmp.name)
|
||||||
|
self.backup = self.base / "backup"
|
||||||
|
self.backup.mkdir()
|
||||||
|
(self.backup / "main.cf").write_text("original main\n")
|
||||||
|
(self.backup / "master.cf").write_text("original master\n")
|
||||||
|
self.main = self.base / "main.cf"
|
||||||
|
self.master = self.base / "master.cf"
|
||||||
|
self.main.write_text("modified main\n")
|
||||||
|
self.master.write_text("modified master\n")
|
||||||
|
self.calls = self.base / "calls"
|
||||||
|
self.log = self.base / "log"
|
||||||
|
|
||||||
|
def run_bash(self, *, function, setup="", fail="", run_args=""):
|
||||||
|
self.assertIn(function, self.functions)
|
||||||
|
script = f'''#!/bin/bash
|
||||||
|
set -u
|
||||||
|
POSTFIX_MAIN_CF={str(self.main)!r}
|
||||||
|
POSTFIX_MASTER_CF={str(self.master)!r}
|
||||||
|
DEACTIVATION_BACKUP_DIR={str(self.backup)!r}
|
||||||
|
POSTFIX_SERVICE=postfix.service
|
||||||
|
log_file={str(self.log)!r}
|
||||||
|
CALLS={str(self.calls)!r}
|
||||||
|
DEACTIVATION_CHANGES_STARTED=true
|
||||||
|
blank_line() {{ :; }}
|
||||||
|
warn() {{ :; }}
|
||||||
|
error() {{ :; }}
|
||||||
|
info() {{ :; }}
|
||||||
|
echononl() {{ :; }}
|
||||||
|
echo_ok() {{ :; }}
|
||||||
|
echo_failed() {{ :; }}
|
||||||
|
cp() {{
|
||||||
|
printf 'cp %s\\n' "${{@: -1}}" >> "$CALLS"
|
||||||
|
if [[ "${{FAIL_CP_MAIN:-0}}" == 1 && "${{@: -1}}" == "$POSTFIX_MAIN_CF" ]]; then return 1; fi
|
||||||
|
command cp "$@"
|
||||||
|
}}
|
||||||
|
restore_systemd_dropins() {{
|
||||||
|
echo restore-dropins >> "$CALLS"
|
||||||
|
[[ "${{FAIL_DROPINS:-0}}" != 1 ]]
|
||||||
|
}}
|
||||||
|
restore_service_states() {{
|
||||||
|
echo restore-services >> "$CALLS"
|
||||||
|
[[ "${{FAIL_SERVICES:-0}}" != 1 ]]
|
||||||
|
}}
|
||||||
|
postfix() {{
|
||||||
|
printf 'postfix %s\\n' "$*" >> "$CALLS"
|
||||||
|
[[ "${{FAIL_POSTFIX_CHECK:-0}}" != 1 ]]
|
||||||
|
}}
|
||||||
|
systemctl() {{
|
||||||
|
printf 'systemctl %s\\n' "$*" >> "$CALLS"
|
||||||
|
case "$1" in
|
||||||
|
is-active) [[ "${{MAIN_INACTIVE:-0}}" != 1 ]] ;;
|
||||||
|
reload) [[ "${{FAIL_RELOAD:-0}}" != 1 ]] ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}}
|
||||||
|
srs_old_queue_manager_is_running() {{ return "${{QMGR_RC:-1}}"; }}
|
||||||
|
srs_forward_worker_is_running() {{ return "${{WORKER_RC:-1}}"; }}
|
||||||
|
srs_delivery_queue_is_empty() {{ return "${{QUEUE_RC:-0}}"; }}
|
||||||
|
sleep() {{ SECONDS=$((SECONDS + 181)); }}
|
||||||
|
{self.functions[function]}
|
||||||
|
{setup}
|
||||||
|
{fail}
|
||||||
|
{function} {run_args}
|
||||||
|
'''
|
||||||
|
return subprocess.run(["bash", "-c", script], capture_output=True, text=True,
|
||||||
|
timeout=10)
|
||||||
|
|
||||||
|
def test_rollback_restores_config_and_reloads(self):
|
||||||
|
result = self.run_bash(function="rollback_deactivation")
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(self.main.read_text(), "original main\n")
|
||||||
|
self.assertEqual(self.master.read_text(), "original master\n")
|
||||||
|
calls = self.calls.read_text()
|
||||||
|
for expected in ("restore-dropins", "restore-services", "postfix check",
|
||||||
|
"systemctl reload postfix.service"):
|
||||||
|
self.assertIn(expected, calls)
|
||||||
|
|
||||||
|
def test_rollback_cp_error_is_reported(self):
|
||||||
|
result = self.run_bash(function="rollback_deactivation", fail="FAIL_CP_MAIN=1")
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(self.main.read_text(), "modified main\n")
|
||||||
|
self.assertEqual(self.master.read_text(), "original master\n")
|
||||||
|
|
||||||
|
def test_rollback_dropin_error_is_reported(self):
|
||||||
|
self.assertNotEqual(self.run_bash(function="rollback_deactivation",
|
||||||
|
fail="FAIL_DROPINS=1").returncode, 0)
|
||||||
|
|
||||||
|
def test_rollback_service_restore_error_is_reported(self):
|
||||||
|
self.assertNotEqual(self.run_bash(function="rollback_deactivation",
|
||||||
|
fail="FAIL_SERVICES=1").returncode, 0)
|
||||||
|
|
||||||
|
def test_rollback_postfix_check_error_is_reported(self):
|
||||||
|
self.assertNotEqual(self.run_bash(function="rollback_deactivation",
|
||||||
|
fail="FAIL_POSTFIX_CHECK=1").returncode, 0)
|
||||||
|
|
||||||
|
def test_rollback_reload_error_is_reported(self):
|
||||||
|
self.assertNotEqual(self.run_bash(function="rollback_deactivation",
|
||||||
|
fail="FAIL_RELOAD=1").returncode, 0)
|
||||||
|
|
||||||
|
def test_rollback_inactive_main_postfix_is_not_silently_successful(self):
|
||||||
|
"""Safety regression: an inactive primary Postfix must not mean success."""
|
||||||
|
result = self.run_bash(function="rollback_deactivation", fail="MAIN_INACTIVE=1")
|
||||||
|
self.assertNotEqual(result.returncode, 0,
|
||||||
|
"BUG: inactive primary Postfix incorrectly reported rollback success")
|
||||||
|
self.assertNotIn("systemctl reload postfix.service", self.calls.read_text())
|
||||||
|
|
||||||
|
def test_drain_succeeds_when_qmgr_workers_and_queue_clear(self):
|
||||||
|
result = self.run_bash(function="wait_for_srs_forward_drain",
|
||||||
|
setup="sleep() { :; }")
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
|
||||||
|
def test_drain_times_out_on_running_old_qmgr(self):
|
||||||
|
self.assertEqual(self.run_bash(function="wait_for_srs_forward_drain",
|
||||||
|
fail="QMGR_RC=0").returncode, 1)
|
||||||
|
|
||||||
|
def test_drain_rejects_process_check_error(self):
|
||||||
|
self.assertEqual(self.run_bash(function="wait_for_srs_forward_drain",
|
||||||
|
fail="WORKER_RC=2").returncode, 2)
|
||||||
|
|
||||||
|
def test_drain_rejects_queue_read_error(self):
|
||||||
|
self.assertEqual(self.run_bash(function="wait_for_srs_forward_drain",
|
||||||
|
fail="QUEUE_RC=2").returncode, 2)
|
||||||
|
|
||||||
|
def test_drain_times_out_if_queue_remains_nonempty(self):
|
||||||
|
self.assertEqual(self.run_bash(function="wait_for_srs_forward_drain",
|
||||||
|
fail="QUEUE_RC=1").returncode, 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--source", type=Path, default=SOURCE,
|
||||||
|
help="Path to deactivate-srs.sh (defaults to sibling repo script)")
|
||||||
|
args, unittest_args = parser.parse_known_args()
|
||||||
|
SOURCE = args.source.resolve()
|
||||||
|
if not SOURCE.is_file():
|
||||||
|
parser.error(f"Source script not found: {SOURCE}")
|
||||||
|
unittest.main(argv=[sys.argv[0], *unittest_args], verbosity=2)
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Isolated fail-closed SRS service status / listener / verification tests.
|
||||||
|
|
||||||
|
Only extracts shell functions and read-only verification code from deactivate-srs.sh.
|
||||||
|
All calls to systemctl and ss use mocks; no system service is modified.
|
||||||
|
|
||||||
|
Run from repository root:
|
||||||
|
python3 tests/test_deactivation_status_mock.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SOURCE = Path(__file__).resolve().parents[1] / "deactivate-srs.sh"
|
||||||
|
TEXT = SOURCE.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
FUNCTIONS = (
|
||||||
|
"read_srs_service_state",
|
||||||
|
"service_active",
|
||||||
|
"service_enabled_state",
|
||||||
|
"service_enabled",
|
||||||
|
"snapshot_service_states",
|
||||||
|
"restore_service_states",
|
||||||
|
"port_is_listening",
|
||||||
|
"master_srs_forward_present",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_function(name):
|
||||||
|
match = re.search(rf"^{re.escape(name)}\(\) \{{\n.*?^\}}", TEXT, re.M | re.S)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Missing shell function {name}")
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
|
||||||
|
FUNCTION_TEXT = "\n".join(extract_function(name) for name in FUNCTIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def section(start, end):
|
||||||
|
begin = TEXT.index(start)
|
||||||
|
finish = TEXT.index(end, begin)
|
||||||
|
return TEXT[begin:finish]
|
||||||
|
|
||||||
|
|
||||||
|
PREFLIGHT = section('services_exist=true\n', '\n\n# ----------\n# Check Postfix integration')
|
||||||
|
FINAL = section('verification_ok=true\n', 'if ! $verification_ok ; then')
|
||||||
|
STATE_CLASSIFICATION = section('srs_integration_enabled=false\n', '\n\n# ----------\n# Summary')
|
||||||
|
|
||||||
|
|
||||||
|
class StatusMockTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory(prefix="srs-status-mock-")
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
self.base = Path(self.tmp.name)
|
||||||
|
self.master = self.base / 'master.cf'
|
||||||
|
self.master.write_text('smtp inet n - y - - smtpd\n')
|
||||||
|
self.calls = self.base / 'calls'
|
||||||
|
self.log = self.base / 'log'
|
||||||
|
|
||||||
|
def run_shell(self, body, config='', functions=True):
|
||||||
|
script = f'''#!/bin/bash
|
||||||
|
set -u
|
||||||
|
log_file={str(self.log)!r}
|
||||||
|
CALLS={str(self.calls)!r}
|
||||||
|
POSTFIX_MASTER_CF={str(self.master)!r}
|
||||||
|
POSTSRSD_SERVICE=postsrsd-srs.service
|
||||||
|
SRS_REVERSE_MAP_SERVICE=srs-reverse-map.service
|
||||||
|
SRS_DELIVERY_SERVICE=postfix@srs-delivery.service
|
||||||
|
declare -A SERVICE_WAS_ACTIVE=()
|
||||||
|
declare -A SERVICE_WAS_ENABLED=()
|
||||||
|
warn() {{ :; }}
|
||||||
|
echononl() {{ :; }}
|
||||||
|
echo_ok() {{ :; }}
|
||||||
|
echo_failed() {{ :; }}
|
||||||
|
echo_inactive() {{ :; }}
|
||||||
|
fatal() {{ echo "FATAL: $*" >&2; exit 70; }}
|
||||||
|
systemctl() {{
|
||||||
|
printf 'systemctl %s\\n' "$*" >> "$CALLS"
|
||||||
|
case "$1" in
|
||||||
|
show)
|
||||||
|
[[ "${{SHOW_FAIL_UNIT:-}}" == "$2" ]] && return 17
|
||||||
|
if [[ "${{SHOW_BAD_UNIT:-}}" == "$2" ]]; then
|
||||||
|
printf 'LoadState=not-found\\nActiveState=inactive\\nUnitFileState=disabled\\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%s\\n' "${{SHOW_DATA:-$'LoadState=loaded\\nActiveState=active\\nUnitFileState=enabled'}}"
|
||||||
|
;;
|
||||||
|
enable|disable|start|stop)
|
||||||
|
[[ "${{FAIL_ACTION:-}}" == "$1" ]] && return 11
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*) return 99 ;;
|
||||||
|
esac
|
||||||
|
}}
|
||||||
|
ss() {{
|
||||||
|
[[ "${{SS_FAIL:-0}}" == 1 ]] && return 7
|
||||||
|
printf '%s\\n' "${{SS_DATA:-}}"
|
||||||
|
}}
|
||||||
|
{FUNCTION_TEXT if functions else ''}
|
||||||
|
{config}
|
||||||
|
{body}
|
||||||
|
'''
|
||||||
|
return subprocess.run(['bash', '-c', script], capture_output=True,
|
||||||
|
text=True, timeout=15)
|
||||||
|
|
||||||
|
def assert_exit(self, body, rc=0, config='', functions=True):
|
||||||
|
p = self.run_shell(body, config=config, functions=functions)
|
||||||
|
self.assertEqual(p.returncode, rc, f"stdout={p.stdout!r}\nstderr={p.stderr!r}")
|
||||||
|
return p
|
||||||
|
|
||||||
|
def test_loaded_active_enabled_and_runtime_enabled(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
read_srs_service_state postsrsd-srs.service || exit 90
|
||||||
|
[[ "$SRS_SERVICE_ACTIVE_STATE" == active && "$SRS_SERVICE_ENABLED_STATE" == enabled ]] || exit 91
|
||||||
|
service_active postsrsd-srs.service || exit 92
|
||||||
|
service_enabled postsrsd-srs.service || exit 93
|
||||||
|
''')
|
||||||
|
self.assert_exit('''
|
||||||
|
SHOW_DATA=$'LoadState=loaded\\nActiveState=active\\nUnitFileState=enabled-runtime'
|
||||||
|
service_enabled postsrsd-srs.service || exit 94
|
||||||
|
[[ "$(service_enabled_state postsrsd-srs.service)" == enabled-runtime ]] || exit 95
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_inactive_disabled_is_valid_and_has_nonzero_status(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
SHOW_DATA=$'LoadState=loaded\\nActiveState=inactive\\nUnitFileState=disabled'
|
||||||
|
service_active postsrsd-srs.service; [[ $? -eq 1 ]] || exit 1
|
||||||
|
service_enabled postsrsd-srs.service; [[ $? -eq 1 ]] || exit 2
|
||||||
|
[[ "$(service_enabled_state postsrsd-srs.service)" == disabled ]] || exit 3
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_systemctl_error_and_not_found_return_two(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
SHOW_FAIL_UNIT=postsrsd-srs.service
|
||||||
|
service_active postsrsd-srs.service; [[ $? -eq 2 ]] || exit 1
|
||||||
|
service_enabled postsrsd-srs.service; [[ $? -eq 2 ]] || exit 2
|
||||||
|
service_enabled_state postsrsd-srs.service >/dev/null; [[ $? -eq 2 ]] || exit 3
|
||||||
|
unset SHOW_FAIL_UNIT
|
||||||
|
SHOW_BAD_UNIT=postsrsd-srs.service
|
||||||
|
read_srs_service_state postsrsd-srs.service; [[ $? -eq 2 ]] || exit 4
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_invalid_and_transient_states_are_rejected(self):
|
||||||
|
for state in [
|
||||||
|
'LoadState=loaded\nActiveState=failed\nUnitFileState=enabled',
|
||||||
|
'LoadState=loaded\nActiveState=activating\nUnitFileState=enabled',
|
||||||
|
'LoadState=loaded\nActiveState=inactive\nUnitFileState=masked',
|
||||||
|
'LoadState=loaded\nActiveState=inactive\nUnitFileState=static',
|
||||||
|
'LoadState=loaded\nActiveState=inactive',
|
||||||
|
'LoadState=loaded\nActiveState=inactive\nUnitFileState=disabled\nUnexpected=yes',
|
||||||
|
]:
|
||||||
|
with self.subTest(state=state):
|
||||||
|
# Quote shell strings safely by passing bash's single-quoted, escaped data.
|
||||||
|
import shlex
|
||||||
|
self.assert_exit('''
|
||||||
|
read_srs_service_state postsrsd-srs.service
|
||||||
|
[[ $? -eq 2 ]]
|
||||||
|
''', config=f'SHOW_DATA={shlex.quote(state)}\n')
|
||||||
|
|
||||||
|
def test_snapshot_complete_and_fail_atomic(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
snapshot_service_states || exit 1
|
||||||
|
for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE" "$SRS_DELIVERY_SERVICE"; do
|
||||||
|
[[ "${SERVICE_WAS_ACTIVE[$unit]}" == true ]] || exit 2
|
||||||
|
[[ "${SERVICE_WAS_ENABLED[$unit]}" == enabled ]] || exit 3
|
||||||
|
done
|
||||||
|
''')
|
||||||
|
self.assert_exit('''
|
||||||
|
SHOW_FAIL_UNIT=srs-reverse-map.service
|
||||||
|
snapshot_service_states; [[ $? -eq 1 ]] || exit 1
|
||||||
|
[[ "${#SERVICE_WAS_ACTIVE[@]}" -eq 0 ]] || exit 2
|
||||||
|
[[ "${#SERVICE_WAS_ENABLED[@]}" -eq 0 ]] || exit 3
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_snapshot_rejects_state_change_after_enabled_preflight(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
SHOW_DATA=$'LoadState=loaded\nActiveState=inactive\nUnitFileState=disabled'
|
||||||
|
snapshot_service_states; [[ $? -eq 1 ]] || exit 1
|
||||||
|
[[ "${#SERVICE_WAS_ACTIVE[@]}" -eq 0 ]] || exit 2
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_restore_exact_states(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
SERVICE_WAS_ENABLED[postsrsd-srs.service]=enabled
|
||||||
|
SERVICE_WAS_ACTIVE[postsrsd-srs.service]=true
|
||||||
|
SERVICE_WAS_ENABLED[srs-reverse-map.service]=enabled-runtime
|
||||||
|
SERVICE_WAS_ACTIVE[srs-reverse-map.service]=true
|
||||||
|
SERVICE_WAS_ENABLED[postfix@srs-delivery.service]=disabled
|
||||||
|
SERVICE_WAS_ACTIVE[postfix@srs-delivery.service]=false
|
||||||
|
restore_service_states || exit 1
|
||||||
|
''')
|
||||||
|
calls = self.calls.read_text()
|
||||||
|
for expected in (
|
||||||
|
'systemctl enable postsrsd-srs.service',
|
||||||
|
'systemctl enable --runtime srs-reverse-map.service',
|
||||||
|
'systemctl disable postfix@srs-delivery.service',
|
||||||
|
'systemctl start postsrsd-srs.service',
|
||||||
|
'systemctl stop postfix@srs-delivery.service',
|
||||||
|
):
|
||||||
|
self.assertIn(expected, calls)
|
||||||
|
|
||||||
|
def test_restore_rejects_invalid_snapshot_before_actions(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
SERVICE_WAS_ENABLED[postsrsd-srs.service]=enabled
|
||||||
|
SERVICE_WAS_ACTIVE[postsrsd-srs.service]=true
|
||||||
|
SERVICE_WAS_ENABLED[srs-reverse-map.service]=bad-value
|
||||||
|
SERVICE_WAS_ACTIVE[srs-reverse-map.service]=true
|
||||||
|
restore_service_states; [[ $? -eq 1 ]] || exit 1
|
||||||
|
[[ ! -e "$CALLS" ]] || exit 2
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_restore_propagates_action_failure(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
for unit in "$POSTSRSD_SERVICE" "$SRS_REVERSE_MAP_SERVICE" "$SRS_DELIVERY_SERVICE"; do
|
||||||
|
SERVICE_WAS_ENABLED["$unit"]=enabled
|
||||||
|
SERVICE_WAS_ACTIVE["$unit"]=true
|
||||||
|
done
|
||||||
|
FAIL_ACTION=start
|
||||||
|
restore_service_states; [[ $? -eq 1 ]]
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_ss_matching_nonmatching_and_command_failure(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
SS_DATA='LISTEN 0 128 127.0.0.1:10003 0.0.0.0:*'
|
||||||
|
port_is_listening 127.0.0.1 10003; [[ $? -eq 0 ]] || exit 1
|
||||||
|
port_is_listening 127.0.0.1 10004; [[ $? -eq 1 ]] || exit 2
|
||||||
|
SS_FAIL=1
|
||||||
|
port_is_listening 127.0.0.1 10003; [[ $? -eq 2 ]] || exit 3
|
||||||
|
''')
|
||||||
|
|
||||||
|
def test_master_service_present_absent_and_read_error(self):
|
||||||
|
self.assert_exit('''
|
||||||
|
master_srs_forward_present; [[ $? -eq 1 ]] || exit 1
|
||||||
|
printf 'srs-forward unix - n n - - pipe\\n argv=/usr/local/sbin/srs-forward.py\\n' > "$POSTFIX_MASTER_CF"
|
||||||
|
master_srs_forward_present; [[ $? -eq 0 ]] || exit 2
|
||||||
|
rm "$POSTFIX_MASTER_CF"
|
||||||
|
master_srs_forward_present 2>/dev/null; [[ $? -eq 2 ]] || exit 3
|
||||||
|
''')
|
||||||
|
|
||||||
|
def verification_script(self):
|
||||||
|
return FINAL + '\n[[ "$verification_ok" == "${EXPECTED}" ]]\n'
|
||||||
|
|
||||||
|
def verification_config(self):
|
||||||
|
return '''
|
||||||
|
check_managed_block_markers() { return 1; }
|
||||||
|
master_srs_forward_present() { return "${MASTER_RC:-1}"; }
|
||||||
|
state_main_cf_matches() { return 0; }
|
||||||
|
state_master_option_matches() { return 0; }
|
||||||
|
srs_alias_tool() { echo absent; }
|
||||||
|
service_active() { return "${ACTIVE_RC:-1}"; }
|
||||||
|
service_enabled() { return "${ENABLED_RC:-1}"; }
|
||||||
|
port_is_listening() { return "${PORT_RC:-1}"; }
|
||||||
|
postfix() { return 0; }
|
||||||
|
POSTFIX_MAIN_CF="${CALLS}.main"
|
||||||
|
POSTFIX_MASTER_CF="${CALLS}.master"
|
||||||
|
POSTFIX_SRS_DROPIN_FILE="${CALLS}.dropin1"
|
||||||
|
SRS_DELIVERY_DROPIN_FILE="${CALLS}.dropin2"
|
||||||
|
SRS_MAIN_CF_BEGIN_MARKER='begin'
|
||||||
|
SRS_MAIN_CF_END_MARKER='end'
|
||||||
|
SRS_MASTER_CF_BEGIN_MARKER='begin'
|
||||||
|
SRS_MASTER_CF_END_MARKER='end'
|
||||||
|
_PRE_SRS_DEFAULT_TRANSPORT_SET=false
|
||||||
|
_PRE_SRS_DEFAULT_TRANSPORT=''
|
||||||
|
_PRE_SRS_RECIPIENT_CANONICAL_MAPS_SET=false
|
||||||
|
_PRE_SRS_RECIPIENT_CANONICAL_MAPS=''
|
||||||
|
_PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT_SET=false
|
||||||
|
_PRE_SRS_SRS_FORWARD_DESTINATION_RECIPIENT_LIMIT=''
|
||||||
|
_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS_SET=false
|
||||||
|
_PRE_SRS_SMTP_RECEIVE_OVERRIDE_OPTIONS=''
|
||||||
|
_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS_SET=false
|
||||||
|
_PRE_SRS_AMAVIS_RECEIVE_OVERRIDE_OPTIONS=''
|
||||||
|
POSTSRSD_SOCKET_HOST=127.0.0.1
|
||||||
|
POSTSRSD_SOCKET_PORT=10003
|
||||||
|
SRS_REVERSE_SOCKET_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_SOCKET_PORT=10004
|
||||||
|
SRS_REINJECT_HOST=127.0.0.1
|
||||||
|
SRS_REINJECT_PORT=10028
|
||||||
|
SRS_REVERSE_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_PORT=10029
|
||||||
|
'''
|
||||||
|
|
||||||
|
def test_final_verification_accepts_only_known_absence(self):
|
||||||
|
for var in ('MASTER_RC', 'ACTIVE_RC', 'ENABLED_RC', 'PORT_RC'):
|
||||||
|
with self.subTest(var=var):
|
||||||
|
self.assert_exit(self.verification_script(),
|
||||||
|
config=self.verification_config() + f'EXPECTED=false\n{var}=2\n')
|
||||||
|
self.assert_exit(self.verification_script(),
|
||||||
|
config=self.verification_config() + 'EXPECTED=true\n')
|
||||||
|
|
||||||
|
def test_disabled_requires_every_service_and_listener_absent(self):
|
||||||
|
"""Mixed runtime states must never be classified as DISABLED."""
|
||||||
|
mock = '''
|
||||||
|
service_exists() { return 0; }
|
||||||
|
service_active() { [[ "$1" == "${ACTIVE_UNIT:-}" ]]; }
|
||||||
|
service_enabled() { [[ "$1" == "${ENABLED_UNIT:-}" ]]; }
|
||||||
|
port_is_listening() { [[ "$2" == "${LISTENER_PORT:-}" ]]; }
|
||||||
|
systemd_dropins_match_expected() { return 1; }
|
||||||
|
POSTFIX_SRS_DROPIN_FILE="$CALLS.dropin1"
|
||||||
|
SRS_DELIVERY_DROPIN_FILE="$CALLS.dropin2"
|
||||||
|
POSTSRSD_SOCKET_HOST=127.0.0.1
|
||||||
|
POSTSRSD_SOCKET_PORT=10003
|
||||||
|
SRS_REVERSE_SOCKET_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_SOCKET_PORT=10004
|
||||||
|
SRS_REINJECT_HOST=127.0.0.1
|
||||||
|
SRS_REINJECT_PORT=10028
|
||||||
|
SRS_REVERSE_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_PORT=10029
|
||||||
|
main_block_status=absent
|
||||||
|
master_block_status=absent
|
||||||
|
pre_srs_main_ok=true
|
||||||
|
pre_srs_master_ok=true
|
||||||
|
master_srs_forward_ok=false
|
||||||
|
main_srs_values_ok=false
|
||||||
|
smtp_override_ok=false
|
||||||
|
amavis_override_ok=false
|
||||||
|
'''
|
||||||
|
test_body = PREFLIGHT + STATE_CLASSIFICATION + '\nprintf "%s\\n" "$srs_state"\n'
|
||||||
|
for setting, expected in (
|
||||||
|
('', 'disabled'),
|
||||||
|
('ACTIVE_UNIT=postsrsd-srs.service\n', 'inconsistent'),
|
||||||
|
('ENABLED_UNIT=srs-reverse-map.service\n', 'inconsistent'),
|
||||||
|
('LISTENER_PORT=10003\n', 'inconsistent'),
|
||||||
|
):
|
||||||
|
with self.subTest(setting=setting or 'none'):
|
||||||
|
result = self.run_shell(test_body, config=mock + setting)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(result.stdout.strip().splitlines()[-1], expected)
|
||||||
|
|
||||||
|
def test_initial_preflight_aborts_on_query_errors(self):
|
||||||
|
config = '''
|
||||||
|
service_exists() { return 0; }
|
||||||
|
service_active() { return "${ACTIVE_RC:-1}"; }
|
||||||
|
service_enabled() { return "${ENABLED_RC:-1}"; }
|
||||||
|
port_is_listening() { return "${PORT_RC:-1}"; }
|
||||||
|
systemd_dropins_match_expected() { return 1; }
|
||||||
|
POSTFIX_SRS_DROPIN_FILE="$CALLS.none1"
|
||||||
|
SRS_DELIVERY_DROPIN_FILE="$CALLS.none2"
|
||||||
|
POSTSRSD_SOCKET_HOST=127.0.0.1
|
||||||
|
POSTSRSD_SOCKET_PORT=10003
|
||||||
|
SRS_REVERSE_SOCKET_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_SOCKET_PORT=10004
|
||||||
|
SRS_REINJECT_HOST=127.0.0.1
|
||||||
|
SRS_REINJECT_PORT=10028
|
||||||
|
SRS_REVERSE_HOST=127.0.0.1
|
||||||
|
SRS_REVERSE_PORT=10029
|
||||||
|
'''
|
||||||
|
for variable in ('ACTIVE_RC', 'ENABLED_RC', 'PORT_RC'):
|
||||||
|
with self.subTest(variable=variable):
|
||||||
|
self.assert_exit(PREFLIGHT, rc=70,
|
||||||
|
config=config + f'{variable}=2\n')
|
||||||
|
self.assert_exit(PREFLIGHT, config=config)
|
||||||
|
self.assert_exit(PREFLIGHT, rc=70, config=config + '''
|
||||||
|
POSTFIX_SRS_DROPIN_FILE="$CALLS.bad-dropin"
|
||||||
|
SRS_DELIVERY_DROPIN_FILE="$CALLS.other-dropin"
|
||||||
|
touch "$POSTFIX_SRS_DROPIN_FILE"
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Isolated, non-root tests for the public MX host installer addition.
|
||||||
|
|
||||||
|
No Postfix services, network DNS records or on-host config files are changed.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
INSTALLER = (ROOT / 'install-update-srs.sh').read_text()
|
||||||
|
COMMON = (ROOT / 'helpers/srs-common.sh').read_text()
|
||||||
|
SAMPLE = (ROOT / 'conf/srs.conf.sample').read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def slice_script(source, start, end):
|
||||||
|
return source[source.index(start):source.index(end)]
|
||||||
|
|
||||||
|
|
||||||
|
PROMPTS = slice_script(INSTALLER, 'read_config_domain_hints() {', '# Consistent installer-only confirmation prompts.')
|
||||||
|
DNS = slice_script(INSTALLER, 'verify_srs_domain_dns() {', '# Compact display of APT simulations;')
|
||||||
|
LOAD = slice_script(COMMON, 'load_srs_config() {', '# ----------\n# Persistent pre-SRS state')
|
||||||
|
MIGRATION = re.search(
|
||||||
|
r"<<'PY_MX_MIGRATE'[^\n]*\n(.*?)\nPY_MX_MIGRATE", INSTALLER, re.S
|
||||||
|
)
|
||||||
|
assert MIGRATION is not None
|
||||||
|
|
||||||
|
|
||||||
|
class InstallerMxHostTests(unittest.TestCase):
|
||||||
|
def run_prompt(self, config=None, hostname='o13-mail.oopen.de', choices='\n\n'):
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
conf = pathlib.Path(temp) / 'srs.conf'
|
||||||
|
if config is not None:
|
||||||
|
conf.write_text(config)
|
||||||
|
script = f'''SRS_CONF_FILE={str(conf)!r}
|
||||||
|
CONFIG_DOMAIN_HINT=''
|
||||||
|
CONFIG_MX_HINT=''
|
||||||
|
SELECTED_MX_HOST=''
|
||||||
|
SELECTED_SRS_DOMAIN=''
|
||||||
|
hostname() {{ printf '%s\\n' {hostname!r}; }}
|
||||||
|
{PROMPTS}
|
||||||
|
prompt_srs_domain
|
||||||
|
printf 'RESULT_MX=%s\\nRESULT_SRS=%s\\n' "$SELECTED_MX_HOST" "$SELECTED_SRS_DOMAIN"
|
||||||
|
'''
|
||||||
|
return subprocess.run(
|
||||||
|
['bash', '-c', script], input=choices, text=True, capture_output=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_first_install_defaults_from_hostname(self):
|
||||||
|
result = self.run_prompt()
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('RESULT_MX=o13-mail.oopen.de', result.stdout)
|
||||||
|
self.assertIn('RESULT_SRS=srs.o13-mail.oopen.de', result.stdout)
|
||||||
|
|
||||||
|
def test_public_mx_override_changes_srs_suggestion(self):
|
||||||
|
result = self.run_prompt(choices='mail.interventionistische-linke.org\n\n')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('RESULT_MX=mail.interventionistische-linke.org', result.stdout)
|
||||||
|
self.assertIn('RESULT_SRS=srs.mail.interventionistische-linke.org', result.stdout)
|
||||||
|
|
||||||
|
def test_configured_values_take_precedence(self):
|
||||||
|
config = '_MX_HOST="mail.example.org"\n_SRS_DOMAIN="srs.mail.example.org"\n'
|
||||||
|
result = self.run_prompt(config=config)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('RESULT_MX=mail.example.org', result.stdout)
|
||||||
|
self.assertIn('RESULT_SRS=srs.mail.example.org', result.stdout)
|
||||||
|
|
||||||
|
def test_legacy_srs_domain_preserved(self):
|
||||||
|
config = '_SRS_DOMAIN="srs.original.example.org"\n'
|
||||||
|
result = self.run_prompt(config=config, choices='mail.example.org\n\n')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('RESULT_MX=mail.example.org', result.stdout)
|
||||||
|
self.assertIn('RESULT_SRS=srs.original.example.org', result.stdout)
|
||||||
|
|
||||||
|
def test_invalid_host_reprompted(self):
|
||||||
|
result = self.run_prompt(choices='bad_host\nmail.example.org\n\n')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('[ invalid ]', result.stderr)
|
||||||
|
self.assertIn('RESULT_MX=mail.example.org', result.stdout)
|
||||||
|
|
||||||
|
def test_prompt_eof_fails_without_defaulting(self):
|
||||||
|
result = self.run_prompt(choices='')
|
||||||
|
self.assertEqual(result.returncode, 2)
|
||||||
|
|
||||||
|
def test_dns_preflight_passes_selected_mx_not_postfix_myhostname(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
binpath = pathlib.Path(temp)
|
||||||
|
(binpath / 'dig').write_text('#!/bin/sh\nexit 0\n')
|
||||||
|
(binpath / 'dig').chmod(0o755)
|
||||||
|
postconf = binpath / 'postconf'
|
||||||
|
postconf.write_text('''#!/bin/sh
|
||||||
|
case "$2" in
|
||||||
|
smtp_bind_address) echo 83.223.90.231 ;;
|
||||||
|
smtp_bind_address6) echo 2a01:31:0:2::ad52:a231 ;;
|
||||||
|
*) echo 'unexpected postconf query' >&2; exit 4 ;;
|
||||||
|
esac
|
||||||
|
''')
|
||||||
|
postconf.chmod(0o755)
|
||||||
|
checker = binpath / 'check.py'
|
||||||
|
checker.write_text('''import sys
|
||||||
|
print('ARGS=' + ';'.join(sys.argv[1:]))
|
||||||
|
''')
|
||||||
|
script = f'''SELECTED_MX_HOST=mail.interventionistische-linke.org
|
||||||
|
SELECTED_SRS_DOMAIN=srs.mail.interventionistische-linke.org
|
||||||
|
SRS_DNS_CHECKER={str(checker)!r}
|
||||||
|
{DNS}
|
||||||
|
verify_srs_domain_dns
|
||||||
|
'''
|
||||||
|
env = dict(os.environ, PATH=f'{temp}:' + os.environ['PATH'])
|
||||||
|
proc = subprocess.run(['bash','-c',script], capture_output=True, text=True, env=env)
|
||||||
|
self.assertEqual(proc.returncode, 0, proc.stderr + proc.stdout)
|
||||||
|
self.assertIn('--hostname;mail.interventionistische-linke.org', proc.stdout)
|
||||||
|
self.assertIn('--ipv4;83.223.90.231', proc.stdout)
|
||||||
|
self.assertIn('--ipv6;2a01:31:0:2::ad52:a231', proc.stdout)
|
||||||
|
|
||||||
|
def test_existing_config_migration_is_append_only(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src = pathlib.Path(tmp) / 'old.conf'
|
||||||
|
dst = pathlib.Path(tmp) / 'new.conf'
|
||||||
|
content = b'# existing comments\n_SRS_DOMAIN="srs.example.org"\n\n'
|
||||||
|
src.write_bytes(content)
|
||||||
|
proc = subprocess.run([sys.executable, '-', str(src), str(dst), 'mail.example.org'],
|
||||||
|
input=MIGRATION.group(1), capture_output=True, text=True)
|
||||||
|
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||||
|
self.assertTrue(dst.read_bytes().startswith(content))
|
||||||
|
self.assertIn(b'_MX_HOST="mail.example.org"\n', dst.read_bytes())
|
||||||
|
self.assertEqual(src.read_bytes(), content)
|
||||||
|
|
||||||
|
def test_migration_rejects_existing_mx_assignment(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
src = pathlib.Path(tmp) / 'old.conf'
|
||||||
|
dst = pathlib.Path(tmp) / 'new.conf'
|
||||||
|
src.write_text('_MX_HOST="other.example.org"\n')
|
||||||
|
proc = subprocess.run([sys.executable, '-', str(src), str(dst), 'mail.example.org'],
|
||||||
|
input=MIGRATION.group(1), capture_output=True, text=True)
|
||||||
|
self.assertNotEqual(proc.returncode, 0)
|
||||||
|
self.assertFalse(dst.exists())
|
||||||
|
|
||||||
|
def run_config_load(self, content):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
conf = pathlib.Path(tmp) / 'srs.conf'
|
||||||
|
conf.write_text(content)
|
||||||
|
script = f'''SRS_CONF_FILE={str(conf)!r}
|
||||||
|
SRS_CONF_SAMPLE_FILE=unused
|
||||||
|
check_secure_source_file() {{ :; }}
|
||||||
|
echononl() {{ :; }}
|
||||||
|
echo_failed() {{ :; }}
|
||||||
|
echo_ok() {{ :; }}
|
||||||
|
fatal() {{ echo "FATAL: $*" >&2; exit 1; }}
|
||||||
|
{LOAD}
|
||||||
|
load_srs_config
|
||||||
|
printf 'MX_IS_SET=%s\\n' "${{_MX_HOST+x}}"
|
||||||
|
'''
|
||||||
|
return subprocess.run(['bash','-c',script],text=True,capture_output=True)
|
||||||
|
|
||||||
|
def test_load_config_accepts_legacy_without_mx_host(self):
|
||||||
|
result = self.run_config_load('_SRS_DOMAIN="srs.example.org"\n')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('MX_IS_SET=', result.stdout)
|
||||||
|
|
||||||
|
def test_load_config_accepts_valid_mx_host(self):
|
||||||
|
result = self.run_config_load('_MX_HOST="mail.example.org"\n_SRS_DOMAIN="srs.example.org"\n')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('MX_IS_SET=x', result.stdout)
|
||||||
|
|
||||||
|
def test_load_config_rejects_invalid_mx_host(self):
|
||||||
|
result = self.run_config_load('_MX_HOST="bad_host"\n_SRS_DOMAIN="srs.example.org"\n')
|
||||||
|
self.assertEqual(result.returncode, 1)
|
||||||
|
self.assertIn('Invalid public MX hostname', result.stderr)
|
||||||
|
|
||||||
|
def test_templates_and_security_contract(self):
|
||||||
|
self.assertIn('_MX_HOST=""', SAMPLE)
|
||||||
|
self.assertIn('_SRS_DOMAIN=""', SAMPLE)
|
||||||
|
self.assertIn('unset _MX_HOST', COMMON)
|
||||||
|
self.assertIn('if [[ -n "${_MX_HOST+x}" ]]', COMMON)
|
||||||
|
self.assertIn('"${_MX_HOST,,}" != "$SELECTED_MX_HOST"', INSTALLER)
|
||||||
|
self.assertIn('install_regular_file "${STAGE_DIR}/rendered/srs.conf" "$SRS_CONF_FILE"', INSTALLER)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user