Make backup report hostname configurable

This commit is contained in:
admin committed 2026-09-12 18:53:01 +02:00
1 parent 99da6460d2
commit 64eab1d3c2
7 files changed
+121 -7

No files matched your search

+20 -1
View File
@@ -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
(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
Phase, Konfigurations-/Arbeits-/Archivpfade, Kompression, Crypt-Ziel und getrennten
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.
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.
+31 -1
View File
@@ -28,6 +28,11 @@ nicht nachträglich.
Der Abschlussmarker wird außerdem bytegenau bestätigt, einschließlich seiner
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)
Das neue Paket übernimmt Konfigurationsabgleich, Rechteprüfung, Installation,
@@ -229,10 +234,35 @@ ENABLE_MAIL_REPORT=true
MAIL_TO="johannes"
MAIL_FROM="" # Standard: app-backup@<FQDN>
MAIL_SUBJECT_PREFIX="[app-backup]"
MAIL_HOSTNAME="" # Leer: System-FQDN; überschreibt nur Betreff/Bericht
MAIL_INCLUDE_LOG_TAIL_LINES=200
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
ü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
@@ -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
```
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
View File
@@ -1,5 +1,8 @@
# 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 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
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
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
werden. Eine bislang deaktivierte Berichtsfunktion bleibt deaktiviert. Fehlende
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
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;
systemd/occ und die systemd-run-Übergabe sind simuliert. Das Übergabemodell
+3
View File
@@ -41,6 +41,9 @@ MAIL_TO="johannes"
# Empty means app-backup@<FQDN>. Existing sender/subject/tail settings are preserved.
MAIL_FROM=""
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_SENDMAIL_BIN="/usr/sbin/sendmail"
MAIL_SEND_TIMEOUT=30
+2 -1
View File
@@ -37,6 +37,7 @@ load_config() {
: "${RCLONE_BWLIMIT:=0}" "${ENABLE_MAIL_REPORT:=true}" "${MAIL_TO:=johannes}"
# 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_HOSTNAME:=}"
: "${MAIL_SENDMAIL_BIN:=/usr/sbin/sendmail}" "${MAIL_SEND_TIMEOUT:=30}"
: "${ENABLE_WORDPRESS:=false}" "${ENABLE_NEXTCLOUD:=false}" "${ENABLE_NEXTCLOUD_DATA:=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
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" \
--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" \
--config "$CONFIG_FILE" --workdir "$WORKDIR" --archive-dir "$ARCHIVE_DIR" \
--compression "$COMPRESSOR" --remote "$RCLONE_REMOTE_BASE" --sendmail "$MAIL_SENDMAIL_BIN" \
+4 -2
View File
@@ -50,7 +50,8 @@ def human(value):
def deliver(args):
host = socket.getfqdn()
fqdn = socket.getfqdn()
host = args.hostname or fqdn
now = dt.datetime.now().astimezone()
state = 'SUCCESS' if args.exit_code == 0 else 'FAIL'
try:
@@ -89,7 +90,7 @@ def deliver(args):
body.extend(['', f'Log-Auszug (letzte {args.tail_lines} Zeilen)', '-----------------', tail])
message = EmailMessage(policy=SMTP)
# 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():
raise ValueError('Mail recipient and sender must not be empty')
message['From'] = sender
@@ -112,6 +113,7 @@ def main():
for name in ('output', 'staging', 'archives'):
snap.add_argument(name)
mail = commands.add_parser('deliver')
mail.add_argument('--hostname', default='')
for name in ('sizes', 'log', 'output', 'to', 'sender', 'subject-prefix', 'run', 'phase',
'upload-status', 'config', 'workdir', 'archive-dir', 'compression', 'remote', 'sendmail'):
mail.add_argument('--'+name, required=True)
+36
View File
@@ -11,6 +11,7 @@ import os
from pathlib import Path
import shutil
import signal
import socket
import subprocess
import tarfile
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):
self.enable_report()
with self.config.open('a') as f: f.write('MAIL_HOSTNAME="gemini.jr-it-services.de"\n')
canonical=self.configure_upgrade()
original=canonical.read_bytes()
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())
message,body=self.read_report()
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.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())
@@ -987,6 +992,8 @@ REPORT_SECRET='config-content-must-not-enter-the-mail'
run=self.backup()
message,body=self.read_report()
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.assertIn('backup@example.invalid',str(message['To']))
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.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):
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')
message,body=self.read_report()
self.assertEqual(result.returncode,42)
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('Letzte Phase: DB-Dump:',body)
self.assertIn('FAILED (exit 42)',body)