Make backup report hostname configurable
This commit is contained in:
1 parent
99da6460d2
commit
64eab1d3c2
7 files changed
+121
-7
No files matched your search
+20
-1
@@ -349,6 +349,25 @@ Absender `app-backup@<FQDN>` bei leerem/fehlendem `MAIL_FROM`, Betreffpräfix
|
|||||||
`/usr/sbin/sendmail`; `MAIL_SEND_TIMEOUT` begrenzt dessen Aufruf auf 30 Sekunden
|
`/usr/sbin/sendmail`; `MAIL_SEND_TIMEOUT` begrenzt dessen Aufruf auf 30 Sekunden
|
||||||
(zulässig 1–300). Der Log-Auszug darf 0–10000 Zeilen umfassen.
|
(zulässig 1–300). Der Log-Auszug darf 0–10000 Zeilen umfassen.
|
||||||
|
|
||||||
|
Seit 12.09.2026 überschreibt `MAIL_HOSTNAME` den Hostnamen im Betreff und im
|
||||||
|
`Host:`-Feld des Berichts. Für Gemini in `/etc/app-backup/app-backup.conf` setzen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MAIL_HOSTNAME="gemini.jr-it-services.de"
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Betreff folgt weiterhin dem Schema `[app-backup] SUCCESS/FAIL HOST LAUF`,
|
||||||
|
zum Beispiel `[app-backup] SUCCESS gemini.jr-it-services.de appbackup_2026-…`.
|
||||||
|
Ein leerer oder fehlender Wert fällt auf `socket.getfqdn()` zurück. Die Einstellung
|
||||||
|
gilt für Erfolgs- und Fehlermails und bleibt beim Paketupdate erhalten. Vor dem
|
||||||
|
Update gesetzt, wirkt sie bereits im Abnahmebackup des neuen Pakets. Spätere
|
||||||
|
Änderungen werden beim nächsten Lauf eingelesen; ein Dienstneustart entfällt.
|
||||||
|
Systemhostname, Archivnamen und Manifest-Hostname bleiben unverändert.
|
||||||
|
Auch der Absender bleibt unabhängig: Ein explizites `MAIL_FROM` wird beibehalten;
|
||||||
|
bei leerem Wert wird weiterhin `app-backup@<System-FQDN>` verwendet.
|
||||||
|
Header-Zeilenumbrüche im Override werden wie bei den anderen Mailheadern
|
||||||
|
abgewiesen; ein Berichtsfehler verändert den Backup-Exit-Code nicht.
|
||||||
|
|
||||||
Der UTF-8-Bericht enthält Zeit, Host, Status, Exit-Code, Dauer, Lauf-ID und letzte
|
Der UTF-8-Bericht enthält Zeit, Host, Status, Exit-Code, Dauer, Lauf-ID und letzte
|
||||||
Phase, Konfigurations-/Arbeits-/Archivpfade, Kompression, Crypt-Ziel und getrennten
|
Phase, Konfigurations-/Arbeits-/Archivpfade, Kompression, Crypt-Ziel und getrennten
|
||||||
Uploadstatus. Dieser wird erst nach erfolgreichem Rücklesen aller Inhalte und
|
Uploadstatus. Dieser wird erst nach erfolgreichem Rücklesen aller Inhalte und
|
||||||
@@ -536,7 +555,7 @@ Die Entfernung der drei DB-Dateien aus dem aktuellen Baum löscht deren Historie
|
|||||||
|
|
||||||
Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real.
|
Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real.
|
||||||
|
|
||||||
Verifikation am 10.09.2026: **65 Testfälle im vollständigen Durchlauf erfolgreich** (603,4 Sekunden), einschließlich ausführlicher Mailberichte und der aktualisierten Paketinstallation. Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion. Der MTA wird durch eine lokale Nachrichten-Attrappe ersetzt; es wurden keine echten E-Mails versendet. Die Zustellung auf Gemini ist nach Installation zu prüfen.
|
Verifikation am 12.09.2026: **68 Testfälle abgedeckt**. Im Gesamtlauf (741,0 Sekunden) bestanden 63 Fälle direkt. Fünf Mail-Testprüfungen erwarteten LF statt der korrekten SMTP-Zeilenumbrüche CRLF; nach Korrektur dieser Prüfungen bestanden alle 13 gezielt wiederholten Mail-/Installertests (155,9 Sekunden), einschließlich der fünf betroffenen Fälle. Geprüft sind Hostname-Override bei Erfolg und Fehler, leerer/fehlender Wert, unveränderter Absender, Header-Schutz und Paketupdate mit erhaltener Einstellung. Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion. Der MTA wird durch eine lokale Nachrichten-Attrappe ersetzt; es wurden keine echten E-Mails versendet. Die Zustellung auf Gemini ist nach Installation zu prüfen.
|
||||||
|
|
||||||
Die Regressionstests für `RCLONE_SIZE_ONLY` und zusätzliche Marker-Zeilenumbrüche scheiterten vor den jeweiligen Korrekturen mit einem fälschlich erfolgreichen Backup und bestanden danach. Upload-/Markerfehler werden an den Befehlsgrenzen injiziert, während Crypt-Übertragung und Inhaltsvergleich real bleiben. Die Tests bilden keine OneDrive-API nach; das tatsächliche HTTP-409-Wiederholungsverhalten ist durch den Betreiberlauf und den rclone-1.74.3-Quellcode separat belegt.
|
Die Regressionstests für `RCLONE_SIZE_ONLY` und zusätzliche Marker-Zeilenumbrüche scheiterten vor den jeweiligen Korrekturen mit einem fälschlich erfolgreichen Backup und bestanden danach. Upload-/Markerfehler werden an den Befehlsgrenzen injiziert, während Crypt-Übertragung und Inhaltsvergleich real bleiben. Die Tests bilden keine OneDrive-API nach; das tatsächliche HTTP-409-Wiederholungsverhalten ist durch den Betreiberlauf und den rclone-1.74.3-Quellcode separat belegt.
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ nicht nachträglich.
|
|||||||
Der Abschlussmarker wird außerdem bytegenau bestätigt, einschließlich seiner
|
Der Abschlussmarker wird außerdem bytegenau bestätigt, einschließlich seiner
|
||||||
Zeilenumbrüche, damit Backup und Restore dieselbe strikte Prüfung verwenden.
|
Zeilenumbrüche, damit Backup und Restore dieselbe strikte Prüfung verwenden.
|
||||||
|
|
||||||
|
Seit 12.09.2026 lässt sich der Hostname in Mail-Betreff und Bericht mit
|
||||||
|
`MAIL_HOSTNAME` festlegen, etwa `gemini.jr-it-services.de` statt
|
||||||
|
`localhost.localdomain`. Einstellung und Update-Schritte stehen unter
|
||||||
|
[Mailberichte](#mailberichte).
|
||||||
|
|
||||||
## Automatisches Update-Paket (empfohlen)
|
## Automatisches Update-Paket (empfohlen)
|
||||||
|
|
||||||
Das neue Paket übernimmt Konfigurationsabgleich, Rechteprüfung, Installation,
|
Das neue Paket übernimmt Konfigurationsabgleich, Rechteprüfung, Installation,
|
||||||
@@ -229,10 +234,35 @@ ENABLE_MAIL_REPORT=true
|
|||||||
MAIL_TO="johannes"
|
MAIL_TO="johannes"
|
||||||
MAIL_FROM="" # Standard: app-backup@<FQDN>
|
MAIL_FROM="" # Standard: app-backup@<FQDN>
|
||||||
MAIL_SUBJECT_PREFIX="[app-backup]"
|
MAIL_SUBJECT_PREFIX="[app-backup]"
|
||||||
|
MAIL_HOSTNAME="" # Leer: System-FQDN; überschreibt nur Betreff/Bericht
|
||||||
MAIL_INCLUDE_LOG_TAIL_LINES=200
|
MAIL_INCLUDE_LOG_TAIL_LINES=200
|
||||||
MAIL_SEND_TIMEOUT=30
|
MAIL_SEND_TIMEOUT=30
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Für Gemini vor dem Start des neuen Update-Pakets die aktive Konfiguration öffnen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudoedit /etc/app-backup/app-backup.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Zeile ergänzen beziehungsweise einen vorhandenen Wert ersetzen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MAIL_HOSTNAME="gemini.jr-it-services.de"
|
||||||
|
```
|
||||||
|
|
||||||
|
Anschließend das aktuelle `.run`-Paket wie oben beschrieben installieren. Es erhält
|
||||||
|
die Einstellung; bereits die Mail des Abnahmebackups verwendet dann beispielsweise
|
||||||
|
`[app-backup] SUCCESS gemini.jr-it-services.de appbackup_2026-…` und
|
||||||
|
`Host: gemini.jr-it-services.de` im Bericht. Auch Fehlermails verwenden den Wert.
|
||||||
|
Nach Installation dieser Version genügt für spätere Änderungen das Bearbeiten der
|
||||||
|
Konfiguration; sie wird bei jedem Lauf neu gelesen, ein Dienstneustart ist nicht nötig.
|
||||||
|
|
||||||
|
Ein leerer oder fehlender Wert verwendet weiterhin den automatisch über Python
|
||||||
|
`socket.getfqdn()` ermittelten Systemnamen. `MAIL_HOSTNAME` ändert weder den
|
||||||
|
Systemhostname noch Archivnamen oder Absender. Für den Absender gilt weiterhin
|
||||||
|
`MAIL_FROM` beziehungsweise bei leerem Wert `app-backup@<System-FQDN>`.
|
||||||
|
|
||||||
`MAIL_TO` darf wie bisher ein lokaler Alias oder eine Mailadresse sein. Versand
|
`MAIL_TO` darf wie bisher ein lokaler Alias oder eine Mailadresse sein. Versand
|
||||||
über `/usr/sbin/sendmail`; die lokale Mailzustellung muss dafür eingerichtet sein.
|
über `/usr/sbin/sendmail`; die lokale Mailzustellung muss dafür eingerichtet sein.
|
||||||
Der Bericht bleibt zusätzlich unter `/var/log/app-backup/report_LAUF.eml` mit
|
Der Bericht bleibt zusätzlich unter `/var/log/app-backup/report_LAUF.eml` mit
|
||||||
@@ -248,4 +278,4 @@ docker build -t app-backup-test -f tests/Dockerfile .
|
|||||||
docker run --rm --network none -v "$PWD:/src:ro" app-backup-test
|
docker run --rm --network none -v "$PWD:/src:ro" app-backup-test
|
||||||
```
|
```
|
||||||
|
|
||||||
ShellCheck, echte MariaDB-Dumps/Imports, gzip/zstd, rsync, lokale Crypt-Uploads und Remote-Restore sowie Fehlerfälle laufen im Container. systemd, Nextcloud-occ und der Mailversand sind simuliert; Nachrichten werden nur lokal erfasst. Am 10.09.2026 bestanden alle 65 Tests. Eine echte Rocky-/Nextcloud-/Gitea-/OneDrive-Wiederherstellungsprobe und die tatsächliche Mailzustellung bleiben Teil der Serverabnahme.
|
ShellCheck, echte MariaDB-Dumps/Imports, gzip/zstd, rsync, lokale Crypt-Uploads und Remote-Restore sowie Fehlerfälle laufen im Container. systemd, Nextcloud-occ und der Mailversand sind simuliert; Nachrichten werden nur lokal erfasst. Am 12.09.2026 wurden 68 Testfälle abgedeckt: 63 bestanden im Gesamtlauf; nach Korrektur von fünf Testprüfungen für SMTP-Zeilenumbrüche bestanden alle 13 gezielt wiederholten Mail-/Installertests, einschließlich dieser fünf Fälle. ShellCheck und Shell-Syntaxprüfung bestanden. Eine echte Rocky-/Nextcloud-/Gitea-/OneDrive-Wiederherstellungsprobe und die tatsächliche Mailzustellung bleiben Teil der Serverabnahme.
|
||||||
+25
-2
@@ -1,5 +1,8 @@
|
|||||||
# Automatisches Update-Paket
|
# Automatisches Update-Paket
|
||||||
|
|
||||||
|
Für Gemini zuerst `MAIL_HOSTNAME="gemini.jr-it-services.de"` in der aktiven
|
||||||
|
Konfiguration setzen: [Mailberichte nach dem Update](#mailberichte-nach-dem-update).
|
||||||
|
|
||||||
Auf dem Arbeitsplatz die gelieferte `.run`-Datei und ihre `.sha256`-Datei per SCP
|
Auf dem Arbeitsplatz die gelieferte `.run`-Datei und ihre `.sha256`-Datei per SCP
|
||||||
auf den Server übertragen. Auf dem Server im selben Verzeichnis prüfen/starten:
|
auf den Server übertragen. Auf dem Server im selben Verzeichnis prüfen/starten:
|
||||||
|
|
||||||
@@ -92,8 +95,28 @@ für Tests/Diagnose gedacht; dabei besteht keine Absicherung gegen SSH-Abbruch.
|
|||||||
|
|
||||||
### Mailberichte nach dem Update
|
### Mailberichte nach dem Update
|
||||||
|
|
||||||
|
Für den gewünschten Gemini-Hostnamen **vor dem Paketstart** die aktive
|
||||||
|
Konfiguration mit `sudoedit /etc/app-backup/app-backup.conf` öffnen und folgende
|
||||||
|
Zeile ergänzen beziehungsweise den vorhandenen Wert ersetzen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MAIL_HOSTNAME="gemini.jr-it-services.de"
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach Prüfsumme kontrollieren und das neue `.run`-Paket wie am Anfang der Anleitung
|
||||||
|
beschrieben starten. Der Installer erhält diesen Wert und die Mail des
|
||||||
|
Abnahmebackups zeigt bereits
|
||||||
|
`[app-backup] SUCCESS gemini.jr-it-services.de appbackup_2026-…` sowie
|
||||||
|
`Host: gemini.jr-it-services.de` im Nachrichtentext. Bei einem Fehler steht
|
||||||
|
entsprechend `FAIL` im Betreff. Ohne Einstellung oder bei `MAIL_HOSTNAME=""`
|
||||||
|
bleibt die automatische Ermittlung über `socket.getfqdn()` aktiv.
|
||||||
|
Der Absender wird weiterhin ausschließlich durch `MAIL_FROM` beziehungsweise
|
||||||
|
den bisherigen Standard `app-backup@<System-FQDN>` bestimmt. Systemhostname und
|
||||||
|
Archivnamen werden nicht geändert. Nach Installation des neuen Codes genügt
|
||||||
|
für spätere Anpassungen eine Konfigurationsänderung vor dem nächsten Lauf.
|
||||||
|
|
||||||
Das Paket stellt die ausführlichen Mailberichte wieder bereit. Vorhandene Werte
|
Das Paket stellt die ausführlichen Mailberichte wieder bereit. Vorhandene Werte
|
||||||
für `ENABLE_MAIL_REPORT`, `MAIL_TO`, `MAIL_FROM`, `MAIL_SUBJECT_PREFIX` und
|
für `ENABLE_MAIL_REPORT`, `MAIL_TO`, `MAIL_FROM`, `MAIL_HOSTNAME`, `MAIL_SUBJECT_PREFIX` und
|
||||||
`MAIL_INCLUDE_LOG_TAIL_LINES` bleiben erhalten; sie müssen nicht neu eingegeben
|
`MAIL_INCLUDE_LOG_TAIL_LINES` bleiben erhalten; sie müssen nicht neu eingegeben
|
||||||
werden. Eine bislang deaktivierte Berichtsfunktion bleibt deaktiviert. Fehlende
|
werden. Eine bislang deaktivierte Berichtsfunktion bleibt deaktiviert. Fehlende
|
||||||
Absender-/Betreff-/Tail-Einstellungen erhalten die in README beschriebenen Defaults.
|
Absender-/Betreff-/Tail-Einstellungen erhalten die in README beschriebenen Defaults.
|
||||||
@@ -452,7 +475,7 @@ Das ausführbare Paket ausschließlich über euren vertrauenswürdigen Kanal bez
|
|||||||
|
|
||||||
## Verifikation der Lieferung
|
## Verifikation der Lieferung
|
||||||
|
|
||||||
65 Testfälle im vollständigen Durchlauf erfolgreich (603,4 Sekunden), einschließlich Mailberichte, Versandfehler/-Timeout, Header-Schutz und Paketupdate mit erhaltenen Mail-Einstellungen. ShellCheck und Shell-Syntaxprüfung erfolgreich. Der MTA wird durch eine lokale Attrappe ersetzt; die Tests versenden keine echten E-Mails. Die Tests für eine über Umgebungsvariablen abgeschwächte Inhaltsprüfung und den nicht bytegenauen Marker-Vergleich reproduzierten die Fehler vor der Korrektur und bestanden danach.
|
Verifikation am 12.09.2026: Gesamtlauf mit 68 Testfällen (741,0 Sekunden), davon 63 direkt erfolgreich. Fünf Mail-Testprüfungen wurden für korrekte SMTP-Zeilenumbrüche (CRLF) angepasst; danach bestanden alle 13 gezielt wiederholten Mail-/Installertests (155,9 Sekunden), darunter sämtliche fünf betroffenen Fälle. Dies umfasst den konfigurierbaren Mail-Hostnamen, automatische FQDN-Ermittlung, erhaltenen Absender, Versandfehler/-Timeout, Header-Schutz und Paketupdate mit erhaltenen Mail-Einstellungen. ShellCheck und Shell-Syntaxprüfung erfolgreich. Der MTA wird durch eine lokale Attrappe ersetzt; die Tests versenden keine echten E-Mails. Die Tests für eine über Umgebungsvariablen abgeschwächte Inhaltsprüfung und den nicht bytegenauen Marker-Vergleich reproduzierten die Fehler vor der Korrektur und bestanden danach.
|
||||||
|
|
||||||
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
|
Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
|
||||||
systemd/occ und die systemd-run-Übergabe sind simuliert. Das Übergabemodell
|
systemd/occ und die systemd-run-Übergabe sind simuliert. Das Übergabemodell
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ MAIL_TO="johannes"
|
|||||||
# Empty means app-backup@<FQDN>. Existing sender/subject/tail settings are preserved.
|
# Empty means app-backup@<FQDN>. Existing sender/subject/tail settings are preserved.
|
||||||
MAIL_FROM=""
|
MAIL_FROM=""
|
||||||
MAIL_SUBJECT_PREFIX="[app-backup]"
|
MAIL_SUBJECT_PREFIX="[app-backup]"
|
||||||
|
# Host in subject/report only; empty uses the system FQDN. Sender is unchanged.
|
||||||
|
# Example for Gemini: MAIL_HOSTNAME="gemini.jr-it-services.de"
|
||||||
|
MAIL_HOSTNAME=""
|
||||||
MAIL_INCLUDE_LOG_TAIL_LINES=200
|
MAIL_INCLUDE_LOG_TAIL_LINES=200
|
||||||
MAIL_SENDMAIL_BIN="/usr/sbin/sendmail"
|
MAIL_SENDMAIL_BIN="/usr/sbin/sendmail"
|
||||||
MAIL_SEND_TIMEOUT=30
|
MAIL_SEND_TIMEOUT=30
|
||||||
|
|||||||
+2
-1
@@ -37,6 +37,7 @@ load_config() {
|
|||||||
: "${RCLONE_BWLIMIT:=0}" "${ENABLE_MAIL_REPORT:=true}" "${MAIL_TO:=johannes}"
|
: "${RCLONE_BWLIMIT:=0}" "${ENABLE_MAIL_REPORT:=true}" "${MAIL_TO:=johannes}"
|
||||||
# Keep the old report settings; an empty sender uses app-backup@<FQDN>.
|
# Keep the old report settings; an empty sender uses app-backup@<FQDN>.
|
||||||
: "${MAIL_FROM:=}" "${MAIL_SUBJECT_PREFIX:=[app-backup]}" "${MAIL_INCLUDE_LOG_TAIL_LINES:=200}"
|
: "${MAIL_FROM:=}" "${MAIL_SUBJECT_PREFIX:=[app-backup]}" "${MAIL_INCLUDE_LOG_TAIL_LINES:=200}"
|
||||||
|
: "${MAIL_HOSTNAME:=}"
|
||||||
: "${MAIL_SENDMAIL_BIN:=/usr/sbin/sendmail}" "${MAIL_SEND_TIMEOUT:=30}"
|
: "${MAIL_SENDMAIL_BIN:=/usr/sbin/sendmail}" "${MAIL_SEND_TIMEOUT:=30}"
|
||||||
: "${ENABLE_WORDPRESS:=false}" "${ENABLE_NEXTCLOUD:=false}" "${ENABLE_NEXTCLOUD_DATA:=true}"
|
: "${ENABLE_WORDPRESS:=false}" "${ENABLE_NEXTCLOUD:=false}" "${ENABLE_NEXTCLOUD_DATA:=true}"
|
||||||
: "${ENABLE_GITEA:=false}" "${ENABLE_MAIL:=false}" "${ENABLE_DB_DUMPS:=true}"
|
: "${ENABLE_GITEA:=false}" "${ENABLE_MAIL:=false}" "${ENABLE_DB_DUMPS:=true}"
|
||||||
@@ -513,7 +514,7 @@ finish() {
|
|||||||
if [[ ${CHECK:-false} != true && $ENABLE_MAIL_REPORT == true && $OPERATION == backup ]]; then
|
if [[ ${CHECK:-false} != true && $ENABLE_MAIL_REPORT == true && $OPERATION == backup ]]; then
|
||||||
python3 "$LIB_DIR/report.py" deliver --sizes "$report_sizes" --log "$LOG_FILE" \
|
python3 "$LIB_DIR/report.py" deliver --sizes "$report_sizes" --log "$LOG_FILE" \
|
||||||
--output "$LOG_DIR/report_$RUN_ID.eml" --to "$MAIL_TO" --sender "$MAIL_FROM" \
|
--output "$LOG_DIR/report_$RUN_ID.eml" --to "$MAIL_TO" --sender "$MAIL_FROM" \
|
||||||
--subject-prefix "$MAIL_SUBJECT_PREFIX" --run "$RUN_ID" --exit-code "$code" \
|
--hostname="$MAIL_HOSTNAME" --subject-prefix "$MAIL_SUBJECT_PREFIX" --run "$RUN_ID" --exit-code "$code" \
|
||||||
--started "$RUN_STARTED_EPOCH" --phase "$BACKUP_PHASE" --upload-status "$UPLOAD_STATUS" \
|
--started "$RUN_STARTED_EPOCH" --phase "$BACKUP_PHASE" --upload-status "$UPLOAD_STATUS" \
|
||||||
--config "$CONFIG_FILE" --workdir "$WORKDIR" --archive-dir "$ARCHIVE_DIR" \
|
--config "$CONFIG_FILE" --workdir "$WORKDIR" --archive-dir "$ARCHIVE_DIR" \
|
||||||
--compression "$COMPRESSOR" --remote "$RCLONE_REMOTE_BASE" --sendmail "$MAIL_SENDMAIL_BIN" \
|
--compression "$COMPRESSOR" --remote "$RCLONE_REMOTE_BASE" --sendmail "$MAIL_SENDMAIL_BIN" \
|
||||||
|
|||||||
+4
-2
@@ -50,7 +50,8 @@ def human(value):
|
|||||||
|
|
||||||
|
|
||||||
def deliver(args):
|
def deliver(args):
|
||||||
host = socket.getfqdn()
|
fqdn = socket.getfqdn()
|
||||||
|
host = args.hostname or fqdn
|
||||||
now = dt.datetime.now().astimezone()
|
now = dt.datetime.now().astimezone()
|
||||||
state = 'SUCCESS' if args.exit_code == 0 else 'FAIL'
|
state = 'SUCCESS' if args.exit_code == 0 else 'FAIL'
|
||||||
try:
|
try:
|
||||||
@@ -89,7 +90,7 @@ def deliver(args):
|
|||||||
body.extend(['', f'Log-Auszug (letzte {args.tail_lines} Zeilen)', '-----------------', tail])
|
body.extend(['', f'Log-Auszug (letzte {args.tail_lines} Zeilen)', '-----------------', tail])
|
||||||
message = EmailMessage(policy=SMTP)
|
message = EmailMessage(policy=SMTP)
|
||||||
# EmailMessage rejects CR/LF in headers and encodes non-ASCII safely.
|
# EmailMessage rejects CR/LF in headers and encodes non-ASCII safely.
|
||||||
sender = args.sender or f'app-backup@{host}'
|
sender = args.sender or f'app-backup@{fqdn}'
|
||||||
if not args.to.strip() or not sender.strip():
|
if not args.to.strip() or not sender.strip():
|
||||||
raise ValueError('Mail recipient and sender must not be empty')
|
raise ValueError('Mail recipient and sender must not be empty')
|
||||||
message['From'] = sender
|
message['From'] = sender
|
||||||
@@ -112,6 +113,7 @@ def main():
|
|||||||
for name in ('output', 'staging', 'archives'):
|
for name in ('output', 'staging', 'archives'):
|
||||||
snap.add_argument(name)
|
snap.add_argument(name)
|
||||||
mail = commands.add_parser('deliver')
|
mail = commands.add_parser('deliver')
|
||||||
|
mail.add_argument('--hostname', default='')
|
||||||
for name in ('sizes', 'log', 'output', 'to', 'sender', 'subject-prefix', 'run', 'phase',
|
for name in ('sizes', 'log', 'output', 'to', 'sender', 'subject-prefix', 'run', 'phase',
|
||||||
'upload-status', 'config', 'workdir', 'archive-dir', 'compression', 'remote', 'sendmail'):
|
'upload-status', 'config', 'workdir', 'archive-dir', 'compression', 'remote', 'sendmail'):
|
||||||
mail.add_argument('--'+name, required=True)
|
mail.add_argument('--'+name, required=True)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -584,6 +585,7 @@ if '--list-only' not in sys.argv and '--exclude=/*' not in sys.argv:
|
|||||||
|
|
||||||
def test_automatic_package_upgrade_and_combined_rollback(self):
|
def test_automatic_package_upgrade_and_combined_rollback(self):
|
||||||
self.enable_report()
|
self.enable_report()
|
||||||
|
with self.config.open('a') as f: f.write('MAIL_HOSTNAME="gemini.jr-it-services.de"\n')
|
||||||
canonical=self.configure_upgrade()
|
canonical=self.configure_upgrade()
|
||||||
original=canonical.read_bytes()
|
original=canonical.read_bytes()
|
||||||
package=self.build_package()
|
package=self.build_package()
|
||||||
@@ -593,6 +595,9 @@ if '--list-only' not in sys.argv and '--exclude=/*' not in sys.argv:
|
|||||||
self.assertIn('SUCCESS',(state/'result.txt').read_text())
|
self.assertIn('SUCCESS',(state/'result.txt').read_text())
|
||||||
message,body=self.read_report()
|
message,body=self.read_report()
|
||||||
self.assertEqual(str(message['From']),'app-backup@example.invalid')
|
self.assertEqual(str(message['From']),'app-backup@example.invalid')
|
||||||
|
self.assertIn(' SUCCESS gemini.jr-it-services.de ',str(message['Subject']))
|
||||||
|
self.assertIn('Host: gemini.jr-it-services.de',body.splitlines())
|
||||||
|
self.assertIn('MAIL_HOSTNAME="gemini.jr-it-services.de"',canonical.read_text())
|
||||||
self.assertIn('OK (Inhalt und Abschlussmarker',body)
|
self.assertIn('OK (Inhalt und Abschlussmarker',body)
|
||||||
self.assertEqual(Path('/usr/local/lib/app-backup/report.py').read_bytes(),(REPO/'lib/report.py').read_bytes())
|
self.assertEqual(Path('/usr/local/lib/app-backup/report.py').read_bytes(),(REPO/'lib/report.py').read_bytes())
|
||||||
self.assertTrue((state/'verified-backup-run').is_file())
|
self.assertTrue((state/'verified-backup-run').is_file())
|
||||||
@@ -987,6 +992,8 @@ REPORT_SECRET='config-content-must-not-enter-the-mail'
|
|||||||
run=self.backup()
|
run=self.backup()
|
||||||
message,body=self.read_report()
|
message,body=self.read_report()
|
||||||
self.assertIn('[Backup-Test] SUCCESS',str(message['Subject']))
|
self.assertIn('[Backup-Test] SUCCESS',str(message['Subject']))
|
||||||
|
self.assertIn(f' SUCCESS {socket.getfqdn()} ',str(message['Subject']))
|
||||||
|
self.assertIn(f'Host: {socket.getfqdn()}',body.splitlines())
|
||||||
self.assertEqual(str(message['From']),'app-backup@example.invalid')
|
self.assertEqual(str(message['From']),'app-backup@example.invalid')
|
||||||
self.assertIn('backup@example.invalid',str(message['To']))
|
self.assertIn('backup@example.invalid',str(message['To']))
|
||||||
for value in ['Status: SUCCESS','Exit code: 0','Dauer:', 'db staged:',
|
for value in ['Status: SUCCESS','Exit code: 0','Dauer:', 'db staged:',
|
||||||
@@ -1000,12 +1007,41 @@ REPORT_SECRET='config-content-must-not-enter-the-mail'
|
|||||||
self.assertEqual(saved.read_bytes(),(self.base/'mail.eml').read_bytes())
|
self.assertEqual(saved.read_bytes(),(self.base/'mail.eml').read_bytes())
|
||||||
self.assertEqual(saved.stat().st_mode & 0o777,0o600)
|
self.assertEqual(saved.stat().st_mode & 0o777,0o600)
|
||||||
|
|
||||||
|
def test_report_hostname_override_preserves_default_sender(self):
|
||||||
|
self.enable_report()
|
||||||
|
with self.config.open('a') as f:
|
||||||
|
f.write('MAIL_HOSTNAME="gemini.jr-it-services.de"\nMAIL_FROM=""\n')
|
||||||
|
run=self.backup()
|
||||||
|
message,body=self.read_report()
|
||||||
|
self.assertEqual(str(message['Subject']),f'[Backup-Test] SUCCESS gemini.jr-it-services.de {run.name}')
|
||||||
|
self.assertIn('Host: gemini.jr-it-services.de',body.splitlines())
|
||||||
|
self.assertEqual(str(message['From']),f'app-backup@{socket.getfqdn()}')
|
||||||
|
|
||||||
|
def test_report_empty_hostname_uses_system_fqdn(self):
|
||||||
|
self.enable_report()
|
||||||
|
with self.config.open('a') as f: f.write('MAIL_HOSTNAME=""\n')
|
||||||
|
self.backup()
|
||||||
|
message,body=self.read_report()
|
||||||
|
self.assertIn(f' SUCCESS {socket.getfqdn()} ',str(message['Subject']))
|
||||||
|
self.assertIn(f'Host: {socket.getfqdn()}',body.splitlines())
|
||||||
|
|
||||||
|
def test_report_hostname_header_injection_is_rejected(self):
|
||||||
|
self.enable_report()
|
||||||
|
with self.config.open('a') as f:
|
||||||
|
f.write("MAIL_HOSTNAME=$'gemini\\nBcc: attacker@example.invalid'\n")
|
||||||
|
result=self.run_script('app-backup.sh')
|
||||||
|
self.assertIn('mail report failed (ValueError)',result.stdout)
|
||||||
|
self.assertFalse((self.base/'mail.eml').exists())
|
||||||
|
|
||||||
def test_report_failure_names_phase_and_has_final_error(self):
|
def test_report_failure_names_phase_and_has_final_error(self):
|
||||||
self.enable_report()
|
self.enable_report()
|
||||||
|
with self.config.open('a') as f: f.write('MAIL_HOSTNAME="gemini.jr-it-services.de"\n')
|
||||||
result=self.run_script('app-backup.sh',ok=False,TEST_FAIL_TOOL='mariadb-dump')
|
result=self.run_script('app-backup.sh',ok=False,TEST_FAIL_TOOL='mariadb-dump')
|
||||||
message,body=self.read_report()
|
message,body=self.read_report()
|
||||||
self.assertEqual(result.returncode,42)
|
self.assertEqual(result.returncode,42)
|
||||||
self.assertIn('[Backup-Test] FAIL',str(message['Subject']))
|
self.assertIn('[Backup-Test] FAIL',str(message['Subject']))
|
||||||
|
self.assertIn(' FAIL gemini.jr-it-services.de ',str(message['Subject']))
|
||||||
|
self.assertIn('Host: gemini.jr-it-services.de',body.splitlines())
|
||||||
self.assertIn('Exit code: 42',body)
|
self.assertIn('Exit code: 42',body)
|
||||||
self.assertIn('Letzte Phase: DB-Dump:',body)
|
self.assertIn('Letzte Phase: DB-Dump:',body)
|
||||||
self.assertIn('FAILED (exit 42)',body)
|
self.assertIn('FAILED (exit 42)',body)
|
||||||
|
|||||||
Reference in new issue
Block a user