Support home-based mailboxes during automated upgrades
This commit is contained in:
1 parent
8e6c9a058f
commit
56fbcdad74
8 files changed
+194
-9
No files matched your search
+14
-2
@@ -83,7 +83,7 @@ Ein neuer Lauf heißt beispielsweise `appbackup_2026-09-10_02-30-00_a1b2c3d4`. Z
|
|||||||
| `nextcloud-data` | `NC_DATA_DIR`, sofern aktiviert |
|
| `nextcloud-data` | `NC_DATA_DIR`, sofern aktiviert |
|
||||||
| `gitea`, `gitea-etc` | `GITEA_DATA_DIR`, `GITEA_ETC_DIR` |
|
| `gitea`, `gitea-etc` | `GITEA_DATA_DIR`, `GITEA_ETC_DIR` |
|
||||||
| `gitea-repos`, `gitea-custom` | Optionale explizite externe Repository-/Custom-Pfade |
|
| `gitea-repos`, `gitea-custom` | Optionale explizite externe Repository-/Custom-Pfade |
|
||||||
| `mail`, `postfix`, `dovecot` | Mailstore und beide Konfigurationsverzeichnisse |
|
| `mail`, `postfix`, `dovecot` | Zentraler Mailstore (bei `ENABLE_MAIL_STORE=true`) und beide Konfigurationsverzeichnisse |
|
||||||
| `mail-home-0`, `mail-home-1`, … | Reihenfolge von `MAIL_HOME_DIRS`, mit Excludes und Dateisystemgrenze |
|
| `mail-home-0`, `mail-home-1`, … | Reihenfolge von `MAIL_HOME_DIRS`, mit Excludes und Dateisystemgrenze |
|
||||||
| `extra-0`, `extra-1`, … | Reihenfolge von `EXTRA_PATHS` |
|
| `extra-0`, `extra-1`, … | Reihenfolge von `EXTRA_PATHS` |
|
||||||
|
|
||||||
@@ -156,6 +156,17 @@ Gitea: `APP_DATA_PATH`, `[repository] ROOT`, Custom-Verzeichnis und `app.ini` pr
|
|||||||
|
|
||||||
`MAIL_BACKUP_MODE=live` und abgeschaltete Stopps sind bewusste Ausnahmen mit Konsistenzrisiko. Das Log weist darauf hin. Home-Verzeichnisse werden mit `--one-file-system` kopiert: Daten anderer Mounts fehlen dann absichtlich. Mailqueues, zusätzliche Spamfilter-/DKIM-Verzeichnisse oder Maildatenbanken sind nur gesichert, wenn sie explizit im Umfang stehen.
|
`MAIL_BACKUP_MODE=live` und abgeschaltete Stopps sind bewusste Ausnahmen mit Konsistenzrisiko. Das Log weist darauf hin. Home-Verzeichnisse werden mit `--one-file-system` kopiert: Daten anderer Mounts fehlen dann absichtlich. Mailqueues, zusätzliche Spamfilter-/DKIM-Verzeichnisse oder Maildatenbanken sind nur gesichert, wenn sie explizit im Umfang stehen.
|
||||||
|
|
||||||
|
`ENABLE_MAIL_STORE=true` bleibt der kompatible Default: `MAIL_DIR` muss existieren.
|
||||||
|
Bei Mailboxen ausschließlich in Benutzerverzeichnissen kann explizit
|
||||||
|
`ENABLE_MAIL_STORE=false` gesetzt werden. Dann sind aktivierte, nicht leere
|
||||||
|
`MAIL_HOME_DIRS` zwingend; fehlende Homes lassen die Vorprüfung scheitern.
|
||||||
|
Postfix/Dovecot-Konfigurationen und Dienststopps bleiben erhalten. Der Restore
|
||||||
|
verwendet dieselbe Komponentenzuordnung und stellt Home-Mailboxen über
|
||||||
|
`mail-home-N` wieder her. Die Vollständigkeit der Benutzerliste sowie Excludes,
|
||||||
|
Symlinks und Mountgrenzen muss der Betreiber prüfen. Der Paket-Updater bietet
|
||||||
|
bei fehlendem zentralem Verzeichnis einen Pfaddialog oder eine ausdrückliche
|
||||||
|
Homes-Bestätigung; Details stehen in [UPDATE_ANLEITUNG.md](UPDATE_ANLEITUNG.md).
|
||||||
|
|
||||||
### Speicher, Retention und Protokolle
|
### Speicher, Retention und Protokolle
|
||||||
|
|
||||||
`MIN_FREE_GB` ist eine Reserveprüfung auf den Staging- und Archivdateisystemen, keine genaue Prognose. Die Kopie braucht Platz für unkomprimierte Quelldaten und Dumps; Archive benötigen zusätzlich komprimierten Platz. Beim Restore kommen die private Kopie des Sicherungssatzes und entpackte Daten hinzu. Eine Platte kann zwischen Prüfungen volllaufen; der Lauf muss dann scheitern. Ausreichend Reserve und ein externes Kapazitätsmonitoring einplanen.
|
`MIN_FREE_GB` ist eine Reserveprüfung auf den Staging- und Archivdateisystemen, keine genaue Prognose. Die Kopie braucht Platz für unkomprimierte Quelldaten und Dumps; Archive benötigen zusätzlich komprimierten Platz. Beim Restore kommen die private Kopie des Sicherungssatzes und entpackte Daten hinzu. Eine Platte kann zwischen Prüfungen volllaufen; der Lauf muss dann scheitern. Ausreichend Reserve und ein externes Kapazitätsmonitoring einplanen.
|
||||||
@@ -293,10 +304,11 @@ 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.
|
||||||
|
|
||||||
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **30 Tests erfolgreich**, 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.
|
Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **34 Tests erfolgreich**, 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.
|
||||||
|
|
||||||
Geprüfte Fälle umfassen:
|
Geprüfte Fälle umfassen:
|
||||||
|
|
||||||
|
- Fehlender Mailstore ohne Live-Änderung, explizite Pfadkorrektur, Homes-only-Update samt tatsächlichem Maildatei-Restore und Ablehnung unvollständiger Home-Konfigurationen.
|
||||||
- Shell-Syntax und ShellCheck der Paketdateien.
|
- Shell-Syntax und ShellCheck der Paketdateien.
|
||||||
- MyISAM-Dump und tatsächlicher SQL-Import samt Dateiwiederherstellung.
|
- MyISAM-Dump und tatsächlicher SQL-Import samt Dateiwiederherstellung.
|
||||||
- gzip/zstd, korrekte Excludes, vorhandener Wartungsmodus und inaktive Dienste.
|
- gzip/zstd, korrekte Excludes, vorhandener Wartungsmodus und inaktive Dienste.
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ RCLONE_REMOTE_BASE="OneDriveCrypt:gemini" # vorhandenes Crypt-Ziel beibehalten!
|
|||||||
RETENTION_KEEP_LAST=3
|
RETENTION_KEEP_LAST=3
|
||||||
ENABLE_REMOTE_RETENTION="false" # während der Migration zunächst aus
|
ENABLE_REMOTE_RETENTION="false" # während der Migration zunächst aus
|
||||||
MAIL_BACKUP_MODE="stop"
|
MAIL_BACKUP_MODE="stop"
|
||||||
|
# Zentrale Mailablage muss existieren; bei ausschließlich in Homes liegenden
|
||||||
|
# Mailboxen ENABLE_MAIL_STORE=false und MAIL_HOME_DIRS vollständig konfigurieren.
|
||||||
|
ENABLE_MAIL_STORE="true"
|
||||||
# Tatsächliche Timer-/Service-Namen einsetzen; Timer vor seinem Service nennen:
|
# Tatsächliche Timer-/Service-Namen einsetzen; Timer vor seinem Service nennen:
|
||||||
QUIESCE_SERVICES=()
|
QUIESCE_SERVICES=()
|
||||||
# Gitea war in der bisherigen mitgelieferten Konfiguration NICHT aktiviert:
|
# Gitea war in der bisherigen mitgelieferten Konfiguration NICHT aktiviert:
|
||||||
|
|||||||
+41
-1
@@ -55,6 +55,19 @@ für Tests/Diagnose gedacht; dabei besteht keine Absicherung gegen SSH-Abbruch.
|
|||||||
|
|
||||||
## Gezielte Rückfragen und Grenzen
|
## Gezielte Rückfragen und Grenzen
|
||||||
|
|
||||||
|
- **Zentrales Mailverzeichnis fehlt, etwa `/var/vmail`:** Der Installer fragt vor
|
||||||
|
der Installation nach dem tatsächlichen absoluten Pfad. Alternativ lässt sich
|
||||||
|
dieser mit `--mail-dir /tatsaechliche/mailablage` übergeben. Kein leeres
|
||||||
|
Ersatzverzeichnis anlegen: Das würde fehlende Maildaten nur verdecken.
|
||||||
|
Liegen **alle** Mailboxen in den angezeigten `MAIL_HOME_DIRS`, im Dialog `homes`
|
||||||
|
wählen und die vollständige Abdeckung bestätigen. `--mail-homes-only` erteilt
|
||||||
|
dieselbe ausdrückliche Bestätigung ohne Dialog. Dabei müssen die Home-Sicherung
|
||||||
|
aktiviert und alle konfigurierten Homes vorhanden sein. Excludes und
|
||||||
|
Dateisystemgrenzen gelten weiterhin; ausgeschlossene Verzeichnisse, verlinkte
|
||||||
|
externe Mailablagen und weitere Mounts sind dadurch nicht abgedeckt.
|
||||||
|
Postfix/Dovecot-Konfigurationen und Maildienst-Stopps bleiben aktiv.
|
||||||
|
Bei unbekannter Mailablage abbrechen und zuerst die Mailserver-Konfiguration
|
||||||
|
prüfen. Ein fehlender Pfad wird nie automatisch übersprungen.
|
||||||
- **Rclone-Datei nicht eindeutig:** Pfad im Dialog oder mit `--rclone-config FILE`
|
- **Rclone-Datei nicht eindeutig:** Pfad im Dialog oder mit `--rclone-config FILE`
|
||||||
angeben. Eine user-eigene Datei wird in eine neue root-eigene Datei kopiert;
|
angeben. Eine user-eigene Datei wird in eine neue root-eigene Datei kopiert;
|
||||||
die vom Benutzer verwendete Originaldatei bleibt unverändert.
|
die vom Benutzer verwendete Originaldatei bleibt unverändert.
|
||||||
@@ -94,6 +107,33 @@ bleiben erhalten; beim Aktivieren kann `Persistent=true` einen Lauf nachholen.
|
|||||||
|
|
||||||
## Fehler und Rückweg
|
## Fehler und Rückweg
|
||||||
|
|
||||||
|
Ein Abbruch `Missing source: /var/vmail` bei der **Vorprüfung**, wie beim Paket
|
||||||
|
`8e6c9a058fa9`, erfordert kein Rollback. Mit dem korrigierten Paket erneut starten
|
||||||
|
und die Mailablage klären. Die bisherige Konfiguration und der Timer sind erhalten;
|
||||||
|
der abgebrochene Versuch enthält noch kein erfolgreiches neues Backup.
|
||||||
|
Bereits bewusst getroffene Entscheidungen können als Optionen mitgegeben werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash app-backup-update-VERSION.run --enable-gitea --cron-services crond.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Optionen passen nur, wenn Gitea aufgenommen werden soll und das Pausieren
|
||||||
|
aller Cronjobs über `crond.service` ausdrücklich akzeptiert wurde.
|
||||||
|
|
||||||
|
Für Mailablagen wie `/home/johannes/Mail` unter den konfigurierten Benutzerhomes
|
||||||
|
zusätzlich `--mail-homes-only` verwenden, sofern damit alle Postfächer erfasst sind:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash app-backup-update-VERSION.run \
|
||||||
|
--enable-gitea --cron-services crond.service --mail-homes-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Installer zeigt die übernommenen Home-Verzeichnisse an. `Mail/` einschließlich
|
||||||
|
`cur`, `new`, `tmp` und Dovecot-Dateien wird mit den normalen Home-Excludes der
|
||||||
|
Vorlage vollständig kopiert, sofern es auf demselben Dateisystem liegt und kein
|
||||||
|
Verweis auf eine externe Ablage ist. Weitere Benutzer müssen in `MAIL_HOME_DIRS`
|
||||||
|
stehen; das Vorhandensein eines einzelnen Postfachs belegt nicht deren Abdeckung.
|
||||||
|
|
||||||
Bei einem Fehler nach Beginn der Installation bleibt der Backup-Timer gestoppt.
|
Bei einem Fehler nach Beginn der Installation bleibt der Backup-Timer gestoppt.
|
||||||
Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder
|
Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder
|
||||||
teilweise wiederhergestellte Anwendungen. Logs und gegebenenfalls
|
teilweise wiederhergestellte Anwendungen. Logs und gegebenenfalls
|
||||||
@@ -125,7 +165,7 @@ Das ausführbare Paket ausschließlich über euren vertrauenswürdigen Kanal bez
|
|||||||
|
|
||||||
## Verifikation der Lieferung
|
## Verifikation der Lieferung
|
||||||
|
|
||||||
30 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
34 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich.
|
||||||
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. Die tatsächliche
|
systemd/occ und die systemd-run-Übergabe sind simuliert. Die tatsächliche
|
||||||
Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore
|
Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ GITEA_DB_CNF="/etc/app-backup/db-gitea.cnf"
|
|||||||
|
|
||||||
ENABLE_MAIL="true"
|
ENABLE_MAIL="true"
|
||||||
MAIL_BACKUP_MODE="stop"
|
MAIL_BACKUP_MODE="stop"
|
||||||
|
# Nur false setzen, wenn alle Mailboxen durch MAIL_HOME_DIRS abgedeckt sind.
|
||||||
|
# Postfix/Dovecot und deren Dienststopps bleiben auch dann aktiviert.
|
||||||
|
ENABLE_MAIL_STORE="true"
|
||||||
MAIL_DIR="/var/vmail"
|
MAIL_DIR="/var/vmail"
|
||||||
POSTFIX_DIR="/etc/postfix"
|
POSTFIX_DIR="/etc/postfix"
|
||||||
DOVECOT_DIR="/etc/dovecot"
|
DOVECOT_DIR="/etc/dovecot"
|
||||||
|
|||||||
+10
-4
@@ -41,7 +41,7 @@ load_config() {
|
|||||||
: "${GITEA_DATA_DIR:=/var/lib/gitea/data}" "${GITEA_ETC_DIR:=/etc/gitea}"
|
: "${GITEA_DATA_DIR:=/var/lib/gitea/data}" "${GITEA_ETC_DIR:=/etc/gitea}"
|
||||||
: "${ENABLE_HTTPD_STOP:=true}" "${HTTPD_SERVICE_NAME:=httpd}"
|
: "${ENABLE_HTTPD_STOP:=true}" "${HTTPD_SERVICE_NAME:=httpd}"
|
||||||
: "${ENABLE_PHPFPM_STOP:=true}" "${PHPFPM_SERVICE_NAME:=php-fpm}"
|
: "${ENABLE_PHPFPM_STOP:=true}" "${PHPFPM_SERVICE_NAME:=php-fpm}"
|
||||||
: "${MAIL_BACKUP_MODE:=stop}" "${ENABLE_MAIL_HOME_DIRS:=true}"
|
: "${MAIL_BACKUP_MODE:=stop}" "${ENABLE_MAIL_HOME_DIRS:=true}" "${ENABLE_MAIL_STORE:=true}"
|
||||||
: "${MAIL_DIR:=/var/vmail}" "${POSTFIX_DIR:=/etc/postfix}" "${DOVECOT_DIR:=/etc/dovecot}"
|
: "${MAIL_DIR:=/var/vmail}" "${POSTFIX_DIR:=/etc/postfix}" "${DOVECOT_DIR:=/etc/dovecot}"
|
||||||
: "${RESTORE_STRICT_DELETE:=false}"
|
: "${RESTORE_STRICT_DELETE:=false}"
|
||||||
# Preserve old space-separated lists; arrays additionally support spaces in paths.
|
# Preserve old space-separated lists; arrays additionally support spaces in paths.
|
||||||
@@ -55,7 +55,7 @@ load_config() {
|
|||||||
read -r -a MAIL_HOME_EXCLUDES <<< "${MAIL_HOME_EXCLUDES[0]}"
|
read -r -a MAIL_HOME_EXCLUDES <<< "${MAIL_HOME_EXCLUDES[0]}"
|
||||||
fi
|
fi
|
||||||
for v in ENABLE_UPLOAD ENABLE_REMOTE_RETENTION ENABLE_MAIL_REPORT ENABLE_WORDPRESS ENABLE_NEXTCLOUD \
|
for v in ENABLE_UPLOAD ENABLE_REMOTE_RETENTION ENABLE_MAIL_REPORT ENABLE_WORDPRESS ENABLE_NEXTCLOUD \
|
||||||
ENABLE_NEXTCLOUD_DATA ENABLE_GITEA ENABLE_MAIL ENABLE_DB_DUMPS ENABLE_NEXTCLOUD_MAINTENANCE \
|
ENABLE_NEXTCLOUD_DATA ENABLE_GITEA ENABLE_MAIL ENABLE_MAIL_STORE ENABLE_DB_DUMPS ENABLE_NEXTCLOUD_MAINTENANCE \
|
||||||
ENABLE_GITEA_SERVICE_STOP ENABLE_HTTPD_STOP ENABLE_PHPFPM_STOP ENABLE_MAIL_HOME_DIRS RESTORE_STRICT_DELETE; do
|
ENABLE_GITEA_SERVICE_STOP ENABLE_HTTPD_STOP ENABLE_PHPFPM_STOP ENABLE_MAIL_HOME_DIRS RESTORE_STRICT_DELETE; do
|
||||||
[[ ${!v} == true || ${!v} == false ]] || die "$v must be true or false"
|
[[ ${!v} == true || ${!v} == false ]] || die "$v must be true or false"
|
||||||
done
|
done
|
||||||
@@ -67,6 +67,10 @@ load_config() {
|
|||||||
[[ $ARCHIVE_PREFIX =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || die "Unsafe ARCHIVE_PREFIX"
|
[[ $ARCHIVE_PREFIX =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || die "Unsafe ARCHIVE_PREFIX"
|
||||||
[[ $COMPRESSOR == zstd || $COMPRESSOR == gzip ]] || die "COMPRESSOR must be zstd or gzip"
|
[[ $COMPRESSOR == zstd || $COMPRESSOR == gzip ]] || die "COMPRESSOR must be zstd or gzip"
|
||||||
[[ $MAIL_BACKUP_MODE == stop || $MAIL_BACKUP_MODE == live ]] || die "MAIL_BACKUP_MODE must be stop or live"
|
[[ $MAIL_BACKUP_MODE == stop || $MAIL_BACKUP_MODE == live ]] || die "MAIL_BACKUP_MODE must be stop or live"
|
||||||
|
# A homes-only setup must still contain mailbox data, not just server config.
|
||||||
|
if [[ $ENABLE_MAIL == true && $ENABLE_MAIL_STORE == false ]]; then
|
||||||
|
[[ $ENABLE_MAIL_HOME_DIRS == true && ${#MAIL_HOME_DIRS[@]} -gt 0 ]] || die 'Mail without a central store requires enabled, nonempty MAIL_HOME_DIRS'
|
||||||
|
fi
|
||||||
if [[ -z ${DB_DUMP_BIN:-} ]]; then DB_DUMP_BIN=$(command -v mariadb-dump || command -v mysqldump || true); fi
|
if [[ -z ${DB_DUMP_BIN:-} ]]; then DB_DUMP_BIN=$(command -v mariadb-dump || command -v mysqldump || true); fi
|
||||||
if [[ -z ${DB_CLIENT_BIN:-} ]]; then DB_CLIENT_BIN=$(command -v mariadb || command -v mysql || true); fi
|
if [[ -z ${DB_CLIENT_BIN:-} ]]; then DB_CLIENT_BIN=$(command -v mariadb || command -v mysql || true); fi
|
||||||
}
|
}
|
||||||
@@ -78,7 +82,8 @@ guard_workspaces() {
|
|||||||
if [[ $ENABLE_NEXTCLOUD == true ]]; then sources+=("${NC_DIR:-}" "${NC_DATA_DIR:-}"); fi
|
if [[ $ENABLE_NEXTCLOUD == true ]]; then sources+=("${NC_DIR:-}" "${NC_DATA_DIR:-}"); fi
|
||||||
if [[ $ENABLE_GITEA == true ]]; then sources+=("$GITEA_DATA_DIR" "$GITEA_ETC_DIR" "${GITEA_REPOS_DIR:-}" "${GITEA_CUSTOM_DIR:-}"); fi
|
if [[ $ENABLE_GITEA == true ]]; then sources+=("$GITEA_DATA_DIR" "$GITEA_ETC_DIR" "${GITEA_REPOS_DIR:-}" "${GITEA_CUSTOM_DIR:-}"); fi
|
||||||
if [[ $ENABLE_MAIL == true ]]; then
|
if [[ $ENABLE_MAIL == true ]]; then
|
||||||
sources+=("$MAIL_DIR" "$POSTFIX_DIR" "$DOVECOT_DIR")
|
if [[ $ENABLE_MAIL_STORE == true ]]; then sources+=("$MAIL_DIR"); fi
|
||||||
|
sources+=("$POSTFIX_DIR" "$DOVECOT_DIR")
|
||||||
if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then sources+=("${MAIL_HOME_DIRS[@]}"); fi
|
if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then sources+=("${MAIL_HOME_DIRS[@]}"); fi
|
||||||
fi
|
fi
|
||||||
# Validate the WHOLE plan before mkdir/chmod touches any configured directory.
|
# Validate the WHOLE plan before mkdir/chmod touches any configured directory.
|
||||||
@@ -149,7 +154,8 @@ build_plan() {
|
|||||||
add_db gitea GITEA
|
add_db gitea GITEA
|
||||||
fi
|
fi
|
||||||
if [[ $ENABLE_MAIL == true ]]; then
|
if [[ $ENABLE_MAIL == true ]]; then
|
||||||
add_source mail "$MAIL_DIR"; add_source postfix "$POSTFIX_DIR"; add_source dovecot "$DOVECOT_DIR"
|
if [[ $ENABLE_MAIL_STORE == true ]]; then add_source mail "$MAIL_DIR"; fi
|
||||||
|
add_source postfix "$POSTFIX_DIR"; add_source dovecot "$DOVECOT_DIR"
|
||||||
if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then
|
if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then
|
||||||
local i=0
|
local i=0
|
||||||
for p in "${MAIL_HOME_DIRS[@]}"; do add_source "mail-home-$i" "$p"; i=$((i+1)); done
|
for p in "${MAIL_HOME_DIRS[@]}"; do add_source "mail-home-$i" "$p"; i=$((i+1)); done
|
||||||
|
|||||||
@@ -440,6 +440,76 @@ QUIESCE_SERVICES=(nc-cron)
|
|||||||
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||||
self.assertNotIn('systemctl stop',self.events.read_text())
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
|
||||||
|
def test_missing_mail_store_requires_explicit_resolution(self):
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
with canonical.open('a') as f: f.write(f'MAIL_DIR="{self.src}/missing-mail"\n')
|
||||||
|
original=canonical.read_bytes()
|
||||||
|
package=self.build_package()
|
||||||
|
result=subprocess.run(['bash',str(package),'--foreground'],env=self.env,capture_output=True,text=True,timeout=60)
|
||||||
|
self.assertNotEqual(result.returncode,0)
|
||||||
|
self.assertIn('--mail-dir DIR',result.stdout+result.stderr)
|
||||||
|
self.assertEqual(canonical.read_bytes(),original)
|
||||||
|
self.assertFalse((self.src/'missing-mail').exists())
|
||||||
|
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
|
||||||
|
def test_package_mail_directory_override(self):
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
with canonical.open('a') as f: f.write(f'MAIL_DIR="{self.src}/missing-mail"\n')
|
||||||
|
package=self.build_package()
|
||||||
|
result=subprocess.run(['bash',str(package),'--foreground','--mail-dir',str(self.src/'mail')],env=self.env,capture_output=True,text=True,timeout=120)
|
||||||
|
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||||
|
self.assertIn('ENABLE_MAIL_STORE=true',canonical.read_text())
|
||||||
|
run=next((self.base/'work/archives').glob('appbackup_*'))
|
||||||
|
self.assertTrue((run/'mail.tar.zst').is_file())
|
||||||
|
self.assertTrue((run/'UPLOADED').is_file())
|
||||||
|
|
||||||
|
def test_package_homes_only_keeps_mail_configs_and_service_stops(self):
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
home=self.src/'johannes'
|
||||||
|
for folder in ['cur','new','tmp']:
|
||||||
|
(home/'Mail'/folder).mkdir(parents=True)
|
||||||
|
mailbox=home/'Mail/cur/test-message:2,S'
|
||||||
|
mailbox.write_bytes(b'Subject: retained mail\n\nMailbox content\n')
|
||||||
|
mailbox.chmod(0o600)
|
||||||
|
original=mailbox.read_bytes()
|
||||||
|
with canonical.open('a') as f:
|
||||||
|
f.write(f'MAIL_DIR="{self.src}/missing-mail"\nENABLE_MAIL_HOME_DIRS=true\nMAIL_HOME_DIRS=("{home}")\n')
|
||||||
|
package=self.build_package()
|
||||||
|
result=subprocess.run(['bash',str(package),'--foreground','--mail-homes-only'],env=self.env,capture_output=True,text=True,timeout=120)
|
||||||
|
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
|
||||||
|
self.assertIn('ENABLE_MAIL_STORE=false',canonical.read_text())
|
||||||
|
run=next((self.base/'work/archives').glob('appbackup_*'))
|
||||||
|
for name in ['mail-home-0','postfix','dovecot']:
|
||||||
|
self.assertTrue((run/f'{name}.tar.zst').is_file())
|
||||||
|
self.assertFalse((run/'mail.tar.zst').exists())
|
||||||
|
self.assertTrue((run/'UPLOADED').is_file())
|
||||||
|
for unit in ['postfix','dovecot']:
|
||||||
|
self.assertIn(f'systemctl stop {unit}',self.events.read_text())
|
||||||
|
self.assertEqual(json.loads(self.state.read_text())['services'][unit],'active')
|
||||||
|
mailbox.write_text('changed after backup')
|
||||||
|
restored=subprocess.run(['bash',str(REPO/'usr-local-sbin/app-restore.sh'),'--config',str(canonical),
|
||||||
|
'--local-run',str(run),'--components','mail-home-0','--apply','--yes'],
|
||||||
|
env=self.env,capture_output=True,text=True,timeout=60)
|
||||||
|
self.assertEqual(restored.returncode,0,restored.stdout+restored.stderr)
|
||||||
|
self.assertEqual(mailbox.read_bytes(),original)
|
||||||
|
self.assertEqual(mailbox.stat().st_mode & 0o777,0o600)
|
||||||
|
|
||||||
|
def test_homes_only_rejects_disabled_empty_and_missing_homes(self):
|
||||||
|
canonical=self.configure_upgrade()
|
||||||
|
original=canonical.read_text()
|
||||||
|
package=self.build_package()
|
||||||
|
for settings in ['ENABLE_MAIL_HOME_DIRS=false', 'ENABLE_MAIL_HOME_DIRS=true\nMAIL_HOME_DIRS=()',
|
||||||
|
f'ENABLE_MAIL_HOME_DIRS=true\nMAIL_HOME_DIRS=("{self.src}/missing-home")']:
|
||||||
|
with self.subTest(settings=settings):
|
||||||
|
canonical.write_text(original+'\n'+settings+'\n')
|
||||||
|
before=canonical.read_bytes()
|
||||||
|
result=subprocess.run(['bash',str(package),'--foreground','--mail-homes-only'],env=self.env,capture_output=True,text=True,timeout=60)
|
||||||
|
self.assertNotEqual(result.returncode,0,result.stdout+result.stderr)
|
||||||
|
self.assertEqual(canonical.read_bytes(),before)
|
||||||
|
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
|
||||||
|
self.assertNotIn('systemctl stop',self.events.read_text())
|
||||||
|
|
||||||
def test_package_failed_backup_keeps_timer_stopped_and_rollback_available(self):
|
def test_package_failed_backup_keeps_timer_stopped_and_rollback_available(self):
|
||||||
canonical=self.configure_upgrade()
|
canonical=self.configure_upgrade()
|
||||||
original=canonical.read_bytes()
|
original=canonical.read_bytes()
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def build(output, version):
|
|||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
umask 077
|
umask 077
|
||||||
if [[ ${{1:-}} == --help ]]; then
|
if [[ ${{1:-}} == --help ]]; then
|
||||||
echo 'Usage: sudo bash app-backup-update-{version}.run [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--install-deps]'
|
echo 'Usage: sudo bash app-backup-update-{version}.run [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--mail-dir DIR | --mail-homes-only] [--install-deps]'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
[[ $EUID == 0 ]] || {{ echo 'Bitte mit sudo bash starten.' >&2; exit 1; }}
|
[[ $EUID == 0 ]] || {{ echo 'Bitte mit sudo bash starten.' >&2; exit 1; }}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ UP_INSTALL_DEPS=false
|
|||||||
UP_GITEA=''
|
UP_GITEA=''
|
||||||
UP_RCLONE=''
|
UP_RCLONE=''
|
||||||
UP_CRON=''
|
UP_CRON=''
|
||||||
|
UP_MAIL_DIR=''
|
||||||
|
UP_MAIL_HOMES=false
|
||||||
UP_STATE=''
|
UP_STATE=''
|
||||||
UP_WORKER=false
|
UP_WORKER=false
|
||||||
while (( $# )); do
|
while (( $# )); do
|
||||||
@@ -26,11 +28,14 @@ while (( $# )); do
|
|||||||
--no-gitea) UP_GITEA=false; shift ;;
|
--no-gitea) UP_GITEA=false; shift ;;
|
||||||
--rclone-config) UP_RCLONE=${2:?Missing rclone configuration}; shift 2 ;;
|
--rclone-config) UP_RCLONE=${2:?Missing rclone configuration}; shift 2 ;;
|
||||||
--cron-services) UP_CRON=${2:?Missing comma-separated systemd units}; shift 2 ;;
|
--cron-services) UP_CRON=${2:?Missing comma-separated systemd units}; shift 2 ;;
|
||||||
|
--mail-dir) UP_MAIL_DIR=${2:?Missing mailbox directory}; shift 2 ;;
|
||||||
|
--mail-homes-only) UP_MAIL_HOMES=true; shift ;;
|
||||||
--worker) UP_WORKER=true; UP_STATE=${2:?Missing state}; shift 2 ;;
|
--worker) UP_WORKER=true; UP_STATE=${2:?Missing state}; shift 2 ;;
|
||||||
--help) echo 'Usage: sudo bash update.sh [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--install-deps]'; exit 0 ;;
|
--help) echo 'Usage: sudo bash update.sh [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--mail-dir DIR | --mail-homes-only] [--install-deps]'; exit 0 ;;
|
||||||
*) die "Unknown argument: $1" ;;
|
*) die "Unknown argument: $1" ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
[[ -z $UP_MAIL_DIR || $UP_MAIL_HOMES == false ]] || die 'Choose --mail-dir OR --mail-homes-only'
|
||||||
[[ $EUID == 0 ]] || die 'Run with sudo bash'
|
[[ $EUID == 0 ]] || die 'Run with sudo bash'
|
||||||
need systemctl; need flock
|
need systemctl; need flock
|
||||||
# This lock excludes two coordinators, independently from ordinary backup jobs.
|
# This lock excludes two coordinators, independently from ordinary backup jobs.
|
||||||
@@ -46,6 +51,51 @@ prompt() {
|
|||||||
append_setting() { printf '%s=%q\n' "$1" "$2" >> "$UP_STATE/candidate.conf"; }
|
append_setting() { printf '%s=%q\n' "$1" "$2" >> "$UP_STATE/candidate.conf"; }
|
||||||
discover() { python3 "$LIB_DIR/update-discover.py" "$@"; }
|
discover() { python3 "$LIB_DIR/update-discover.py" "$@"; }
|
||||||
|
|
||||||
|
# Do not infer mailbox coverage from an existing directory. In particular,
|
||||||
|
# creating an empty /var/vmail would make preflight pass without saving mail.
|
||||||
|
prepare_mail() {
|
||||||
|
local choice home
|
||||||
|
if [[ $ENABLE_MAIL != true ]]; then
|
||||||
|
[[ -z $UP_MAIL_DIR && $UP_MAIL_HOMES == false ]] || die 'Mail options require ENABLE_MAIL=true'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -n $UP_MAIL_DIR ]]; then
|
||||||
|
MAIL_DIR=$UP_MAIL_DIR
|
||||||
|
ENABLE_MAIL_STORE=true
|
||||||
|
elif [[ $UP_MAIL_HOMES == true ]]; then
|
||||||
|
ENABLE_MAIL_STORE=false
|
||||||
|
elif [[ $ENABLE_MAIL_STORE == true && ! -d $MAIL_DIR ]]; then
|
||||||
|
echo "Konfiguriertes zentrales Mailverzeichnis fehlt: $MAIL_DIR"
|
||||||
|
printf 'Konfigurierte Mail-Homes: '; printf '%q ' "${MAIL_HOME_DIRS[@]}"; echo
|
||||||
|
echo 'Absoluten Pfad zur tatsaechlichen zentralen Mailablage angeben.'
|
||||||
|
echo 'Nur wenn ALLE Mailboxen in den konfigurierten Homes liegen: homes eingeben.'
|
||||||
|
echo 'Bei unbekannter Ablage abbrechen; kein leeres Ersatzverzeichnis anlegen.'
|
||||||
|
[[ -t 0 ]] || die 'Missing mail source: supply --mail-dir DIR or explicitly confirm coverage with --mail-homes-only'
|
||||||
|
read -r -p 'Mailverzeichnis / homes / Enter zum Abbrechen: ' choice
|
||||||
|
[[ -n $choice ]] || die 'Mail storage unresolved; installed package/config/timer unchanged'
|
||||||
|
if [[ $choice == homes ]]; then
|
||||||
|
printf 'Home-Excludes: '; printf '%q ' "${MAIL_HOME_EXCLUDES[@]}"; echo
|
||||||
|
prompt 'Alle Mailboxen liegen in diesen Homes, ohne ausgeschlossene Pfade oder weitere Mounts? [j/N]' || die 'Mail coverage not confirmed'
|
||||||
|
ENABLE_MAIL_STORE=false
|
||||||
|
else MAIL_DIR=$choice; fi
|
||||||
|
fi
|
||||||
|
if [[ $ENABLE_MAIL_STORE == true ]]; then
|
||||||
|
[[ $MAIL_DIR == /* ]] || die 'MAIL_DIR must be absolute'
|
||||||
|
check_source_path "$MAIL_DIR"
|
||||||
|
append_setting MAIL_DIR "$MAIL_DIR"
|
||||||
|
echo "Zentrales Mailverzeichnis: $MAIL_DIR"
|
||||||
|
else
|
||||||
|
[[ $ENABLE_MAIL_HOME_DIRS == true && ${#MAIL_HOME_DIRS[@]} -gt 0 ]] || die 'Homes-only mail requires enabled, nonempty MAIL_HOME_DIRS'
|
||||||
|
for home in "${MAIL_HOME_DIRS[@]}"; do
|
||||||
|
[[ $home == /* ]] || die 'MAIL_HOME_DIRS must be absolute'
|
||||||
|
check_source_path "$home"
|
||||||
|
done
|
||||||
|
printf 'Mail ausschliesslich in Homes (Excludes und Mountgrenzen gelten): '
|
||||||
|
printf '%q ' "${MAIL_HOME_DIRS[@]}"; echo
|
||||||
|
fi
|
||||||
|
append_setting ENABLE_MAIL_STORE "$ENABLE_MAIL_STORE"
|
||||||
|
}
|
||||||
|
|
||||||
if [[ $UP_WORKER == false ]]; then
|
if [[ $UP_WORKER == false ]]; then
|
||||||
systemctl is-active --quiet app-backup-upgrade.service && die 'Upgrade service is already running'
|
systemctl is-active --quiet app-backup-upgrade.service && die 'Upgrade service is already running'
|
||||||
# Only install missing, small prerequisites when requested. Never upgrade PHP,
|
# Only install missing, small prerequisites when requested. Never upgrade PHP,
|
||||||
@@ -87,6 +137,7 @@ if [[ $UP_WORKER == false ]]; then
|
|||||||
append_setting ENABLE_UPLOAD true
|
append_setting ENABLE_UPLOAD true
|
||||||
append_setting ENABLE_NEXTCLOUD_MAINTENANCE true
|
append_setting ENABLE_NEXTCLOUD_MAINTENANCE true
|
||||||
append_setting MAIL_BACKUP_MODE stop
|
append_setting MAIL_BACKUP_MODE stop
|
||||||
|
prepare_mail
|
||||||
if [[ $ENABLE_WORDPRESS == true || $ENABLE_NEXTCLOUD == true ]]; then
|
if [[ $ENABLE_WORDPRESS == true || $ENABLE_NEXTCLOUD == true ]]; then
|
||||||
for pair in "ENABLE_HTTPD_STOP:$HTTPD_SERVICE_NAME" "ENABLE_PHPFPM_STOP:$PHPFPM_SERVICE_NAME"; do
|
for pair in "ENABLE_HTTPD_STOP:$HTTPD_SERVICE_NAME" "ENABLE_PHPFPM_STOP:$PHPFPM_SERVICE_NAME"; do
|
||||||
name=${pair%%:*}; unit=${pair#*:}
|
name=${pair%%:*}; unit=${pair#*:}
|
||||||
|
|||||||
Reference in new issue
Block a user